diff --git a/articles/api.py b/articles/api.py index 21fe9c4..931c436 100644 --- a/articles/api.py +++ b/articles/api.py @@ -16,6 +16,7 @@ from articles.models import ( Article, ArticlePDF, + ArticleVersion, Discussion, Reaction, Review, @@ -29,6 +30,7 @@ ArticleOut, ArticlesListOut, ArticleUpdateSchema, + ArticleVersionDiffOut, CommunityArticleStatsResponse, DateCount, Message, @@ -473,6 +475,15 @@ def update_article( } try: + # Save current state as a version before overwriting + latest_version = ArticleVersion.objects.filter(article=article).count() + ArticleVersion.objects.create( + article=article, + version=latest_version + 1, + title=article.title, + abstract=article.abstract, + ) + # Update the article fields only if they are provided article.title = details.payload.title article.abstract = details.payload.abstract @@ -551,6 +562,60 @@ def update_article( return 500, {"message": "An unexpected error occurred. Please try again later."} +@router.get( + "/{article_id}/versions", + response={200: List[ArticleVersionDiffOut], codes_4xx: Message, codes_5xx: Message}, + auth=OptionalJWTAuth, + summary="Get article version history with diffs", +) +def get_article_versions(request, article_id: int): + import difflib + + try: + article = Article.objects.get(id=article_id) + except Article.DoesNotExist: + return 404, {"message": "Article not found."} + + versions = list(ArticleVersion.objects.filter(article=article).order_by("version")) + if not versions: + return 200, [] + + # Build diff pairs: each version vs the next (or current article for the latest) + result = [] + for i, version in enumerate(versions): + if i + 1 < len(versions): + next_title = versions[i + 1].title + next_abstract = versions[i + 1].abstract + else: + next_title = article.title + next_abstract = article.abstract + + title_diff = list( + difflib.unified_diff( + version.title.splitlines(), + next_title.splitlines(), + lineterm="", + ) + ) + abstract_diff = list( + difflib.unified_diff( + version.abstract.splitlines(), + next_abstract.splitlines(), + lineterm="", + ) + ) + result.append( + ArticleVersionDiffOut( + version=version.version, + created_at=version.created_at, + title_diff=title_diff, + abstract_diff=abstract_diff, + ) + ) + + return 200, result + + @router.get( "/", response={ diff --git a/articles/migrations/0034_articleversion.py b/articles/migrations/0034_articleversion.py new file mode 100644 index 0000000..f2e08db --- /dev/null +++ b/articles/migrations/0034_articleversion.py @@ -0,0 +1,41 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("articles", "0033_alter_userflag_entity_type_alter_userflag_flag_type"), + ] + + operations = [ + migrations.CreateModel( + name="ArticleVersion", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("version", models.PositiveIntegerField()), + ("title", models.CharField(max_length=500)), + ("abstract", models.TextField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "article", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="versions", + to="articles.article", + ), + ), + ], + options={ + "ordering": ["-version"], + }, + ), + ] diff --git a/articles/models.py b/articles/models.py index 775a569..d7f445a 100644 --- a/articles/models.py +++ b/articles/models.py @@ -67,6 +67,22 @@ def __str__(self): return self.title +class ArticleVersion(models.Model): + article = models.ForeignKey( + Article, on_delete=models.CASCADE, related_name="versions" + ) + version = models.PositiveIntegerField() + title = models.CharField(max_length=500) + abstract = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ["-version"] + + def __str__(self): + return f"Version {self.version} of {self.article.title}" + + class ArticlePDF(models.Model): def get_pdf_upload_path(instance, filename): # Get file extension diff --git a/articles/schemas.py b/articles/schemas.py index f96e9ed..e941026 100644 --- a/articles/schemas.py +++ b/articles/schemas.py @@ -10,6 +10,7 @@ AnonymousIdentity, Article, ArticlePDF, + ArticleVersion, Discussion, DiscussionComment, DiscussionSubscription, @@ -370,6 +371,19 @@ class ArticleFilters(Schema): community_id: Optional[int] = None +class ArticleVersionOut(ModelSchema): + class Config: + model = ArticleVersion + model_fields = ["id", "version", "title", "abstract", "created_at"] + + +class ArticleVersionDiffOut(Schema): + version: int + created_at: datetime + title_diff: List[str] + abstract_diff: List[str] + + """ Article Reviews Schemas for serialization and validation """