Skip to content

Commit

Permalink
Merge branch 'main' into feat--generate-article-summaries-with-chat-gpt
Browse files Browse the repository at this point in the history
  • Loading branch information
iankressin committed Dec 13, 2024
2 parents 182edbb + d495b2f commit 96e3449
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 44 deletions.
58 changes: 48 additions & 10 deletions server/apps/research/admin/article_admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.contrib import admin
from django import forms
from django.db.models import Q
from apps.research.models import Article, ArticleSlugHistory
from tinymce.widgets import TinyMCE
from .slug_history import current_slug_history
Expand All @@ -16,9 +17,34 @@ class Meta:

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['acknowledgement'].widget = TinyMCE(attrs={'cols': 80, 'rows': 30, 'id': "acknowledgement_richtext_field", 'placeholder': f"Enter Acknowledgement here"})
self.fields['content'].widget = TinyMCE(attrs={'cols': 80, 'rows': 30, 'id': "content_richtext_field", 'placeholder': f"Enter Article Content here"})
self.fields['gpt_summary'].widget = TinyMCE(attrs={'cols': 80, 'rows': 15, 'id': "gpt_summary_richtext_field", 'placeholder': f"GPT-generated summary will appear here"})

# Add filtering for related articles to exclude current article and drafts
if self.instance.pk:
self.fields['related_articles'].queryset = Article.objects.filter(
status='ready'
).exclude(
Q(pk=self.instance.pk) | Q(status='draft')
).order_by('-scheduled_publish_time')

# Configure TinyMCE widgets
self.fields['acknowledgement'].widget = TinyMCE(attrs={
'cols': 80,
'rows': 30,
'id': "acknowledgement_richtext_field",
'placeholder': "Enter Acknowledgement here"
})
self.fields['content'].widget = TinyMCE(attrs={
'cols': 80,
'rows': 30,
'id': "content_richtext_field",
'placeholder': "Enter Article Content here"
})
self.fields['gpt_summary'].widget = TinyMCE(attrs={
'cols': 80,
'rows': 15,
'id': "gpt_summary_richtext_field",
'placeholder': "GPT-generated summary will appear here"
})

class ArticleAdmin(admin.ModelAdmin):
"""Admin interface for the Article model."""
Expand Down Expand Up @@ -50,7 +76,6 @@ def generate_summary_view(self, request):
if request.method == 'POST':
content = request.POST.get('content')
try:
# Run the async function in the sync view
gpt_summary = asyncio.run(self._generate_summary(content))
return JsonResponse({'summary': gpt_summary})
except Exception as e:
Expand All @@ -64,19 +89,31 @@ def current_slug_history(self, obj):
current_slug_history.short_description = 'Slug Change History'

fieldsets = [
('Article Details', {'fields': ['title', 'slug', 'authors', 'acknowledgement', 'categories', 'thumb', 'content', 'summary', 'gpt_summary', 'status', 'scheduled_publish_time']}),
('Sponsorship Details', {'fields': ['is_sponsored', 'sponsor_color', 'sponsor_text_color']}),
('Article Details', {
'fields': [
'title', 'slug', 'authors', 'acknowledgement', 'categories',
'thumb', 'content', 'summary', 'gpt_summary', 'status', 'scheduled_publish_time'
]
}),
('Related Content', {
'fields': ['related_articles'],
'description': 'Select up to 3 related articles that will appear at the end of this article'
}),
('Sponsorship Details', {
'fields': ['is_sponsored', 'sponsor_color', 'sponsor_text_color']
}),
('URL Management', {
'fields': ('current_slug_history',),
'classes': ('collapse',),
'description': 'History of URL changes for this article'
}),
]

list_display = ('title', 'display_authors', 'status', 'views', 'display_categories', 'min_read', 'created_at', 'scheduled_publish_time')
search_fields = ('title', 'authors__user__username', 'authors__twitter_username', 'content')
list_per_page = 25
list_filter = ('authors', 'status', 'categories', 'created_at', 'is_sponsored')
readonly_fields = ('views','current_slug_history',)
readonly_fields = ('views', 'current_slug_history',)
list_editable = ('status',)

class Media:
Expand All @@ -95,8 +132,8 @@ def display_categories(self, obj):
return ", ".join(category.name for category in obj.categories.all())
display_categories.short_description = 'Categories'

def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)

def has_change_permission(self, request, obj=None):
"""Check if the user has permission to change the article."""
Expand All @@ -117,6 +154,7 @@ def has_delete_permission(self, request, obj=None):
if obj is not None and not obj.authors.filter(user=request.user).exists():
return False
return True

@admin.register(ArticleSlugHistory)
class ArticleSlugHistoryAdmin(admin.ModelAdmin):
"""Admin interface for the ArticleSlugHistory model."""
Expand All @@ -138,5 +176,5 @@ def has_add_permission(self, request):

def has_delete_permission(self, request, obj=None):
return False # Prevent deletion

admin.site.register(Article, ArticleAdmin)
2 changes: 1 addition & 1 deletion server/apps/research/admin/category_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ class CategoryAdmin(admin.ModelAdmin):
search_fields = ('name',)
list_filter = ('created_at',)
ordering = ('name',)
readonly_fields = ('slug',)
readonly_fields = ('slug',)
15 changes: 14 additions & 1 deletion server/apps/research/migrations/0015_category_slug.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
# Generated by Django 5.0.8 on 2024-12-09 11:07
# Generated by Django 5.0.8 on 2024-12-13 07:34

from django.db import migrations, models

def remove_duplicate_slugs(apps, schema_editor):
Category = apps.get_model('research', 'Category')
seen_slugs = {}

for category in Category.objects.order_by('id'):
base_slug = category.slug
counter = 1
while category.slug in seen_slugs:
category.slug = f"{base_slug}-{counter}"
counter += 1
seen_slugs[category.slug] = True
category.save()

class Migration(migrations.Migration):

Expand All @@ -15,4 +27,5 @@ class Migration(migrations.Migration):
name='slug',
field=models.SlugField(blank=True, max_length=255),
),
migrations.RunPython(remove_duplicate_slugs),
]
18 changes: 0 additions & 18 deletions server/apps/research/migrations/0016_alter_category_slug.py

This file was deleted.

18 changes: 18 additions & 0 deletions server/apps/research/migrations/0016_article_related_articles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.0.8 on 2024-12-13 12:03

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('research', '0015_category_slug'),
]

operations = [
migrations.AddField(
model_name='article',
name='related_articles',
field=models.ManyToManyField(blank=True, help_text='Select up to 3 related articles', related_name='referenced_by', to='research.article', verbose_name='Related Articles'),
),
]
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
class Migration(migrations.Migration):

dependencies = [
('research', '0016_alter_category_slug'),
('research', '0016_article_related_articles'),
]

operations = [
Expand Down
36 changes: 34 additions & 2 deletions server/apps/research/models/article.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.db import models
from django.utils.text import slugify
from django.core.exceptions import ValidationError
from apps.common.models import BaseModel
from apps.research.managers import ArticleObjects
from .category import Category
Expand Down Expand Up @@ -39,13 +40,27 @@ class Article(BaseModel):
is_sponsored = models.BooleanField(default=False)
sponsor_color = models.CharField(max_length=7, default="#FF0420")
sponsor_text_color = models.CharField(max_length=7, default="#000000")
related_articles = models.ManyToManyField(
'self',
blank=True,
symmetrical=False,
related_name='referenced_by',
verbose_name='Related Articles',
help_text='Select up to 3 related articles'
)

objects = models.Manager()
post_objects = ArticleObjects()

class Meta:
ordering = ('-scheduled_publish_time',)

def clean(self):
super().clean()
# Ensure no more than 3 related articles are selected
if self.related_articles.count() > 3:
raise ValidationError({'related_articles': 'You can select up to 3 related articles only.'})

def calculate_min_read(self):
word_count = len(self.content.split())
words_per_minute = 300 # Average reading speed (words per minute)
Expand Down Expand Up @@ -81,6 +96,24 @@ def build_table_of_contents(self):
self.table_of_contents = toc
self.content = str(soup)

def get_related_articles(self):
"""
Returns manually selected related articles if they exist,
otherwise falls back to automatic recommendations
"""
manual_related = self.related_articles.filter(status='ready').order_by('-scheduled_publish_time')[:3]

if manual_related.exists():
return manual_related

# Fallback logic - articles with matching categories
return Article.objects.filter(
status='ready',
categories__in=self.categories.all()
).exclude(
id=self.id
).distinct().order_by('-scheduled_publish_time')[:3]

def save(self, *args, **kwargs):
"""Override the save method to generate a unique slug and build table of contents."""
if not self.slug or self.title_update():
Expand Down Expand Up @@ -115,7 +148,6 @@ def save(self, *args, **kwargs):

def generate_unique_slug(self):
"""Generate a unique slug for the article."""

base_slug = slugify(self.title)
slug = base_slug
num = 1
Expand Down Expand Up @@ -144,7 +176,7 @@ class Meta:
indexes = [
models.Index(fields=['old_slug']),
]
db_table = 'research_articleslughistory' # explicitly set table name
db_table = 'research_articleslughistory'

def __str__(self):
return f"{self.old_slug} -> {self.article.slug}"
14 changes: 10 additions & 4 deletions server/apps/research/models/category.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@
class Category(BaseModel):
"""Model for categories."""
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True, blank=True)
slug = models.SlugField(max_length=255, blank=True)

class Meta:
verbose_name_plural = 'Categories'

def save(self, *args, **kwargs):
if not self.slug:
self.slug = self.generate_slug()
super().save(*args, **kwargs)
with transaction.atomic():
try:
if not self.slug:
self.slug = self.generate_slug()
if len(self.slug) > 255:
raise ValueError("Generated slug exceeds maximum length")
except ValueError as e:
raise ValueError(f"Failed to generate valid slug") from e
super().save(*args, **kwargs)

def __str__(self):
return self.name
Expand Down
Loading

0 comments on commit 96e3449

Please sign in to comment.