Skip to content

Commit 9e771dd

Browse files
authored
Merge pull request #824 from ubyssey/490-section-columns
490 section columns
2 parents c9b52fb + 7ee196d commit 9e771dd

32 files changed

+991
-26
lines changed

dispatch/api/serializers.py

+90-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
from dispatch.modules.content.models import (
66
Article, Image, ImageAttachment, ImageGallery, Issue,
7-
File, Page, Author, Section, Tag, Topic, Video, VideoAttachment, Poll, PollAnswer, PollVote)
7+
File, Page, Author, Section, Tag, Topic, Video,
8+
VideoAttachment, Poll, PollAnswer, PollVote, Subsection)
89
from dispatch.modules.auth.models import Person, User, Invite
910
from dispatch.admin.registration import send_invitation
1011
from dispatch.theme.exceptions import WidgetNotFound, InvalidField
@@ -520,6 +521,83 @@ def insert_data(self, content):
520521

521522
return map(self.insert_instance, content)
522523

524+
class SubsectionArticleSerializer(DispatchModelSerializer):
525+
"""Serializes articles for the Subsection model"""
526+
id = serializers.ReadOnlyField(source='parent_id')
527+
authors = AuthorSerializer(many=True, read_only=True)
528+
529+
class Meta:
530+
model = Article
531+
fields = (
532+
'id',
533+
'headline',
534+
'authors',
535+
'published_version'
536+
)
537+
538+
class SubsectionSerializer(DispatchModelSerializer):
539+
"""Serializes the Subsection model"""
540+
541+
authors = AuthorSerializer(many=True, read_only=True)
542+
author_ids = serializers.ListField(
543+
write_only=True,
544+
child=serializers.JSONField(),
545+
validators=[AuthorValidator]
546+
)
547+
authors_string = serializers.CharField(source='get_author_string', read_only=True)
548+
articles = SubsectionArticleSerializer(many=True, read_only=True, source='get_articles')
549+
article_ids = serializers.ListField(
550+
write_only=True,
551+
child=serializers.JSONField(),
552+
required=False
553+
)
554+
section = SectionSerializer(read_only=True)
555+
section_id = serializers.IntegerField(write_only=True)
556+
557+
slug = serializers.SlugField(validators=[SlugValidator()])
558+
559+
class Meta:
560+
model = Subsection
561+
fields = (
562+
'id',
563+
'is_active',
564+
'name',
565+
'section',
566+
'section_id',
567+
'slug',
568+
'description',
569+
'authors',
570+
'author_ids',
571+
'authors_string',
572+
'articles',
573+
'article_ids'
574+
)
575+
576+
def create(self, validated_data):
577+
instance = Subsection()
578+
return self.update(instance, validated_data)
579+
580+
def update(self, instance, validated_data):
581+
# Update basic fields
582+
instance.name = validated_data.get('name', instance.name)
583+
instance.slug = validated_data.get('slug', instance.slug)
584+
instance.is_active = validated_data.get('is_active', instance.is_active)
585+
instance.section_id = validated_data.get('section_id', instance.section_id)
586+
instance.description = validated_data.get('description', instance.description)
587+
588+
# Save instance before processing/saving content in order to save associations to correct ID
589+
instance.save()
590+
591+
authors = validated_data.get('author_ids')
592+
593+
instance.save_authors(authors, is_publishable=False)
594+
595+
article_ids = validated_data.get('article_ids')
596+
597+
instance.save_articles(article_ids)
598+
599+
return instance
600+
523601
class ArticleSerializer(DispatchModelSerializer, DispatchPublishableSerializer):
524602
"""Serializes the Article model."""
525603

@@ -529,6 +607,9 @@ class ArticleSerializer(DispatchModelSerializer, DispatchPublishableSerializer):
529607
section = SectionSerializer(read_only=True)
530608
section_id = serializers.IntegerField(write_only=True)
531609

610+
subsection = SubsectionSerializer(source='get_subsection', read_only=True)
611+
subsection_id = serializers.IntegerField(write_only=True, required=False, allow_null=True)
612+
532613
featured_image = ImageAttachmentSerializer(required=False, allow_null=True)
533614
featured_video = VideoAttachmentSerializer(required=False, allow_null=True)
534615

@@ -585,6 +666,8 @@ class Meta:
585666
'authors_string',
586667
'section',
587668
'section_id',
669+
'subsection',
670+
'subsection_id',
588671
'published_at',
589672
'is_published',
590673
'is_breaking',
@@ -642,7 +725,7 @@ def update(self, instance, validated_data):
642725
instance.integrations = validated_data.get('integrations', instance.integrations)
643726
instance.template = template
644727
instance.template_data = template_data
645-
728+
646729
instance.save()
647730

648731
instance.content = validated_data.get('content', instance.content)
@@ -656,6 +739,7 @@ def update(self, instance, validated_data):
656739
instance.save_featured_video(featured_video)
657740

658741
authors = validated_data.get('author_ids')
742+
659743
if authors:
660744
instance.save_authors(authors, is_publishable=True)
661745

@@ -666,6 +750,10 @@ def update(self, instance, validated_data):
666750
if topic_id != False:
667751
instance.save_topic(topic_id)
668752

753+
subsection_id = validated_data.get('subsection_id', None)
754+
if subsection_id is not None:
755+
instance.save_subsection(subsection_id)
756+
669757
# Perform a final save (without revision), update content and featured image
670758
instance.save(
671759
update_fields=['content', 'featured_image', 'featured_video', 'topic'],

dispatch/api/urls.py

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
router.register(r'videos', views.VideoViewSet, base_name='api-videos')
2727
router.register(r'invites', views.InviteViewSet, base_name='api-invites')
2828
router.register(r'polls', views.PollViewSet, base_name='api-polls')
29+
router.register(r'subsections', views.SubsectionViewSet, base_name='api-subsections')
2930

3031
dashboard_recent_articles = views.DashboardViewSet.as_view({'get': 'list_recent_articles'})
3132
dashboard_user_actions = views.DashboardViewSet.as_view({'get': 'list_actions'})

dispatch/api/validators.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
from rest_framework.exceptions import ValidationError
44
from dispatch.theme.exceptions import InvalidField, TemplateNotFound
5+
from django.contrib.contenttypes.models import ContentType
56

67
from django.contrib.auth.password_validation import validate_password
78

89
from dispatch.api.exceptions import InvalidFilename, InvalidGalleryAttachments
9-
from dispatch.models import Image, Person
10+
from dispatch.models import Image, Person, Section, Page, Subsection, Article
1011

1112
class PasswordValidator(object):
1213
def __init__(self, confirm_field):
@@ -44,13 +45,30 @@ def set_context(self, serializer_field):
4445
self.model = serializer_field.parent.Meta.model
4546

4647
def __call__(self, slug):
47-
if self.instance is None:
48+
if self.model.__name__ is 'Subsection':
49+
self.validate_subsection_slug(slug)
50+
elif self.instance is None:
4851
if self.model.objects.filter(slug=slug).exists():
4952
raise ValidationError('%s with slug \'%s\' already exists.' % (self.model.__name__, slug))
5053
else:
5154
if self.model.objects.filter(slug=slug).exclude(parent=self.instance.parent).exists():
5255
raise ValidationError('%s with slug \'%s\' already exists.' % (self.model.__name__, slug))
5356

57+
def validate_subsection_slug(self, slug):
58+
if Section.objects.filter(slug=slug).exists():
59+
raise ValidationError('A section with that slug already exists.')
60+
if Page.objects.filter(slug=slug).exists():
61+
raise ValidationError('A page with that slug already exists')
62+
63+
if self.instance is None:
64+
if self.model.objects.filter(slug=slug).exists():
65+
raise ValidationError('%s with slug \'%s\' already exists.' % (self.model.__name__, slug))
66+
else:
67+
if self.model.objects.filter(slug=slug).exclude(id=self.instance.id):
68+
raise ValidationError('%s with slug \'%s\' already exists.' % (self.model.__name__, slug))
69+
70+
71+
5472
def AuthorValidator(data):
5573
"""Raise a ValidationError if data does not match the author format."""
5674
if not isinstance(data, list):
@@ -90,4 +108,4 @@ def TemplateValidator(template, template_data, tags):
90108
errors['instructions'] = 'Must have a corresponding timeline tag'
91109

92110
if errors:
93-
raise ValidationError(errors)
111+
raise ValidationError(errors)

dispatch/api/views.py

+22-4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from dispatch.models import (
2121
Article, File, Image, ImageAttachment, ImageGallery, Issue,
2222
Page, Author, Person, Section, Tag, Topic, User, Video,
23-
Poll, PollAnswer, PollVote, Invite)
23+
Poll, PollAnswer, PollVote, Invite, Subsection)
2424

2525
from dispatch.core.settings import get_settings
2626
from dispatch.admin.registration import reset_password
@@ -31,7 +31,7 @@
3131
FileSerializer, IssueSerializer, ImageGallerySerializer, TagSerializer,
3232
TopicSerializer, PersonSerializer, UserSerializer, IntegrationSerializer,
3333
ZoneSerializer, WidgetSerializer, TemplateSerializer, VideoSerializer,
34-
PollSerializer, PollVoteSerializer, InviteSerializer)
34+
PollSerializer, PollVoteSerializer, InviteSerializer, SubsectionSerializer)
3535
from dispatch.api.exceptions import (
3636
ProtectedResourceError, BadCredentials, PollClosed, InvalidPoll,
3737
UnpermittedActionError)
@@ -87,7 +87,7 @@ def get_queryset(self):
8787
'authors'
8888
)
8989

90-
queryset = queryset.order_by('-updated_at')
90+
queryset = queryset.order_by('-updated_at')
9191

9292
q = self.request.query_params.get('q', None)
9393
section = self.request.query_params.get('section', None)
@@ -99,7 +99,7 @@ def get_queryset(self):
9999

100100
if section is not None:
101101
queryset = queryset.filter(section_id=section)
102-
102+
103103
if tags is not None:
104104
for tag in tags:
105105
queryset = queryset.filter(tags__id=tag)
@@ -109,6 +109,24 @@ def get_queryset(self):
109109

110110
return queryset
111111

112+
class SubsectionViewSet(DispatchModelViewSet):
113+
"""Viewset for the Subsection model views."""
114+
model = Subsection
115+
serializer_class = SubsectionSerializer
116+
117+
def get_queryset(self):
118+
queryset = Subsection.objects.all()
119+
q = self.request.query_params.get('q', None)
120+
section = self.request.query_params.get('section', None)
121+
122+
if q is not None:
123+
queryset = queryset.filter(name__icontains=q)
124+
125+
if section is not None:
126+
queryset = queryset.filter(section_id=section)
127+
128+
return queryset
129+
112130
class PageViewSet(DispatchModelViewSet, DispatchPublishableMixin):
113131
"""Viewset for Page model views."""
114132
model = Page
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.11 on 2018-08-10 20:30
3+
from __future__ import unicode_literals
4+
5+
import dispatch.modules.content.mixins
6+
from django.db import migrations, models
7+
import django.db.models.deletion
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
dependencies = [
13+
('dispatch', '0016_breaking_news'),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name='Subsection',
19+
fields=[
20+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21+
('name', models.CharField(max_length=100, unique=True)),
22+
('slug', models.SlugField(unique=True)),
23+
('description', models.TextField(null=True)),
24+
('is_active', models.BooleanField(default=False)),
25+
('authors', models.ManyToManyField(related_name='subsection_authors', to='dispatch.Author')),
26+
('section', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dispatch.Section')),
27+
],
28+
bases=(models.Model, dispatch.modules.content.mixins.AuthorMixin),
29+
),
30+
migrations.AddField(
31+
model_name='article',
32+
name='subsection',
33+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='article_subsection', to='dispatch.Subsection'),
34+
),
35+
]

dispatch/modules/content/models.py

+36-7
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from django.db.models import (
1414
Model, DateTimeField, CharField, TextField, PositiveIntegerField,
1515
ImageField, FileField, BooleanField, UUIDField, ForeignKey,
16-
ManyToManyField, SlugField, SET_NULL, CASCADE)
16+
ManyToManyField, SlugField, SET_NULL, CASCADE, F)
1717
from django.conf import settings
1818
from django.core.validators import MaxValueValidator
1919
from django.utils import timezone
@@ -307,6 +307,7 @@ class Article(Publishable, AuthorMixin):
307307

308308
headline = CharField(max_length=255)
309309
section = ForeignKey('Section')
310+
subsection = ForeignKey('Subsection', related_name='article_subsection', blank=True, null=True)
310311
authors = ManyToManyField('Author', related_name='article_authors')
311312
topic = ForeignKey('Topic', null=True)
312313
tags = ManyToManyField('Tag')
@@ -372,11 +373,41 @@ def save_topic(self, topic_id):
372373
pass
373374

374375
def get_absolute_url(self):
375-
"""
376-
Returns article URL.
377-
"""
376+
""" Returns article URL. """
378377
return "%s%s/%s/" % (settings.BASE_URL, self.section.slug, self.slug)
379378

379+
def get_subsection(self):
380+
""" Returns the subsection set in the parent article """
381+
return self.parent.subsection
382+
383+
def save_subsection(self, subsection_id):
384+
""" Save the subsection to the parent article """
385+
Article.objects.filter(parent_id=self.parent.id).update(subsection_id=subsection_id)
386+
387+
class Subsection(Model, AuthorMixin):
388+
name = CharField(max_length=100, unique=True)
389+
slug = SlugField(unique=True)
390+
description = TextField(null=True)
391+
authors = ManyToManyField('Author', related_name='subsection_authors')
392+
section = ForeignKey('Section')
393+
is_active = BooleanField(default=False)
394+
395+
AuthorModel = Author
396+
397+
def save_articles(self, article_ids):
398+
Article.objects.filter(subsection=self).update(subsection=None)
399+
Article.objects.filter(parent_id__in=article_ids).update(subsection=self)
400+
401+
def get_articles(self):
402+
return Article.objects.filter(subsection=self, id=F('parent_id'))
403+
404+
def get_published_articles(self):
405+
return Article.objects.filter(subsection=self, is_published=True)
406+
407+
def get_absolute_url(self):
408+
""" Returns the subsection URL. """
409+
return "%s%s/" % (settings.BASE_URL, self.slug)
410+
380411
class Page(Publishable):
381412
parent = ForeignKey('Page', related_name='page_parent', blank=True, null=True)
382413
parent_page = ForeignKey('Page', related_name='parent_page_fk', null=True)
@@ -386,9 +417,7 @@ def get_author_string(self):
386417
return None
387418

388419
def get_absolute_url(self):
389-
"""
390-
Returns page URL.
391-
"""
420+
""" Returns page URL. """
392421
return "%s%s/" % (settings.BASE_URL, self.slug)
393422

394423
class Video(Model):

dispatch/static/manager/src/js/actions/ArticlesActions.js

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class ArticlesActions extends PublishableActions {
1919

2020
data.section_id = data.section
2121

22+
data.subsection_id = data.subsection
23+
2224
data.author_ids = data.authors
2325

2426
data.tag_ids = data.tags

0 commit comments

Comments
 (0)