Skip to content

Commit 7f79fbe

Browse files
authored
Merge pull request #189 from 2077-Collective/main
main to staging sync
2 parents 27c96c6 + d9ec418 commit 7f79fbe

14 files changed

+212
-26
lines changed

.github/workflows/lock-main.yaml

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Restrict Merge to Main
1+
name: Merge to Main
22

33
on:
44
pull_request:
@@ -8,11 +8,3 @@ on:
88
jobs:
99
check_source_branch:
1010
runs-on: ubuntu-latest
11-
12-
steps:
13-
- name: Check Source Branch
14-
run: |
15-
if [ "${{ github.event.pull_request.head.ref }}" != "staging" ]; then
16-
echo "PRs to main branch must come from staging branch only."
17-
exit 1
18-
fi

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# 2077 Collective
44

5-
Content management system written in Python(Django) and React.js
5+
Content management system written in Python(Django) and Sveltekit
66

77
## Project Architecture
88

server/apps/research/admin/article_admin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ class ArticleAdmin(admin.ModelAdmin):
1818
form = ArticleForm
1919
fieldsets = [
2020
('Article Details', {'fields': ['title', 'slug', 'authors', 'acknowledgement', 'categories', 'thumb', 'content', 'summary', 'status', 'scheduled_publish_time']}),
21+
('Sponsorship Details', {'fields': ['is_sponsored', 'sponsor_color', 'sponsor_text_color']}),
2122
]
2223
list_display = ('title', 'display_authors', 'status', 'views', 'display_categories', 'min_read', 'created_at', 'scheduled_publish_time')
2324
search_fields = ('title', 'authors__user__username', 'authors__twitter_username', 'content')
2425
list_per_page = 25
25-
list_filter = ('authors', 'status', 'categories', 'created_at')
26+
list_filter = ('authors', 'status', 'categories', 'created_at', 'is_sponsored')
2627
readonly_fields = ('views',)
2728
list_editable = ('status',)
2829

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.0.8 on 2024-10-14 21:02
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('research', '0004_alter_article_authors'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='article',
15+
name='table_of_contents',
16+
field=models.JSONField(blank=True, default=list),
17+
),
18+
]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 5.0.8 on 2024-10-23 08:16
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('research', '0005_article_table_of_contents'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='article',
15+
name='is_sponsored',
16+
field=models.BooleanField(default=False),
17+
),
18+
migrations.AddField(
19+
model_name='article',
20+
name='sponsor_color',
21+
field=models.CharField(default='#FF0420', max_length=7),
22+
),
23+
migrations.AddField(
24+
model_name='article',
25+
name='sponsor_text_color',
26+
field=models.CharField(default='#000000', max_length=7),
27+
),
28+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.0.8 on 2024-10-23 10:50
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('research', '0006_article_is_sponsored_article_sponsor_color_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='article',
15+
name='sponsor_color',
16+
field=models.CharField(default='#FF0420 !important', max_length=7),
17+
),
18+
migrations.AlterField(
19+
model_name='article',
20+
name='sponsor_text_color',
21+
field=models.CharField(default='#000000 !important', max_length=7),
22+
),
23+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.0.8 on 2024-10-23 10:53
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('research', '0007_alter_article_sponsor_color_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='article',
15+
name='sponsor_color',
16+
field=models.CharField(default='bg-red-700 !important', max_length=7),
17+
),
18+
migrations.AlterField(
19+
model_name='article',
20+
name='sponsor_text_color',
21+
field=models.CharField(default='#bg-gray-700 !important', max_length=7),
22+
),
23+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.0.8 on 2024-10-23 10:58
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('research', '0008_alter_article_sponsor_color_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='article',
15+
name='sponsor_text_color',
16+
field=models.CharField(default='text-gray-700 !important', max_length=7),
17+
),
18+
]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 5.0.8 on 2024-10-23 11:20
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('research', '0009_alter_article_sponsor_text_color'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='article',
15+
name='sponsor_padding',
16+
field=models.CharField(default='px-4 py-6', max_length=20),
17+
),
18+
migrations.AlterField(
19+
model_name='article',
20+
name='sponsor_color',
21+
field=models.CharField(default='#FF0420', max_length=7),
22+
),
23+
migrations.AlterField(
24+
model_name='article',
25+
name='sponsor_text_color',
26+
field=models.CharField(default='#000000', max_length=7),
27+
),
28+
]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 5.0.8 on 2024-10-28 05:02
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('research', '0010_article_sponsor_padding_alter_article_sponsor_color_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.RemoveField(
14+
model_name='article',
15+
name='sponsor_padding',
16+
),
17+
]

server/apps/research/models/article.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
from django.utils import timezone
88
from django.conf import settings
99
from tinymce.models import HTMLField
10+
import json
11+
from bs4 import BeautifulSoup
12+
import uuid
1013

1114
def get_default_thumb():
1215
return f"{settings.MEDIA_URL}images/2077-Collective.png"
@@ -30,7 +33,11 @@ class Article(BaseModel):
3033
views = models.PositiveBigIntegerField(default=0)
3134
status = models.CharField(max_length=10, choices=options, default='draft', db_index=True)
3235
scheduled_publish_time = models.DateTimeField(null=True, blank=True, db_index=True)
33-
36+
table_of_contents = models.JSONField(default=list, blank=True)
37+
is_sponsored = models.BooleanField(default=False)
38+
sponsor_color = models.CharField(max_length=7, default="#FF0420")
39+
sponsor_text_color = models.CharField(max_length=7, default="#000000")
40+
3441
objects = models.Manager()
3542
post_objects = ArticleObjects()
3643

@@ -48,12 +55,38 @@ def calculate_min_read(self):
4855
def __str__(self):
4956
return self.title
5057

51-
def save(self, *args, **kwargs):
52-
"""Override the save method to generate a unique slug."""
58+
def build_table_of_contents(self):
59+
"""Build the table of contents from the article content."""
60+
soup = BeautifulSoup(self.content, 'html.parser')
61+
headers = soup.find_all(['h1', 'h2', 'h3'])
62+
63+
toc = []
64+
stack = [{'level': 0, 'children': toc}]
65+
66+
for header in headers:
67+
level = int(header.name[1])
68+
title = header.get_text()
69+
header['id'] = slugify(title)
5370

71+
while level <= stack[-1]['level']:
72+
stack.pop()
73+
74+
new_item = {'title': title, 'id': header['id'], 'children': []}
75+
76+
stack[-1]['children'].append(new_item)
77+
stack.append({'level': level, 'children': new_item['children']})
78+
79+
self.table_of_contents = toc
80+
self.content = str(soup)
81+
82+
def save(self, *args, **kwargs):
83+
"""Override the save method to generate a unique slug and build table of contents."""
5484
if not self.slug or self.title_update():
55-
self.slug = self.generate_unique_slug()
56-
85+
self.slug = self.generate_unique_slug()
86+
87+
if self.content:
88+
self.build_table_of_contents()
89+
5790
if self.scheduled_publish_time and self.status == 'draft' and timezone.now() >= self.scheduled_publish_time:
5891
self.status = 'ready'
5992

@@ -76,4 +109,4 @@ def title_update(self):
76109
original = Article.objects.filter(pk=self.pk).only('title').first()
77110
if original:
78111
return original.title != self.title
79-
return False
112+
return False

server/apps/research/serializers/article_serializer.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55

66
class ArticleListSerializer(serializers.ModelSerializer):
77
categories = CategorySerializer(many=True)
8+
authors = AuthorSerializer(many=True)
89

910
class Meta:
1011
model = Article
11-
include = ['categories']
12+
include = ['categories' 'authors']
1213
exclude = [
13-
'content', 'authors', 'scheduled_publish_time', 'acknowledgement',
14-
'status', 'views', 'created_at', 'updated_at'
14+
'content', 'scheduled_publish_time', 'acknowledgement',
15+
'status', 'views', 'created_at', 'updated_at', 'table_of_contents'
1516
]
1617

1718
class ArticleSerializer(serializers.ModelSerializer):
@@ -21,13 +22,13 @@ class ArticleSerializer(serializers.ModelSerializer):
2122
views = serializers.ReadOnlyField()
2223
min_read = serializers.ReadOnlyField()
2324

24-
2525
class Meta:
2626
model = Article
2727
fields = [
28-
'id', 'slug', 'title', 'authors', 'thumb',
29-
'categories', 'summary', 'acknowledgement', 'content', 'min_read',
30-
'status', 'views', 'created_at', 'updated_at', 'scheduled_publish_time'
28+
'id', 'slug', 'title', 'authors', 'thumb', 'categories', 'summary',
29+
'acknowledgement', 'content', 'min_read', 'status', 'views',
30+
'created_at', 'updated_at', 'scheduled_publish_time', 'table_of_contents',
31+
'is_sponsored', 'sponsor_color', 'sponsor_text_color'
3132
]
3233

3334
class ArticleCreateUpdateSerializer(serializers.ModelSerializer):
@@ -37,7 +38,7 @@ class ArticleCreateUpdateSerializer(serializers.ModelSerializer):
3738

3839
class Meta:
3940
model = Article
40-
fields = ['title', 'slug', 'categories', 'thumb', 'content', 'summary', 'acknowledgement', 'status', 'authors', 'scheduled_publish_time']
41+
fields = ['title', 'slug', 'categories', 'thumb', 'content', 'summary', 'acknowledgement', 'status', 'authors', 'scheduled_publish_time', 'is_sponsored', 'sponsor_color', 'sponsor_text_color']
4142

4243
def create(self, validated_data: dict) -> Article:
4344
"""Create a new article instance."""
@@ -78,4 +79,4 @@ def update(self, instance: Article, validated_data: dict) -> Article:
7879

7980
return instance
8081
except Exception as e:
81-
raise serializers.ValidationError(f"Error updating article: {str(e)}")
82+
raise serializers.ValidationError(f"Error updating article: {str(e)}")
Loading

server/requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ amqp==5.2.0
22
anyio==4.4.0
33
asgiref==3.8.1
44
attrs==24.1.0
5+
beautifulsoup4==4.12.3
56
billiard==4.2.0
7+
bs4==0.0.2
68
celery==5.4.0
79
certifi==2024.7.4
810
cffi==1.17.0
@@ -21,6 +23,7 @@ django-cors-headers==4.4.0
2123
django-filter==24.2
2224
django-js-asset==2.2.0
2325
django-shortcuts==1.6
26+
django-sortedm2m==4.0.0
2427
django-timezone-field==7.0
2528
django-tinymce==4.1.0
2629
djangorestframework==3.15.2
@@ -49,6 +52,7 @@ requests==2.32.3
4952
rpds-py==0.19.1
5053
six==1.16.0
5154
sniffio==1.3.1
55+
soupsieve==2.6
5256
sqlparse==0.5.1
5357
text-unidecode==1.3
5458
tzdata==2024.1

0 commit comments

Comments
 (0)