Skip to content

Commit

Permalink
Merge pull request #803 from ubyssey/433-article-poll-embed
Browse files Browse the repository at this point in the history
433 article poll embed
  • Loading branch information
Rowansdabomb authored Jun 8, 2018
2 parents 6e2dda0 + 4c8ee26 commit 7c94d0d
Show file tree
Hide file tree
Showing 129 changed files with 2,568 additions and 1,281 deletions.
8 changes: 8 additions & 0 deletions dispatch/api/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,11 @@ class InvalidGalleryAttachments(APIException):
class BadCredentials(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'Invalid user credentials'

class PollClosed(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'Poll closed'

class InvalidPoll(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'Invalid poll'
105 changes: 101 additions & 4 deletions dispatch/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
from rest_framework.validators import UniqueValidator

from dispatch.modules.content.models import (
Article, Image, ImageAttachment, ImageGallery,
Issue, File, Page, Author, Section, Tag, Topic,
Video, VideoAttachment)

Article, Image, ImageAttachment, ImageGallery, Issue,
File, Page, Author, Section, Tag, Topic, Video, VideoAttachment, Poll, PollAnswer, PollVote)
from dispatch.modules.auth.models import Person, User

from dispatch.api.mixins import DispatchModelSerializer, DispatchPublishableSerializer
Expand Down Expand Up @@ -746,3 +744,102 @@ def update(self, instance, validated_data):
instance.save(validated_data)

return instance

class PollVoteSerializer(DispatchModelSerializer):
"""Serializes the PollVote model"""

answer_id = serializers.IntegerField(write_only=True)

class Meta:

model = PollVote
fields = (
'id',
'answer_id',
)

class PollAnswerSerializer(DispatchModelSerializer):
"""Serializes the PollAnswer model"""

poll_id = serializers.IntegerField(write_only=True)
vote_count = serializers.SerializerMethodField()

class Meta:
model = PollAnswer
fields = (
'id',
'name',
'vote_count',
'poll_id'
)

def get_vote_count(self, obj):
vote_count = 0
poll = Poll.objects.get(id=obj.poll_id)

if self.is_authenticated() or poll.show_results:
vote_count = obj.get_vote_count()

return vote_count

class PollSerializer(DispatchModelSerializer):
"""Serializes the Poll model."""

answers = serializers.SerializerMethodField()
answers_json = JSONField(
required=False,
write_only=True
)
total_votes = serializers.SerializerMethodField()
question = serializers.CharField(required=True)
name = serializers.CharField(required=True)

class Meta:
model = Poll
fields = (
'id',
'is_open',
'show_results',
'name',
'question',
'answers',
'answers_json',
'total_votes'
)

def get_total_votes(self,obj):
total_votes = 0

if self.is_authenticated() or obj.show_results:
total_votes = obj.get_total_votes()

return total_votes

def get_answers(self, obj):
answers = PollAnswer.objects.filter(poll_id=obj.id)
serializer = PollAnswerSerializer(answers, many=True, context=self.context)
return serializer.data

def create(self, validated_data):
# Create new ImageGallery instance
instance = Poll()

# Then save as usual
return self.update(instance, validated_data, True)

def update(self, instance, validated_data, is_new=False):
# Update all the basic fields
instance.question = validated_data.get('question', instance.question)
instance.name = validated_data.get('name', instance.name)
instance.is_open = validated_data.get('is_open', instance.is_open)
instance.show_results = validated_data.get('show_results', instance.show_results)
# Save instance before processing/saving content in order to
# save associations to correct ID
instance.save()

answers = validated_data.get('answers_json')

if isinstance(answers, list):
instance.save_answers(answers, is_new)

return instance
1 change: 1 addition & 0 deletions dispatch/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
router.register(r'zones', views.ZoneViewSet, base_name='api-zones')
router.register(r'token', views.TokenViewSet, base_name='api-token')
router.register(r'videos', views.VideoViewSet, base_name='api-videos')
router.register(r'polls', views.PollViewSet, base_name='api-polls')

dashboard_recent_articles = views.DashboardViewSet.as_view({'get': 'list_recent_articles'})
dashboard_user_actions = views.DashboardViewSet.as_view({'get': 'list_actions'})
Expand Down
47 changes: 43 additions & 4 deletions dispatch/api/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.db.models import Q, ProtectedError, Prefetch
from django.contrib.auth import authenticate
from django.db import IntegrityError

from rest_framework import viewsets, mixins, filters, status
from rest_framework.response import Response
Expand All @@ -14,14 +15,15 @@

from dispatch.models import (
Article, File, Image, ImageAttachment, ImageGallery, Issue,
Page, Author, Person, Section, Tag, Topic, User, Video)
Page, Author, Person, Section, Tag, Topic, User, Video, Poll, PollAnswer, PollVote)

from dispatch.api.mixins import DispatchModelViewSet, DispatchPublishableMixin
from dispatch.api.serializers import (
ArticleSerializer, PageSerializer, SectionSerializer, ImageSerializer, FileSerializer, IssueSerializer,
ImageGallerySerializer, TagSerializer, TopicSerializer, PersonSerializer, UserSerializer,
IntegrationSerializer, ZoneSerializer, WidgetSerializer, TemplateSerializer, VideoSerializer)
from dispatch.api.exceptions import ProtectedResourceError, BadCredentials
IntegrationSerializer, ZoneSerializer, WidgetSerializer, TemplateSerializer, VideoSerializer, PollSerializer,
PollVoteSerializer )
from dispatch.api.exceptions import ProtectedResourceError, BadCredentials, PollClosed, InvalidPoll

from dispatch.theme import ThemeManager
from dispatch.theme.exceptions import ZoneNotFound, TemplateNotFound
Expand Down Expand Up @@ -163,7 +165,7 @@ def get_queryset(self):
if q is not None:
# If a search term (q) is present, filter queryset by term against `name`
queryset = queryset.filter(name__icontains=q)

return queryset

class TopicViewSet(DispatchModelViewSet):
Expand Down Expand Up @@ -248,6 +250,43 @@ def get_queryset(self):
queryset = queryset.filter(title__icontains=q)
return queryset

class PollViewSet(DispatchModelViewSet):
"""Viewset for the Poll model views."""
model = Poll
serializer_class = PollSerializer

def get_queryset(self):
queryset = Poll.objects.all()
q = self.request.query_params.get('q', None)
if q is not None:
queryset = queryset.filter(Q(name__icontains=q) | Q(question__icontains=q) )
return queryset

@detail_route(permission_classes=[AllowAny], methods=['post'],)
def vote(self, request, pk=None):
poll = get_object_or_404(Poll.objects.all(), pk=pk)

if not poll.is_open:
raise PollClosed()

answer = get_object_or_404(PollAnswer.objects.all(), pk=request.data['answer_id'])

if answer.poll != poll:
raise InvalidPoll()

# Change vote
if 'vote_id' in request.data:
vote_id = request.data['vote_id']
vote = PollVote.objects.filter(answer__poll=poll, id=vote_id).update(answer=answer)
return Response({'id': vote_id})

serializer = PollVoteSerializer(data=request.data)

serializer.is_valid(raise_exception=True)
serializer.save()

return Response(serializer.data)

class TemplateViewSet(viewsets.GenericViewSet):
"""Viewset for Template views"""
permission_classes = (IsAuthenticated,)
Expand Down
43 changes: 43 additions & 0 deletions dispatch/migrations/0012_polls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-06-04 19:15
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion
import uuid


class Migration(migrations.Migration):

dependencies = [
('dispatch', '0011_article_featured_video'),
]

operations = [
migrations.CreateModel(
name='Poll',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('question', models.CharField(max_length=255)),
('is_open', models.BooleanField(default=True)),
('show_results', models.BooleanField(default=True)),
],
),
migrations.CreateModel(
name='PollAnswer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('poll', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='dispatch.Poll')),
],
),
migrations.CreateModel(
name='PollVote',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('answer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='dispatch.PollAnswer')),
],
),
]
26 changes: 26 additions & 0 deletions dispatch/modules/content/embeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,31 @@ class AdvertisementEmbed(AbstractTemplateEmbed):
class PullQuoteEmbed(AbstractTemplateEmbed):
TEMPLATE = 'embeds/quote.html'

class WidgetEmbed(AbstractEmbed):
@classmethod
def render(self, data):

from dispatch.theme import ThemeManager
from dispatch.theme.exceptions import ZoneNotFound, WidgetNotFound

try:
widget_id = data['widget_id']
widget = ThemeManager.Widgets.get(widget_id)
except:
return ''

return widget.render(data=data['data'])

# except ZoneNotFound:
# return ''

# try:
# return zone.widget.render(add_context=kwargs)
# except (WidgetNotFound, AttributeError):
# pass

# return ''

class ImageEmbed(AbstractTemplateEmbed):
TEMPLATE = 'embeds/image.html'

Expand Down Expand Up @@ -135,6 +160,7 @@ def prepare_data(self, data):
'size': len(images)
}

embeds.register('widget', WidgetEmbed)
embeds.register('quote', PullQuoteEmbed)
embeds.register('code', CodeEmbed)
embeds.register('advertisement', AdvertisementEmbed)
Expand Down
46 changes: 44 additions & 2 deletions dispatch/modules/content/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
from PIL import Image as Img

from django.db import IntegrityError
from django.db import transaction

from django.db.models import (
Model, DateTimeField, CharField, TextField, PositiveIntegerField,
ImageField, FileField, BooleanField, UUIDField, ForeignKey,
ManyToManyField, SlugField, SET_NULL)
ManyToManyField, SlugField, SET_NULL, CASCADE)
from django.conf import settings
from django.core.validators import MaxValueValidator
from django.utils import timezone
Expand Down Expand Up @@ -222,7 +224,7 @@ def save_featured_image(self, data):
if data is None:
if attachment:
attachment.delete()

self.featured_image = None
return

Expand Down Expand Up @@ -553,3 +555,43 @@ class Issue(Model):
volume = PositiveIntegerField(null=True)
issue = PositiveIntegerField(null=True)
date = DateTimeField()

class Poll(Model):
name = CharField(max_length=255)
question = CharField(max_length=255)
is_open = BooleanField(default=True)
show_results = BooleanField(default=True)

@transaction.atomic
def save_answers(self, answers, is_new):
if not is_new:
self.delete_old_answers(answers)
for answer in answers:
try:
answer_id = answer.get('id')
answer_obj = PollAnswer.objects.get(poll=self, id=answer_id)
answer_obj.name = answer['name']
except PollAnswer.DoesNotExist:
answer_obj = PollAnswer(poll=self, name=answer['name'])
answer_obj.save()

def delete_old_answers(self, answers):
PollAnswer.objects.filter(poll=self) \
.exclude(id__in=[answer.get('id', 0) for answer in answers]) \
.delete()

def get_total_votes(self):
return PollVote.objects.filter(answer__poll=self).count()

class PollAnswer(Model):
poll = ForeignKey(Poll, related_name='answers', on_delete=CASCADE)
name = CharField(max_length=255)

def get_vote_count(self):
"""Return the number of votes for this answer"""
return PollVote.objects.filter(answer=self).count()

class PollVote(Model):
id = UUIDField(default=uuid.uuid4, primary_key=True)
answer = ForeignKey(PollAnswer, related_name='votes', on_delete=CASCADE)
timestamp = DateTimeField(auto_now_add=True)
11 changes: 10 additions & 1 deletion dispatch/static/manager/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,18 @@ module.exports = {
"semi": ["error", "never"],
"no-unused-vars": ["warn", {"args": "after-used"}],
"no-console": ["warn"],
"keyword-spacing": 2,
"react/prop-types": 0,
"react/no-find-dom-node": 0,
"react/self-closing-comp": 2,
"react/jsx-indent-props": ["error", 2],
"react/no-find-dom-node": 0
"react/jsx-first-prop-new-line": ["error", "multiline-multiprop"],
"react/jsx-closing-bracket-location": ["error", "after-props"],
"react/jsx-curly-spacing": 2,
"react/no-string-refs": 0,
"react/jsx-props-no-multi-spaces": 2,
"react/jsx-space-before-closing": 2,
"react/no-deprecated": 0,
},
"plugins": [
"react"
Expand Down
Loading

0 comments on commit 7c94d0d

Please sign in to comment.