Skip to content

Commit 2d16efd

Browse files
authored
Merge pull request #16 from malkoG/feature/curation
아티클 큐레이션 기능
2 parents a6915a8 + cc84308 commit 2d16efd

19 files changed

+1008
-2
lines changed

.env.sample

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
OPENAI_API_KEY=sk-proj-xxxxx
2+
GEMINI_API_KEY=asdasd

docker-compose.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ services:
1515
- ./pythonkr_backend:/app/pythonkr_backend
1616
ports:
1717
- "8080:8080"
18+
env_file:
19+
- .env
1820
depends_on:
1921
- db
2022
environment:
@@ -23,4 +25,4 @@ services:
2325
POSTGRES_PASSWORD: pktesting
2426

2527
volumes:
26-
postgres_data:
28+
postgres_data:

pyproject.toml

+7
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,18 @@ description = "Add your description here"
55
readme = "README.md"
66
requires-python = ">=3.12"
77
dependencies = [
8+
"beautifulsoup4>=4.13.3",
89
"django>=5.1.7",
910
"gunicorn>=23.0.0",
1011
"httpx>=0.28.1",
12+
"llm>=0.24.2",
13+
"llm-gemini>=0.18.1",
14+
"lxml>=5.3.2",
1115
"markdown>=3.7",
1216
"psycopg[binary]>=3.2.5",
17+
"readtime>=3.0.0",
18+
"requests>=2.32.3",
19+
"tiktoken>=0.9.0",
1320
"wagtail>=6.4.1",
1421
"wagtail-bakery>=0.8.0",
1522
]

pythonkr_backend/curation/__init__.py

Whitespace-only changes.

pythonkr_backend/curation/admin.py

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from django.contrib import admin, messages
2+
from .models import Article, Category # Or combine imports
3+
4+
5+
@admin.register(Category)
6+
class CategoryAdmin(admin.ModelAdmin):
7+
list_display = ('name', 'slug')
8+
search_fields = ('name',)
9+
prepopulated_fields = {'slug': ('name',)} # Auto-populate slug from name
10+
11+
12+
@admin.action(description="Fetch content, summarize, and translate selected articles")
13+
def summarize_selected_articles(modeladmin, request, queryset):
14+
success_count = 0
15+
errors = []
16+
17+
for article in queryset:
18+
result = article.fetch_and_summarize()
19+
if result.startswith("Error"):
20+
errors.append(f"{article.url}: {result}")
21+
else:
22+
success_count += 1
23+
24+
if success_count > 0:
25+
modeladmin.message_user(
26+
request,
27+
f"Successfully processed {success_count} article(s) (fetch, summarize, translate).",
28+
messages.SUCCESS
29+
)
30+
31+
if errors:
32+
error_message = "Errors encountered:\n" + "\n".join(errors)
33+
modeladmin.message_user(request, error_message, messages.WARNING)
34+
35+
36+
37+
38+
@admin.register(Article)
39+
class ArticleAdmin(admin.ModelAdmin):
40+
list_display = ('url', 'title', 'display_categories', 'summary_preview', 'summary_ko_preview', 'reading_time_minutes', 'updated_at', 'created_at')
41+
list_filter = ('categories', 'created_at', 'updated_at')
42+
search_fields = ('url', 'title', 'summary', 'summary_ko', 'categories__name')
43+
readonly_fields = ('created_at', 'updated_at', 'summary', 'summary_ko', 'reading_time_minutes')
44+
actions = [summarize_selected_articles]
45+
filter_horizontal = ('categories',)
46+
47+
fieldsets = (
48+
('Article Information', {
49+
'fields': ('url', 'title', 'categories')
50+
}),
51+
('Generated Content', {
52+
'fields': ('summary', 'summary_ko', 'reading_time_minutes'),
53+
'classes': ('collapse',)
54+
}),
55+
('Metadata', {
56+
'fields': ('created_at', 'updated_at'),
57+
'classes': ('collapse',)
58+
}),
59+
)
60+
61+
@admin.display(description='Categories')
62+
def display_categories(self, obj):
63+
"""Displays categories as a comma-separated string in the list view."""
64+
if obj.categories.exists():
65+
return ", ".join([category.name for category in obj.categories.all()])
66+
return '-' # Or None, or empty string
67+
68+
def get_readonly_fields(self, request, obj=None):
69+
# Make 'categories' always read-only as it's set by the LLM
70+
readonly = list(super().get_readonly_fields(request, obj))
71+
if 'categories' not in readonly:
72+
readonly.append('categories')
73+
return readonly
74+
75+
@admin.display(description='Summary Preview')
76+
def summary_preview(self, obj):
77+
if obj.summary:
78+
preview = obj.summary[:100]
79+
return f"{preview}..." if len(obj.summary) > 100 else preview
80+
return "No summary available"
81+
82+
@admin.display(description='Korean Summary Preview')
83+
def summary_ko_preview(self, obj):
84+
if obj.summary_ko:
85+
if obj.summary_ko.startswith("Translation Error"): return obj.summary_ko
86+
preview = obj.summary_ko[:50]
87+
return f"{preview}..." if len(obj.summary_ko) > 50 else preview
88+
return "No Korean summary"

pythonkr_backend/curation/apps.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class CurationConfig(AppConfig):
5+
default_auto_field = 'django.db.models.BigAutoField'
6+
name = 'curation'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Generated by Django 5.2 on 2025-04-20 05:42
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
initial = True
9+
10+
dependencies = [
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='Article',
16+
fields=[
17+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18+
('url', models.URLField(help_text='The unique URL of the article.', max_length=2048, unique=True)),
19+
('title', models.CharField(blank=True, help_text='Article title (can be fetched automatically or entered manually).', max_length=512)),
20+
('summary', models.TextField(blank=True, help_text='AI-generated summary of the article.')),
21+
('created_at', models.DateTimeField(auto_now_add=True)),
22+
('updated_at', models.DateTimeField(auto_now=True)),
23+
],
24+
),
25+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2 on 2025-04-20 06:20
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('curation', '0001_initial'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='article',
15+
name='reading_time_minutes',
16+
field=models.PositiveIntegerField(blank=True, help_text='Estimated reading time in minutes.', null=True),
17+
),
18+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2 on 2025-04-20 06:23
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('curation', '0002_article_reading_time_minutes'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='article',
15+
name='summary_ko',
16+
field=models.TextField(blank=True, help_text='Korean translation of the summary (via OpenAI).'),
17+
),
18+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2 on 2025-04-20 06:34
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('curation', '0003_article_summary_ko'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='article',
15+
name='reading_time_minutes',
16+
field=models.PositiveIntegerField(blank=True, help_text='Estimated reading time in minutes (based on full article content).', null=True),
17+
),
18+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Generated by Django 5.2 on 2025-04-20 07:11
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('curation', '0004_alter_article_reading_time_minutes'),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='Category',
15+
fields=[
16+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17+
('name', models.CharField(help_text="The name of the category (e.g., 'Web Development', 'LLM').", max_length=100, unique=True)),
18+
('slug', models.SlugField(blank=True, help_text='A URL-friendly slug for the category.', max_length=100, unique=True)),
19+
],
20+
options={
21+
'verbose_name_plural': 'Categories',
22+
'ordering': ['name'],
23+
},
24+
),
25+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2 on 2025-04-20 07:25
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('curation', '0005_category'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='article',
15+
name='categories',
16+
field=models.ManyToManyField(blank=True, help_text='Select one or more categories for this article.', related_name='articles', to='curation.category'),
17+
),
18+
]

pythonkr_backend/curation/migrations/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)