Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(fix): fix related articles logic #199

Merged
merged 2 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions server/apps/research/admin/article_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,23 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'to_article':
# Get the parent object (Article) from the request
obj_id = request.resolver_match.kwargs.get('object_id')
parent_obj = None
# For new articles (when obj_id is None), show all ready articles
base_queryset = Article.objects.filter(status='ready')

if obj_id:
try:
parent_obj = Article.objects.get(pk=obj_id)
# Exclude self-reference and articles that already have a relationship
base_queryset = base_queryset.exclude(
id=parent_obj.id
).exclude(
related_from__to_article=parent_obj
)
except Article.DoesNotExist:
pass

base_queryset = Article.objects.filter(status='ready')
if parent_obj:
# Exclude self-reference and articles that already have a relationship
base_queryset = base_queryset.exclude(
id=parent_obj.id
).exclude(
related_from__to_article=parent_obj
)
kwargs['queryset'] = base_queryset
return super().formfield_for_foreignkey(db_field, request, **kwargs)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
Expand All @@ -56,9 +55,18 @@ def current_slug_history(self, obj):
current_slug_history.short_description = 'Slug Change History'

def get_inlines(self, request, obj):
if obj is None:
return []
return super().get_inlines(request, obj)
# Allow inlines for both new and existing articles
return [RelatedArticleInline]

def save_related(self, request, form, formsets, change):
"""Handle saving related articles for both new and existing articles."""
super().save_related(request, form, formsets, change)

# Process related articles from inline formsets
for formset in formsets:
if isinstance(formset, RelatedArticleInline):
# The related articles will be saved automatically through the formset
pass

fieldsets = [
('Article Details', {'fields': ['title', 'slug', 'authors', 'acknowledgement', 'categories', 'thumb', 'content', 'summary', 'status', 'scheduled_publish_time']}),
Expand Down
63 changes: 50 additions & 13 deletions server/apps/research/models/article.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ def build_table_of_contents(self):

def save(self, *args, **kwargs):
"""Override the save method to generate a unique slug and build table of contents."""
is_new = self.pk is None
temp_related_articles = []

# If this is a new article and there are related articles in the form
if is_new and hasattr(self, "_temp_related_articles"):
temp_related_articles = self._temp_related_articles
delattr(self, "_temp_related_articles")

if not self.slug or self.title_update():
self.slug = self.generate_unique_slug()

Expand Down Expand Up @@ -150,7 +158,24 @@ def save(self, *args, **kwargs):
):
self.status = "ready"

super().save(*args, **kwargs)
with transaction.atomic():
super().save(*args, **kwargs)

# If this was a new article and we had temporary related articles
if is_new and temp_related_articles:
for related_article in temp_related_articles:
RelatedArticle.objects.create(
from_article=self, to_article=related_article
)

def set_temp_related_articles(self, related_articles):
"""
Store related articles temporarily before the initial save.

Args:
related_articles: List of Article instances to be related
"""
self._temp_related_articles = related_articles

def generate_unique_slug(self):
"""Generate a unique slug for the article."""
Expand All @@ -174,7 +199,7 @@ def title_update(self):

class RelatedArticle(models.Model):
"""Through model for related articles to prevent circular references."""

from_article = models.ForeignKey(
Article, on_delete=models.CASCADE, related_name="related_from"
)
Expand All @@ -191,23 +216,35 @@ class Meta:
name="prevent_self_reference",
)
]

def clean(self):
# Prevent direct circular references
if RelatedArticle.objects.filter(
from_article=self.to_article, to_article=self.from_article
).exists():
if (
self.from_article.pk
and RelatedArticle.objects.filter(
from_article=self.to_article, to_article=self.from_article
).exists()
):
raise ValidationError("Circular references detected.")

# Maximum of 3 related articles
if RelatedArticle.objects.filter(from_article=self.from_article).count() >= 3:
if (
self.from_article.pk
and RelatedArticle.objects.filter(from_article=self.from_article).count()
>= 3
):
raise ValidationError("Maximum of 3 related articles allowed.")

def save(self, *args, **kwargs):
# Acquire a lock on related articles for this from_article
RelatedArticle.objects.select_for_update().filter(from_article=self.from_article)
self.clean()
super().save(*args, **kwargs)
with transaction.atomic():
# Only acquire lock if from_article exists in database
if self.from_article.pk:
RelatedArticle.objects.select_for_update().filter(
from_article=self.from_article
)
self.clean()
losndu marked this conversation as resolved.
Show resolved Hide resolved
super().save(*args, **kwargs)


class ArticleSlugHistory(models.Model):
"""Model to track historical slugs for articles."""
Expand Down
19 changes: 13 additions & 6 deletions server/apps/research/serializers/article_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,16 @@ class Meta:
model = Article
fields = ['title', 'slug', 'categories', 'thumb', 'content', 'summary', 'acknowledgement', 'status', 'authors', 'scheduled_publish_time', 'is_sponsored', 'sponsor_color', 'sponsor_text_color', 'related_article_ids']

def validate_related_articles_ids(self, value):
"""Validate related articles"""
def validate_related_article_ids(self, value):
"""Validate related articles."""
if len(value) > 3:
raise serializers.ValidationError("You can only have a maximum of 3 related articles.")

# Check for self-reference
instance = self.instance
if instance and instance.id in [article.id for article in value]:
raise serializers.ValidationError("An article cannot be related to itself.")

return value

def create(self, validated_data: dict) -> Article:
Expand All @@ -72,18 +78,19 @@ def create(self, validated_data: dict) -> Article:
if user_author:
authors = [user_author]

article = Article(**validated_data)
article.save()
# Create and save the article first
article = Article.objects.create(**validated_data)

if authors:
article.authors.set(authors)
if categories:
article.categories.set(categories)

# Handle related articles
# Handle related articles after the article is created
for related_article in related_article_ids:
RelatedArticle.objects.create(
from_article=article, to_article=related_article
from_article=article,
to_article=related_article
)

return article
Expand Down
Loading