From 871746c45d7121ff8b7149e4eb79a852fec8334b Mon Sep 17 00:00:00 2001 From: Ron Y <31330116+i-ron-y@users.noreply.github.com> Date: Mon, 5 Mar 2018 03:54:23 -0800 Subject: [PATCH 01/55] Support poll embed --- dispatch/modules/content/embeds.py | 4 ++ .../ArticleEditor/ArticleContentEditor.js | 2 + .../dispatch-editor/embeds/PollEmbed.js | 61 +++++++++++++++++++ .../js/vendor/dispatch-editor/embeds/index.js | 2 + .../src/js/vendor/dispatch-editor/index.js | 6 +- 5 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 dispatch/static/manager/src/js/vendor/dispatch-editor/embeds/PollEmbed.js diff --git a/dispatch/modules/content/embeds.py b/dispatch/modules/content/embeds.py index 2ca0334fe..90321c79d 100644 --- a/dispatch/modules/content/embeds.py +++ b/dispatch/modules/content/embeds.py @@ -83,6 +83,9 @@ class AdvertisementEmbed(AbstractTemplateEmbed): class PullQuoteEmbed(AbstractTemplateEmbed): TEMPLATE = 'embeds/quote.html' +class PollEmbed(AbstractTemplateEmbed): + TEMPLATE = 'embeds/poll.html' + class ImageEmbed(AbstractTemplateEmbed): TEMPLATE = 'embeds/image.html' @@ -135,6 +138,7 @@ def prepare_data(self, data): 'size': len(images) } +embeds.register('poll', PollEmbed) embeds.register('quote', PullQuoteEmbed) embeds.register('code', CodeEmbed) embeds.register('advertisement', AdvertisementEmbed) diff --git a/dispatch/static/manager/src/js/components/ArticleEditor/ArticleContentEditor.js b/dispatch/static/manager/src/js/components/ArticleEditor/ArticleContentEditor.js index e433df32c..ebc7e9025 100644 --- a/dispatch/static/manager/src/js/components/ArticleEditor/ArticleContentEditor.js +++ b/dispatch/static/manager/src/js/components/ArticleEditor/ArticleContentEditor.js @@ -10,6 +10,7 @@ import { GalleryEmbed, CodeEmbed, WidgetEmbed, + PollEmbed } from '../../vendor/dispatch-editor' const embeds = [ @@ -19,6 +20,7 @@ const embeds = [ PullQuoteEmbed, GalleryEmbed, WidgetEmbed, + PollEmbed ] export default class ArticleContentEditor extends React.Component { diff --git a/dispatch/static/manager/src/js/vendor/dispatch-editor/embeds/PollEmbed.js b/dispatch/static/manager/src/js/vendor/dispatch-editor/embeds/PollEmbed.js new file mode 100644 index 000000000..82fc56b8a --- /dev/null +++ b/dispatch/static/manager/src/js/vendor/dispatch-editor/embeds/PollEmbed.js @@ -0,0 +1,61 @@ +import React from 'react' + +import { FormInput, TextInput } from '../../../components/inputs' + +function PollEmbedComponent(props) { + return ( +
+
+ + props.updateField('question', e.target.value)} /> + + + props.updateField('choice1', e.target.value)} /> + + + props.updateField('choice2', e.target.value)} /> + + + props.updateField('choice3', e.target.value)} /> + + + props.updateField('choice4', e.target.value)} /> + + + props.updateField('choice5', e.target.value)} /> + +
+
+ ) +} + +export default { + type: 'poll', + component: PollEmbedComponent, + defaultData: { + question: '', + choice1: '', + choice2: '', + choice3: '', + choice4: '', + choice5: '' + } +} diff --git a/dispatch/static/manager/src/js/vendor/dispatch-editor/embeds/index.js b/dispatch/static/manager/src/js/vendor/dispatch-editor/embeds/index.js index 4af5b345d..b7585e9cd 100644 --- a/dispatch/static/manager/src/js/vendor/dispatch-editor/embeds/index.js +++ b/dispatch/static/manager/src/js/vendor/dispatch-editor/embeds/index.js @@ -4,6 +4,7 @@ import CodeEmbed from './CodeEmbed' import PullQuoteEmbed from './PullQuoteEmbed' import GalleryEmbed from './GalleryEmbed' import WidgetEmbed from './WidgetEmbed' +import PollEmbed from './PollEmbed' export { ImageEmbed, @@ -12,4 +13,5 @@ export { CodeEmbed, GalleryEmbed, WidgetEmbed, + PollEmbed } diff --git a/dispatch/static/manager/src/js/vendor/dispatch-editor/index.js b/dispatch/static/manager/src/js/vendor/dispatch-editor/index.js index 1b19d53a4..24076aeb4 100644 --- a/dispatch/static/manager/src/js/vendor/dispatch-editor/index.js +++ b/dispatch/static/manager/src/js/vendor/dispatch-editor/index.js @@ -6,7 +6,8 @@ import { PullQuoteEmbed, VideoEmbed, CodeEmbed, - WidgetEmbed + WidgetEmbed, + PollEmbed } from './embeds' import { @@ -28,5 +29,6 @@ export { PullQuoteEmbed, VideoEmbed, CodeEmbed, - WidgetEmbed + WidgetEmbed, + PollEmbed } From ca2204f4f5ea15d89f001a8fce42720ff5615bf6 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Thu, 24 May 2018 13:40:48 -0700 Subject: [PATCH 02/55] made models, views and serializers for polls, poll answers, and, and poll votes --- dispatch/api/serializers.py | 93 +++++++++++++++++++++++++++++- dispatch/api/urls.py | 2 + dispatch/api/views.py | 19 +++++- dispatch/migrations/0011_polls.py | 38 ++++++++++++ dispatch/modules/content/models.py | 25 +++++++- 5 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 dispatch/migrations/0011_polls.py diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index 1c0cb1e0b..5921f9195 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -4,7 +4,7 @@ from dispatch.modules.content.models import ( Article, Image, ImageAttachment, ImageGallery, Issue, - File, Page, Author, Section, Tag, Topic, Video) + File, Page, Author, Section, Tag, Topic, Video, Poll, PollAnswer, PollVote) from dispatch.modules.auth.models import Person, User from dispatch.api.mixins import DispatchModelSerializer, DispatchPublishableSerializer @@ -715,3 +715,94 @@ 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', + ) + + def create(self, validated_data): + # Create new ImageGallery instance + instance = PollVote() + + # Then save as usual + return self.update(instance, validated_data) + + def update(self, instance, validated_data): + # Get the proper Poll Answer + answer_id = validated_data.get('answer_id', False) + try: + answer = PollAnswer.objects.get(id=answer_id) + except PollAnswer.DoesNotExist: + answer = None + # Set the vote's answer + if answer is not None: + instance.answer = answer + instance.save() + + return instance + +class PollAnswerSerializer(DispatchModelSerializer): + """Serializes the PollAnswer model""" + votes = PollVoteSerializer(many=True, required=False, read_only=True) + poll_id = serializers.IntegerField(write_only=True) + vote_count = serializers.IntegerField(source='get_votes', read_only=True) + + class Meta: + model = PollAnswer + fields = ( + 'id', + 'name', + 'votes', + 'vote_count', + 'poll_id' + ) + + def get_vote_count(self, obj): + return obj.votes.count() + +class PollSerializer(DispatchModelSerializer): + """Serializes the Poll model.""" + answers = PollAnswerSerializer(many=True, read_only=True, required=False) + answers_json = JSONField( + required=False, + write_only=True + ) + total_votes = serializers.IntegerField(source='get_total_votes', read_only=True) + + class Meta: + model = Poll + fields = ( + 'id', + 'question', + 'answers', + 'answers_json', + 'total_votes' + ) + + def create(self, validated_data): + # Create new ImageGallery instance + instance = Poll() + + # Then save as usual + return self.update(instance, validated_data) + + def update(self, instance, validated_data): + # Update all the basic fields + instance.question = validated_data.get('question', instance.question) + + # Save instance before processing/saving content in order to + # save associations to correct ID + instance.save() + + answers = validated_data.get('answers_json', False) + + if isinstance(answers, list): + instance.save_answers(answers) + + return instance diff --git a/dispatch/api/urls.py b/dispatch/api/urls.py index b633e5383..ea609b489 100644 --- a/dispatch/api/urls.py +++ b/dispatch/api/urls.py @@ -24,6 +24,8 @@ 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') +router.register(r'vote', views.PollVoteViewSet, base_name='api-votes') dashboard_recent_articles = views.DashboardViewSet.as_view({'get': 'list_recent_articles'}) dashboard_user_actions = views.DashboardViewSet.as_view({'get': 'list_actions'}) diff --git a/dispatch/api/views.py b/dispatch/api/views.py index f47f7f3db..ca2575203 100644 --- a/dispatch/api/views.py +++ b/dispatch/api/views.py @@ -14,13 +14,14 @@ 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, 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) + IntegrationSerializer, ZoneSerializer, WidgetSerializer, TemplateSerializer, VideoSerializer, PollSerializer, + PollVoteSerializer ) from dispatch.api.exceptions import ProtectedResourceError, BadCredentials from dispatch.theme import ThemeManager @@ -163,7 +164,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): @@ -248,6 +249,18 @@ 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 + queryset = Poll.objects.all() + +class PollVoteViewSet(DispatchModelViewSet): + """Viewset for the PollVote Model""" + model = PollVote + serializer_class = PollVoteSerializer + queryset = PollVote.objects.all() + class TemplateViewSet(viewsets.GenericViewSet): """Viewset for Template views""" permission_classes = (IsAuthenticated,) diff --git a/dispatch/migrations/0011_polls.py b/dispatch/migrations/0011_polls.py new file mode 100644 index 000000000..b4f8f079e --- /dev/null +++ b/dispatch/migrations/0011_polls.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-05-23 23:18 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dispatch', '0010_image_tags'), + ] + + operations = [ + migrations.CreateModel( + name='Poll', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('question', models.CharField(max_length=255, unique=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.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('answer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='dispatch.PollAnswer')), + ], + ), + ] diff --git a/dispatch/modules/content/models.py b/dispatch/modules/content/models.py index 6ec301b11..65eb31276 100644 --- a/dispatch/modules/content/models.py +++ b/dispatch/modules/content/models.py @@ -10,7 +10,7 @@ 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 @@ -502,3 +502,26 @@ class Issue(Model): volume = PositiveIntegerField(null=True) issue = PositiveIntegerField(null=True) date = DateTimeField() + +class Poll(Model): + question = CharField(max_length=255, unique=True) + + def save_answers(self, answers): + PollAnswer.objects.filter(poll=self).delete() + for answer in answers: + answer_obj = PollAnswer(poll=self, name=answer['name']) + answer_obj.save() + + def get_total_votes(self): + return PollVote.objects.all().filter(answer__poll=self).count() + +class PollAnswer(Model): + poll = ForeignKey(Poll, related_name='answers', on_delete=CASCADE) + name = CharField(max_length=255) + + def get_votes(self): + """Return the number of votes for this answer""" + return PollVote.objects.all().filter(answer=self).count() + +class PollVote(Model): + answer = ForeignKey(PollAnswer, related_name='votes', on_delete=CASCADE) From b6a78e6e136edc5c0d4fa2fccbc92e07fc48b6c6 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Thu, 24 May 2018 15:51:35 -0700 Subject: [PATCH 03/55] add polls page --- .../manager/src/js/actions/PollsActions.js | 30 ++++++++ .../static/manager/src/js/api/dispatch.js | 20 ++++++ .../manager/src/js/components/Header.js | 1 + .../src/js/components/PersonEditor/index.js | 17 +---- .../src/js/components/PollEditor/PollForm.js | 44 ++++++++++++ .../src/js/components/PollEditor/index.js | 63 ++++++++++++++++ .../manager/src/js/constants/ActionTypes.js | 1 + .../manager/src/js/constants/Schemas.js | 1 + dispatch/static/manager/src/js/index.js | 6 ++ .../manager/src/js/pages/Polls/NewPollPage.js | 12 ++++ .../src/js/pages/Polls/PollIndexPage.js | 72 +++++++++++++++++++ .../manager/src/js/pages/Polls/PollPage.js | 12 ++++ .../manager/src/js/pages/Polls/index.js | 9 +++ dispatch/static/manager/src/js/pages/index.js | 5 +- .../manager/src/js/reducers/AppReducer.js | 4 +- .../manager/src/js/reducers/PollsReducer.js | 14 ++++ 16 files changed, 293 insertions(+), 18 deletions(-) create mode 100644 dispatch/static/manager/src/js/actions/PollsActions.js create mode 100644 dispatch/static/manager/src/js/components/PollEditor/PollForm.js create mode 100644 dispatch/static/manager/src/js/components/PollEditor/index.js create mode 100644 dispatch/static/manager/src/js/pages/Polls/NewPollPage.js create mode 100644 dispatch/static/manager/src/js/pages/Polls/PollIndexPage.js create mode 100644 dispatch/static/manager/src/js/pages/Polls/PollPage.js create mode 100644 dispatch/static/manager/src/js/pages/Polls/index.js create mode 100644 dispatch/static/manager/src/js/reducers/PollsReducer.js diff --git a/dispatch/static/manager/src/js/actions/PollsActions.js b/dispatch/static/manager/src/js/actions/PollsActions.js new file mode 100644 index 000000000..f562cdefd --- /dev/null +++ b/dispatch/static/manager/src/js/actions/PollsActions.js @@ -0,0 +1,30 @@ +import { push } from 'react-router-redux' + +import * as types from '../constants/ActionTypes' +import { pollSchema } from '../constants/Schemas' + +import DispatchAPI from '../api/dispatch' + +import { ResourceActions } from '../util/redux' + +class PollsActions extends ResourceActions { + + search(query) { + let queryObj = {} + + if (query) { + queryObj.q = query + } + + return dispatch => { + dispatch(push({ pathname: '/polls/', query: queryObj })) + } + } + +} + +export default new PollsActions( + types.POLLS, + DispatchAPI.polls, + pollSchema +) diff --git a/dispatch/static/manager/src/js/api/dispatch.js b/dispatch/static/manager/src/js/api/dispatch.js index cc7d5e4c5..9a12b10d2 100644 --- a/dispatch/static/manager/src/js/api/dispatch.js +++ b/dispatch/static/manager/src/js/api/dispatch.js @@ -13,8 +13,10 @@ function prepareMultipartPayload(payload) { for (var key in payload) { if (payload.hasOwnProperty(key)) { if (payload[key] && payload[key].constructor === File) { + console.log('file', payload[key]) formData.append(key, payload[key]) } else if (typeof payload[key] !== 'undefined') { + console.log('not a file', payload[key]) formData.append(key, JSON.parse(JSON.stringify(payload[key]))) } } @@ -429,6 +431,24 @@ const DispatchAPI = { save: (token, userId, data) => { return patchRequest('users', userId, data, token) } + }, + 'polls': { + list: (token, query) => { + return getRequest('polls', null, query, token) + }, + get: (token, pollId) => { + return getRequest('polls', pollId, null, token) + }, + save: (token, pollId, data) => { + return patchRequest('polls', pollId, data, token) + }, + create: (token, data) => { + return postRequest('polls', null, data, token) + }, + delete: (token, pollId) => { + return deleteRequest('polls', pollId, null, token) + }, + } } diff --git a/dispatch/static/manager/src/js/components/Header.js b/dispatch/static/manager/src/js/components/Header.js index 2a6551ade..7dc567c91 100644 --- a/dispatch/static/manager/src/js/components/Header.js +++ b/dispatch/static/manager/src/js/components/Header.js @@ -14,6 +14,7 @@ export default function Header(props) { Articles Pages + Polls Widgets Galleries Files diff --git a/dispatch/static/manager/src/js/components/PersonEditor/index.js b/dispatch/static/manager/src/js/components/PersonEditor/index.js index 6b41a8ccf..438a28569 100644 --- a/dispatch/static/manager/src/js/components/PersonEditor/index.js +++ b/dispatch/static/manager/src/js/components/PersonEditor/index.js @@ -20,19 +20,6 @@ const mapStateToProps = (state) => { } } -function makeFormData(data) { - let formData = new FormData() - - formData.append('full_name', data.full_name || '') - formData.append('slug', data.slug || '') - formData.append('description', data.description || '') - formData.append('image', data.image, data.image ? data.image.name : null) - formData.append('facebook_url', data.facebook_url || '') - formData.append('twitter_url', data.twitter_url || '') - - return formData -} - const mapDispatchToProps = (dispatch) => { return { getListItem: (token, personId) => { @@ -42,10 +29,10 @@ const mapDispatchToProps = (dispatch) => { dispatch(personsActions.set(person)) }, saveListItem: (token, personId, data) => { - dispatch(personsActions.save(token, personId, makeFormData(data))) + dispatch(personsActions.save(token, personId, data)) }, createListItem: (token, data) => { - dispatch(personsActions.create(token, makeFormData(data), AFTER_DELETE)) + dispatch(personsActions.create(token, data, AFTER_DELETE)) }, deleteListItem: (token, personId, next) => { dispatch(personsActions.delete(token, personId, next)) diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js new file mode 100644 index 000000000..1f3eb6ba4 --- /dev/null +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -0,0 +1,44 @@ +import React from 'react' + +import { FormInput, TextInput } from '../inputs' + + +export default class PollForm extends React.Component { + listPolls(query) { + let queryObj = {} + + if (query) { + queryObj['q'] = query + } + + this.props.listPolls(this.props.token, queryObj) + } + + + render() { + return ( +
e.preventDefault()}> + + this.props.update('question', e.target.value) } /> + + + this.props.update('option', e.target.value) } /> + +
+ ) + } +} diff --git a/dispatch/static/manager/src/js/components/PollEditor/index.js b/dispatch/static/manager/src/js/components/PollEditor/index.js new file mode 100644 index 000000000..92678aeed --- /dev/null +++ b/dispatch/static/manager/src/js/components/PollEditor/index.js @@ -0,0 +1,63 @@ +import React from 'react' +import { connect } from 'react-redux' + +import pollsActions from '../../actions/PollsActions' +import PollForm from './PollForm' + +import ItemEditor from '../ItemEditor' + +const TYPE = 'Poll' +const AFTER_DELETE = 'polls' + +const mapStateToProps = (state) => { + return { + listItem: state.app.polls.single, + entities: { + remote: state.app.entities.polls, + local: state.app.entities.local.polls, + }, + token: state.app.auth.token + } +} + +function prepareData(data) { + +} + +const mapDispatchToProps = (dispatch) => { + return { + getListItem: (token, pollId) => { + dispatch(pollsActions.get(token, pollId)) + }, + setListItem: (poll) => { + dispatch(pollsActions.set(poll)) + }, + saveListItem: (token, pollId, data) => { + dispatch(pollsActions.save(token, pollId, prepareData(data))) + }, + createListItem: (token, data) => { + dispatch(pollsActions.create(token, prepareData(data), AFTER_DELETE)) + }, + deleteListItem: (token, pollId, next) => { + dispatch(pollsActions.delete(token, pollId, next)) + } + } +} + +function PollEditorComponent(props) { + return ( + + ) +} + +const PollEditor = connect( + mapStateToProps, + mapDispatchToProps +)(PollEditorComponent) + +export default PollEditor diff --git a/dispatch/static/manager/src/js/constants/ActionTypes.js b/dispatch/static/manager/src/js/constants/ActionTypes.js index 06c845146..859233997 100644 --- a/dispatch/static/manager/src/js/constants/ActionTypes.js +++ b/dispatch/static/manager/src/js/constants/ActionTypes.js @@ -15,6 +15,7 @@ export const GALLERIES = resourceActionTypes('GALLERIES') export const EVENTS = resourceActionTypes('EVENTS', ['COUNT_PENDING']) export const USERS = resourceActionTypes('USERS') export const VIDEOS = resourceActionTypes('VIDEOS') +export const POLLS = resourceActionTypes('POLLS') // Authentication actions export const AUTH = actionTypes('AUTH', [ diff --git a/dispatch/static/manager/src/js/constants/Schemas.js b/dispatch/static/manager/src/js/constants/Schemas.js index a31887ee2..b70acdc44 100644 --- a/dispatch/static/manager/src/js/constants/Schemas.js +++ b/dispatch/static/manager/src/js/constants/Schemas.js @@ -16,6 +16,7 @@ export const widgetSchema = new Schema('widgets') export const eventSchema = new Schema('events') export const userSchema = new Schema('users') export const videoSchema = new Schema('videos') +export const pollSchema = new Schema('polls') articleSchema.define({ section: sectionSchema, diff --git a/dispatch/static/manager/src/js/index.js b/dispatch/static/manager/src/js/index.js index e9b1d5827..ffe701202 100644 --- a/dispatch/static/manager/src/js/index.js +++ b/dispatch/static/manager/src/js/index.js @@ -98,6 +98,12 @@ render(( + + + + + + diff --git a/dispatch/static/manager/src/js/pages/Polls/NewPollPage.js b/dispatch/static/manager/src/js/pages/Polls/NewPollPage.js new file mode 100644 index 000000000..32b12c227 --- /dev/null +++ b/dispatch/static/manager/src/js/pages/Polls/NewPollPage.js @@ -0,0 +1,12 @@ +import React from 'react' + +import PollEditor from '../../components/PollEditor' + +export default function NewPollPage(props) { + return ( + + ) +} diff --git a/dispatch/static/manager/src/js/pages/Polls/PollIndexPage.js b/dispatch/static/manager/src/js/pages/Polls/PollIndexPage.js new file mode 100644 index 000000000..955b86bb5 --- /dev/null +++ b/dispatch/static/manager/src/js/pages/Polls/PollIndexPage.js @@ -0,0 +1,72 @@ +import React from 'react' +import { connect } from 'react-redux' +import { replace } from 'react-router-redux' + +import ItemIndexPage from '../ItemIndexPage' +import pollsActions from '../../actions/PollsActions' + +const mapStateToProps = (state) => { + return { + token: state.app.auth.token, + listItems: state.app.polls.list, + entities: { + listItems: state.app.entities.polls + } + } +} + +const mapDispatchToProps = (dispatch) => { + return { + listListItems: (token, query) => { + dispatch(pollsActions.list(token, query)) + }, + toggleListItem: (pollId) => { + dispatch(pollsActions.toggle(pollId)) + }, + toggleAllListItems: (pollIds) => { + dispatch(pollsActions.toggleAll(pollIds)) + }, + clearSelectedListItems: () => { + dispatch(pollsActions.clearSelected()) + }, + clearListItems: () => { + dispatch(pollsActions.clearAll()) + }, + deleteListItems: (token, pollIds, goDownPage) => { + dispatch(pollsActions.deleteMany(token, pollIds)) + if (goDownPage) { + dispatch(replace({ + pathname: '/polls/', + query: { + page: goDownPage + } + })) + } + }, + searchListItems: (query) => { + dispatch(pollsActions.search(query)) + } + } +} + +function PollsPageComponent(props) { + return ( + item.total_votes + ]} + {... props} /> + ) +} + +const PollsPage = connect( + mapStateToProps, + mapDispatchToProps +)(PollsPageComponent) + +export default PollsPage diff --git a/dispatch/static/manager/src/js/pages/Polls/PollPage.js b/dispatch/static/manager/src/js/pages/Polls/PollPage.js new file mode 100644 index 000000000..c7defd2a4 --- /dev/null +++ b/dispatch/static/manager/src/js/pages/Polls/PollPage.js @@ -0,0 +1,12 @@ +import React from 'react' + +import PollEditor from '../../components/PollEditor' + +export default function PollPage(props) { + return ( + + ) +} diff --git a/dispatch/static/manager/src/js/pages/Polls/index.js b/dispatch/static/manager/src/js/pages/Polls/index.js new file mode 100644 index 000000000..2954a3371 --- /dev/null +++ b/dispatch/static/manager/src/js/pages/Polls/index.js @@ -0,0 +1,9 @@ +import PollIndexPage from './PollIndexPage' +import PollPage from './PollPage' +import NewPollPage from './NewPollPage' + +export { + PollIndexPage as Index, + PollPage as Poll, + NewPollPage as NewPoll +} diff --git a/dispatch/static/manager/src/js/pages/index.js b/dispatch/static/manager/src/js/pages/index.js index 930d2f1ff..e97f8b625 100644 --- a/dispatch/static/manager/src/js/pages/index.js +++ b/dispatch/static/manager/src/js/pages/index.js @@ -17,7 +17,7 @@ import * as Persons from './Persons' import * as Images from './Images' import * as Events from './Events' import * as Videos from './Videos' - +import * as Polls from './Polls' export { LoginPage as Login, LogoutPage as Logout, @@ -36,5 +36,6 @@ export { Widgets as Widgets, Persons as Persons, Events as Events, - Videos as Videos + Videos as Videos, + Polls as Polls } diff --git a/dispatch/static/manager/src/js/reducers/AppReducer.js b/dispatch/static/manager/src/js/reducers/AppReducer.js index 65b302c7b..c7b59fd84 100644 --- a/dispatch/static/manager/src/js/reducers/AppReducer.js +++ b/dispatch/static/manager/src/js/reducers/AppReducer.js @@ -21,6 +21,7 @@ import widgetsReducer from './WidgetsReducer' import eventsReducer from './EventsReducer' import userReducer from './UserReducer' import videosReducer from './VideosReducer' +import pollsReducer from './PollsReducer' export default combineReducers({ entities: entitiesReducer, @@ -43,5 +44,6 @@ export default combineReducers({ widgets: widgetsReducer, events: eventsReducer, users: userReducer, - videos: videosReducer + videos: videosReducer, + polls: pollsReducer }) diff --git a/dispatch/static/manager/src/js/reducers/PollsReducer.js b/dispatch/static/manager/src/js/reducers/PollsReducer.js new file mode 100644 index 000000000..f65e54d2a --- /dev/null +++ b/dispatch/static/manager/src/js/reducers/PollsReducer.js @@ -0,0 +1,14 @@ +import { combineReducers } from 'redux' + +import * as types from '../constants/ActionTypes' + +import { + buildManyResourceReducer, + buildSingleResourceReducer +} from '../util/redux' + + +export default combineReducers({ + list: buildManyResourceReducer(types.POLLS).getReducer(), + single: buildSingleResourceReducer(types.POLLS).getReducer(), +}) From 6e2ebbe34f1cbaf5c2dd7c4503f7184eeba26e55 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Fri, 25 May 2018 12:17:27 -0700 Subject: [PATCH 04/55] poll form working, adding poll name and is open field to poll model --- dispatch/api/serializers.py | 13 +++- dispatch/api/views.py | 8 +- dispatch/migrations/0011_polls.py | 38 ---------- dispatch/modules/content/models.py | 4 +- .../src/js/components/PollEditor/PollForm.js | 74 ++++++++++++++++--- .../src/js/components/PollEditor/index.js | 9 ++- 6 files changed, 89 insertions(+), 57 deletions(-) delete mode 100644 dispatch/migrations/0011_polls.py diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index 5921f9195..eecf21e7b 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -738,12 +738,15 @@ def update(self, instance, validated_data): answer_id = validated_data.get('answer_id', False) try: answer = PollAnswer.objects.get(id=answer_id) + poll_id = answer.poll.id + poll = Poll.objects.get(id=poll_id) except PollAnswer.DoesNotExist: answer = None # Set the vote's answer - if answer is not None: - instance.answer = answer - instance.save() + if poll['is_open']: + if answer is not None: + instance.answer = answer + instance.save() return instance @@ -774,11 +777,15 @@ class PollSerializer(DispatchModelSerializer): write_only=True ) total_votes = serializers.IntegerField(source='get_total_votes', read_only=True) + question = serializers.CharField(required=True) + name = serializers.CharField(required=True) class Meta: model = Poll fields = ( 'id', + 'is_open', + 'name', 'question', 'answers', 'answers_json', diff --git a/dispatch/api/views.py b/dispatch/api/views.py index ca2575203..d19101736 100644 --- a/dispatch/api/views.py +++ b/dispatch/api/views.py @@ -253,7 +253,13 @@ class PollViewSet(DispatchModelViewSet): """Viewset for the Poll model views.""" model = Poll serializer_class = PollSerializer - queryset = Poll.objects.all() + + def get_queryset(self): + queryset = Poll.objects.all() + q = self.request.query_params.get('q', None) + if q is not None: + queryset = queryset.filter(question__icontains=q) + return queryset class PollVoteViewSet(DispatchModelViewSet): """Viewset for the PollVote Model""" diff --git a/dispatch/migrations/0011_polls.py b/dispatch/migrations/0011_polls.py deleted file mode 100644 index b4f8f079e..000000000 --- a/dispatch/migrations/0011_polls.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-05-23 23:18 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('dispatch', '0010_image_tags'), - ] - - operations = [ - migrations.CreateModel( - name='Poll', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('question', models.CharField(max_length=255, unique=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.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('answer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='dispatch.PollAnswer')), - ], - ), - ] diff --git a/dispatch/modules/content/models.py b/dispatch/modules/content/models.py index 65eb31276..8191c6c98 100644 --- a/dispatch/modules/content/models.py +++ b/dispatch/modules/content/models.py @@ -504,7 +504,9 @@ class Issue(Model): date = DateTimeField() class Poll(Model): - question = CharField(max_length=255, unique=True) + name = CharField(max_length=255) + question = CharField(max_length=255) + is_open = BooleanField(default=True) def save_answers(self, answers): PollAnswer.objects.filter(poll=self).delete() diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index 1f3eb6ba4..2d05738ce 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -1,4 +1,5 @@ import React from 'react' +import { Button, Intent } from '@blueprintjs/core' import { FormInput, TextInput } from '../inputs' @@ -14,6 +15,67 @@ export default class PollForm extends React.Component { this.props.listPolls(this.props.token, queryObj) } + addAnswer() { + let answers = this.props.listItem.answers || [] + let id = answers.length + let answer = { + 'id': id, + 'name': '', + 'votes': [], + 'vote_count': 0 + } + answers.push(answer) + this.props.update('answers', answers) + } + + handleUpdateAnswer(id, e) { + var answers = this.props.listItem.answers + for(var i = 0; i < answers.length; i++) { + if(answers[i].id === id){ + answers[i].name = e.target.value + } + } + this.props.update('answers', answers) + } + + + renderAnswers() { + let answers = this.props.listItem.answers.map( + (answer, index) => { + let name = answer.name + let votes = answer.vote_count + let id = index + 1 + + return ( + + this.handleUpdateAnswer(answer.id, e) } /> + + ) + } + ) + return ( +
+ {answers} +
+ ) + } + + renderAddAnswerButton() { + return ( + + ) + } render() { return ( @@ -28,16 +90,8 @@ export default class PollForm extends React.Component { fill={true} onChange={ e => this.props.update('question', e.target.value) } /> - - this.props.update('option', e.target.value) } /> - + {this.props.listItem.answers ? this.renderAnswers() : null} + {this.renderAddAnswerButton()} ) } diff --git a/dispatch/static/manager/src/js/components/PollEditor/index.js b/dispatch/static/manager/src/js/components/PollEditor/index.js index 92678aeed..3a261064b 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/index.js +++ b/dispatch/static/manager/src/js/components/PollEditor/index.js @@ -20,8 +20,9 @@ const mapStateToProps = (state) => { } } -function prepareData(data) { - +function prepareJSONData(data) { + data.answers_json = data.answers + return data } const mapDispatchToProps = (dispatch) => { @@ -33,10 +34,10 @@ const mapDispatchToProps = (dispatch) => { dispatch(pollsActions.set(poll)) }, saveListItem: (token, pollId, data) => { - dispatch(pollsActions.save(token, pollId, prepareData(data))) + dispatch(pollsActions.save(token, pollId, prepareJSONData(data))) }, createListItem: (token, data) => { - dispatch(pollsActions.create(token, prepareData(data), AFTER_DELETE)) + dispatch(pollsActions.create(token, prepareJSONData(data), AFTER_DELETE)) }, deleteListItem: (token, pollId, next) => { dispatch(pollsActions.delete(token, pollId, next)) From 13abc90a17daf148e406aa0e13f447dd4b19afaa Mon Sep 17 00:00:00 2001 From: JamieRL Date: Fri, 25 May 2018 12:18:25 -0700 Subject: [PATCH 05/55] remake migration file for cleanup --- dispatch/migrations/0011_polls.py | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 dispatch/migrations/0011_polls.py diff --git a/dispatch/migrations/0011_polls.py b/dispatch/migrations/0011_polls.py new file mode 100644 index 000000000..c75e6ac75 --- /dev/null +++ b/dispatch/migrations/0011_polls.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-05-25 19:17 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dispatch', '0010_image_tags'), + ] + + 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)), + ], + ), + 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.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('answer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='dispatch.PollAnswer')), + ], + ), + ] From 6f4df180fdb5563e1854232ff35cd06db8836176 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Fri, 25 May 2018 14:09:22 -0700 Subject: [PATCH 06/55] change front end to handle new name field, fix update in poll serializer to save poll name --- dispatch/api/serializers.py | 1 + .../manager/src/js/components/PollEditor/PollForm.js | 10 ++++++++++ .../manager/src/js/components/PollEditor/index.js | 2 +- .../static/manager/src/js/pages/Polls/PollIndexPage.js | 5 +++-- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index eecf21e7b..27a5b766e 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -802,6 +802,7 @@ def create(self, validated_data): def update(self, instance, validated_data): # Update all the basic fields instance.question = validated_data.get('question', instance.question) + instance.name = validated_data.get('name', instance.name) # Save instance before processing/saving content in order to # save associations to correct ID diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index 2d05738ce..8aef66287 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -80,6 +80,16 @@ export default class PollForm extends React.Component { render() { return (
e.preventDefault()}> + + this.props.update('name', e.target.value) } /> + ) } diff --git a/dispatch/static/manager/src/js/pages/Polls/PollIndexPage.js b/dispatch/static/manager/src/js/pages/Polls/PollIndexPage.js index 955b86bb5..2f3cc4eb9 100644 --- a/dispatch/static/manager/src/js/pages/Polls/PollIndexPage.js +++ b/dispatch/static/manager/src/js/pages/Polls/PollIndexPage.js @@ -54,10 +54,11 @@ function PollsPageComponent(props) { item.question, item => item.total_votes ]} {... props} /> From dc1f0c6353933fbfc21e42237324d0d6e8c4f3b2 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Fri, 25 May 2018 16:05:23 -0700 Subject: [PATCH 07/55] Update poll form to have name on front end and can now add and remove answers --- .../src/js/components/PollEditor/PollForm.js | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index 8aef66287..c5effad5d 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -17,7 +17,7 @@ export default class PollForm extends React.Component { addAnswer() { let answers = this.props.listItem.answers || [] - let id = answers.length + let id = answers[answers.length - 1].id + 1 let answer = { 'id': id, 'name': '', @@ -28,6 +28,12 @@ export default class PollForm extends React.Component { this.props.update('answers', answers) } + removeAnswer(id) { + var answers = this.props.listItem.answers + answers.splice(id, 1) + this.props.update('answer', answers) + } + handleUpdateAnswer(id, e) { var answers = this.props.listItem.answers for(var i = 0; i < answers.length; i++) { @@ -44,18 +50,23 @@ export default class PollForm extends React.Component { (answer, index) => { let name = answer.name let votes = answer.vote_count - let id = index + 1 - + let key = index + 1 + let id = index return ( this.handleUpdateAnswer(answer.id, e) } /> + ) } From 24e8320fbc447a4bbb30870f85a30b7869aeda8d Mon Sep 17 00:00:00 2001 From: JamieRL Date: Mon, 28 May 2018 12:26:46 -0700 Subject: [PATCH 08/55] fix issue with adding answers to new poll --- .../manager/src/js/components/PollEditor/PollForm.js | 2 +- .../static/manager/src/js/reducers/ToasterReducer.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index c5effad5d..73b175100 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -17,7 +17,7 @@ export default class PollForm extends React.Component { addAnswer() { let answers = this.props.listItem.answers || [] - let id = answers[answers.length - 1].id + 1 + let id = answers[answers.length - 1] ? answers[answers.length - 1].id + 1 : 1 let answer = { 'id': id, 'name': '', diff --git a/dispatch/static/manager/src/js/reducers/ToasterReducer.js b/dispatch/static/manager/src/js/reducers/ToasterReducer.js index 0c4731218..1425f4c13 100644 --- a/dispatch/static/manager/src/js/reducers/ToasterReducer.js +++ b/dispatch/static/manager/src/js/reducers/ToasterReducer.js @@ -73,6 +73,18 @@ export default function toasterReducer(toaster = {}, action) { case rejected(types.PAGES.UNPUBLISH): return showToast('Page could not be unpublished', Intent.DANGER) + // Polls + case fulfilled(types.POLLS.CREATE): + case fulfilled(types.POLLS.SAVE): + return showToast('Poll saved') + case rejected(types.POLLS.CREATE): + case rejected(types.POLLS.SAVE): + return showToast('Poll could not be savedd', Intent.DANGER) + case fulfilled(types.POLLS.DELETE_MANY): + return showToast(`${action.payload.length} poll${action.payload.length > 1 ? 's' : ''} deleted`) + case rejected(types.POLLS.DELETE_MANY): + return showToast('Some polls could not be deleted', Intent.DANGER) + // Images case fulfilled(types.IMAGES.SAVE): return showToast('Image saved') From 4dd69fe1374c513369a1b801559dbac8461b24f1 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Mon, 28 May 2018 12:36:39 -0700 Subject: [PATCH 09/55] fix typo in toast for failed create or save --- dispatch/static/manager/src/js/reducers/ToasterReducer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dispatch/static/manager/src/js/reducers/ToasterReducer.js b/dispatch/static/manager/src/js/reducers/ToasterReducer.js index 1425f4c13..05f7e73ab 100644 --- a/dispatch/static/manager/src/js/reducers/ToasterReducer.js +++ b/dispatch/static/manager/src/js/reducers/ToasterReducer.js @@ -79,7 +79,7 @@ export default function toasterReducer(toaster = {}, action) { return showToast('Poll saved') case rejected(types.POLLS.CREATE): case rejected(types.POLLS.SAVE): - return showToast('Poll could not be savedd', Intent.DANGER) + return showToast('Poll could not be saved', Intent.DANGER) case fulfilled(types.POLLS.DELETE_MANY): return showToast(`${action.payload.length} poll${action.payload.length > 1 ? 's' : ''} deleted`) case rejected(types.POLLS.DELETE_MANY): From f253753e7904a7596f4050d1e53141f5eaedb801 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Mon, 28 May 2018 13:48:46 -0700 Subject: [PATCH 10/55] can search polls by name or by question --- dispatch/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dispatch/api/views.py b/dispatch/api/views.py index d19101736..d0b034267 100644 --- a/dispatch/api/views.py +++ b/dispatch/api/views.py @@ -258,7 +258,7 @@ def get_queryset(self): queryset = Poll.objects.all() q = self.request.query_params.get('q', None) if q is not None: - queryset = queryset.filter(question__icontains=q) + queryset = queryset.filter(Q(name__icontains=q) | Q(question__icontains=q) ) return queryset class PollVoteViewSet(DispatchModelViewSet): From 44be389ec99481a04b0cf0040adae7060d5773f7 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Mon, 28 May 2018 15:11:45 -0700 Subject: [PATCH 11/55] change pollvoteviewset to only have create method, and allow unauthenticated requests --- dispatch/api/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dispatch/api/views.py b/dispatch/api/views.py index d0b034267..631e6636f 100644 --- a/dispatch/api/views.py +++ b/dispatch/api/views.py @@ -261,8 +261,10 @@ def get_queryset(self): queryset = queryset.filter(Q(name__icontains=q) | Q(question__icontains=q) ) return queryset -class PollVoteViewSet(DispatchModelViewSet): +class PollVoteViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): """Viewset for the PollVote Model""" + permission_classes = (AllowAny,) + model = PollVote serializer_class = PollVoteSerializer queryset = PollVote.objects.all() From 5e81f896a8dfd40e77426cec17bd0632856c0401 Mon Sep 17 00:00:00 2001 From: rowansdabomb Date: Mon, 28 May 2018 15:14:30 -0700 Subject: [PATCH 12/55] polls get data from backend --- dispatch/api/serializers.py | 1 + dispatch/api/views.py | 48 +++++++++------ dispatch/modules/content/embeds.py | 26 ++++++++ dispatch/modules/content/models.py | 1 + .../static/manager/src/js/api/dispatch.js | 2 - .../src/js/components/PollEditor/PollForm.js | 1 + .../manager/src/js/components/WidgetFields.js | 3 +- .../js/components/ZoneEditor/WidgetField.js | 1 - .../src/js/components/fields/PollField.js | 12 ++++ .../src/js/components/fields/WidgetField.js | 1 - .../manager/src/js/components/fields/index.js | 4 +- .../inputs/selects/PollSelectInput.js | 61 +++++++++++++++++++ dispatch/theme/fields.py | 10 ++- 13 files changed, 144 insertions(+), 27 deletions(-) create mode 100644 dispatch/static/manager/src/js/components/fields/PollField.js create mode 100644 dispatch/static/manager/src/js/components/inputs/selects/PollSelectInput.js diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index 27a5b766e..91f051acc 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -779,6 +779,7 @@ class PollSerializer(DispatchModelSerializer): total_votes = serializers.IntegerField(source='get_total_votes', read_only=True) question = serializers.CharField(required=True) name = serializers.CharField(required=True) + # id = serializers.IntegerField(read_only=True) class Meta: model = Poll diff --git a/dispatch/api/views.py b/dispatch/api/views.py index d19101736..c86619f4c 100644 --- a/dispatch/api/views.py +++ b/dispatch/api/views.py @@ -249,24 +249,6 @@ 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(question__icontains=q) - return queryset - -class PollVoteViewSet(DispatchModelViewSet): - """Viewset for the PollVote Model""" - model = PollVote - serializer_class = PollVoteSerializer - queryset = PollVote.objects.all() - class TemplateViewSet(viewsets.GenericViewSet): """Viewset for Template views""" permission_classes = (IsAuthenticated,) @@ -400,6 +382,36 @@ def widgets(self, request, pk=None): return self.get_paginated_response(serializer.data) +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(question__icontains=q) + return queryset + + # def list(self, request): + # q = request.query_params.get('q', None) + # if q is not None: + # zones = ThemeManager.Zones.search(q) + # else: + # zones = ThemeManager.Zones.list() + + # serializer = ZoneSerializer(zones, many=True) + + # return self.get_paginated_response(serializer.data) + + +class PollVoteViewSet(DispatchModelViewSet): + """Viewset for the PollVote Model""" + model = PollVote + serializer_class = PollVoteSerializer + queryset = PollVote.objects.all() + class DashboardViewSet(viewsets.GenericViewSet): permission_classes = (IsAuthenticated,) diff --git a/dispatch/modules/content/embeds.py b/dispatch/modules/content/embeds.py index d4cf435f7..523a9390b 100644 --- a/dispatch/modules/content/embeds.py +++ b/dispatch/modules/content/embeds.py @@ -86,6 +86,31 @@ class PullQuoteEmbed(AbstractTemplateEmbed): class PollEmbed(AbstractTemplateEmbed): TEMPLATE = 'embeds/poll.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' @@ -139,6 +164,7 @@ def prepare_data(self, data): } embeds.register('poll', PollEmbed) +embeds.register('widget', WidgetEmbed) embeds.register('quote', PullQuoteEmbed) embeds.register('code', CodeEmbed) embeds.register('advertisement', AdvertisementEmbed) diff --git a/dispatch/modules/content/models.py b/dispatch/modules/content/models.py index 8191c6c98..c98e43d35 100644 --- a/dispatch/modules/content/models.py +++ b/dispatch/modules/content/models.py @@ -504,6 +504,7 @@ class Issue(Model): date = DateTimeField() class Poll(Model): + # id = UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = CharField(max_length=255) question = CharField(max_length=255) is_open = BooleanField(default=True) diff --git a/dispatch/static/manager/src/js/api/dispatch.js b/dispatch/static/manager/src/js/api/dispatch.js index 9a12b10d2..52ae96f70 100644 --- a/dispatch/static/manager/src/js/api/dispatch.js +++ b/dispatch/static/manager/src/js/api/dispatch.js @@ -13,10 +13,8 @@ function prepareMultipartPayload(payload) { for (var key in payload) { if (payload.hasOwnProperty(key)) { if (payload[key] && payload[key].constructor === File) { - console.log('file', payload[key]) formData.append(key, payload[key]) } else if (typeof payload[key] !== 'undefined') { - console.log('not a file', payload[key]) formData.append(key, JSON.parse(JSON.stringify(payload[key]))) } } diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index 73b175100..ad10f1c9d 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -17,6 +17,7 @@ export default class PollForm extends React.Component { addAnswer() { let answers = this.props.listItem.answers || [] + let id = answers[answers.length - 1] ? answers[answers.length - 1].id + 1 : 1 let answer = { 'id': id, diff --git a/dispatch/static/manager/src/js/components/WidgetFields.js b/dispatch/static/manager/src/js/components/WidgetFields.js index 9bd8e88f6..c9989bfca 100644 --- a/dispatch/static/manager/src/js/components/WidgetFields.js +++ b/dispatch/static/manager/src/js/components/WidgetFields.js @@ -4,7 +4,6 @@ import { connect } from 'react-redux' import WidgetField from './ZoneEditor/WidgetField' class WidgetFieldsComponent extends React.Component { - render() { const widget = this.props.entities.widgets[this.props.widgetId] const fields = widget ? widget.fields.map((field) => ( @@ -14,7 +13,7 @@ class WidgetFieldsComponent extends React.Component { data={this.props.data[field.name] || null} onChange={(data) => this.props.updateField(field.name, data)} /> )) : null - + return (
{fields} diff --git a/dispatch/static/manager/src/js/components/ZoneEditor/WidgetField.js b/dispatch/static/manager/src/js/components/ZoneEditor/WidgetField.js index 91e771fa2..fea45a969 100644 --- a/dispatch/static/manager/src/js/components/ZoneEditor/WidgetField.js +++ b/dispatch/static/manager/src/js/components/ZoneEditor/WidgetField.js @@ -6,7 +6,6 @@ import { FormInput } from '../inputs' export default function WidgetField(props) { const Field = fields[props.field.type] - let fieldError = '' let childErrors = null if (props.field.type == 'widget' && props.error) { diff --git a/dispatch/static/manager/src/js/components/fields/PollField.js b/dispatch/static/manager/src/js/components/fields/PollField.js new file mode 100644 index 000000000..f7633740f --- /dev/null +++ b/dispatch/static/manager/src/js/components/fields/PollField.js @@ -0,0 +1,12 @@ +import React from 'react' + +import PollSelectInput from '../inputs/selects/PollSelectInput' + +export default function PollField(props) { + return ( + props.onChange(selected)} /> + ) +} \ No newline at end of file diff --git a/dispatch/static/manager/src/js/components/fields/WidgetField.js b/dispatch/static/manager/src/js/components/fields/WidgetField.js index c0c24337d..3b3abe4d6 100644 --- a/dispatch/static/manager/src/js/components/fields/WidgetField.js +++ b/dispatch/static/manager/src/js/components/fields/WidgetField.js @@ -38,7 +38,6 @@ export default class WidgetFieldComponent extends React.Component { render() { const widget = this.getWidget() const widgetData = this.getWidgetData() - let fields = widget ? widget.fields.map((field) => ( this.props.onChange(selected)} + fetchResults={(query) => this.listPolls(query)} + attribute='name' + editMessage={this.props.selected ? `Edit ${label}` : `Add ${label}`} /> + ) + } + +} + +const mapStateToProps = (state) => { + return { + polls: state.app.polls.list, + entities: { + polls: state.app.entities.polls + }, + token: state.app.auth.token + } +} + +const mapDispatchToProps = (dispatch) => { + return { + listPolls: (token, query) => { + dispatch(pollsActions.list(token, query)) + } + } +} + +const PollSelectInput = connect( + mapStateToProps, + mapDispatchToProps +)(PollSelectInputComponent) + +export default PollSelectInput diff --git a/dispatch/theme/fields.py b/dispatch/theme/fields.py index da635a8d2..f8311817c 100644 --- a/dispatch/theme/fields.py +++ b/dispatch/theme/fields.py @@ -4,8 +4,8 @@ from django.utils.dateparse import parse_datetime from django.core.exceptions import ObjectDoesNotExist -from dispatch.models import Article, Image -from dispatch.api.serializers import ArticleSerializer, ImageSerializer, WidgetSerializer +from dispatch.models import Article, Image, Poll +from dispatch.api.serializers import ArticleSerializer, ImageSerializer, WidgetSerializer, PollSerializer from dispatch.theme.exceptions import InvalidField, WidgetNotFound from dispatch.theme.validators import is_valid_id @@ -319,3 +319,9 @@ def prepare_data(self, data): widget = self.get_widget(data['id']) widget.set_data(data['data']) return widget + +class PollField(ModelField): + type = 'poll' + + model = Poll + serializer = PollSerializer From 23ac7503eab0e5925df3547bb3a09e6d46cdeae6 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Mon, 28 May 2018 16:43:18 -0700 Subject: [PATCH 13/55] Add open/close poll functinality to front end --- dispatch/api/serializers.py | 1 + .../src/js/components/PollEditor/PollForm.js | 65 +++++++++++++++++-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index 27a5b766e..c69a72bf6 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -803,6 +803,7 @@ def update(self, instance, validated_data): # 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) # Save instance before processing/saving content in order to # save associations to correct ID diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index 73b175100..5e6095136 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -15,6 +15,33 @@ export default class PollForm extends React.Component { this.props.listPolls(this.props.token, queryObj) } + constructor(props){ + super(props) + if(props.listItem.id === 'new') { + this.state = { + answers : [ + { + 'id': 0, + 'name': '', + 'votes': [], + 'vote_count': 0 + }, + { + 'id': 1, + 'name': '', + 'votes': [], + 'vote_count': 0 + } + ] + } + } + else { + this.state = { + answers: props.listItem.answers + } + } + } + addAnswer() { let answers = this.props.listItem.answers || [] let id = answers[answers.length - 1] ? answers[answers.length - 1].id + 1 : 1 @@ -35,7 +62,7 @@ export default class PollForm extends React.Component { } handleUpdateAnswer(id, e) { - var answers = this.props.listItem.answers + var answers = this.state.answers for(var i = 0; i < answers.length; i++) { if(answers[i].id === id){ answers[i].name = e.target.value @@ -44,9 +71,16 @@ export default class PollForm extends React.Component { this.props.update('answers', answers) } + openPoll() { + this.props.update('is_open', true) + } + + closePoll() { + this.props.update('is_open', false) + } renderAnswers() { - let answers = this.props.listItem.answers.map( + let answers = this.state.answers.map( (answer, index) => { let name = answer.name let votes = answer.vote_count @@ -88,6 +122,26 @@ export default class PollForm extends React.Component { ) } + renderPollToggle() { + if (this.props.listItem.is_open) { + return ( + + ) + } + else + return ( + + ) + } + render() { return ( e.preventDefault()}> @@ -111,8 +165,11 @@ export default class PollForm extends React.Component { fill={true} onChange={ e => this.props.update('question', e.target.value) } /> - {this.props.listItem.answers ? this.renderAnswers() : null} - {this.renderAddAnswerButton()} +
+ {this.renderAnswers()} + {this.renderAddAnswerButton()} +
+ {(this.props.listItem.id === 'new') ? null : this.renderPollToggle()} ) } From b3e933dff5b631cfb13aa805446cae9c4985dbc5 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Tue, 29 May 2018 09:51:08 -0700 Subject: [PATCH 14/55] put form in div --- .../src/js/components/PollEditor/PollForm.js | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index 5e6095136..89363dee3 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -144,33 +144,33 @@ export default class PollForm extends React.Component { render() { return ( -
e.preventDefault()}> - - this.props.update('name', e.target.value) } /> - - - this.props.update('question', e.target.value) } /> - -
+
+ e.preventDefault()}> + + this.props.update('name', e.target.value) } /> + + + this.props.update('question', e.target.value) } /> + {this.renderAnswers()} {this.renderAddAnswerButton()} -
+ {(this.props.listItem.id === 'new') ? null : this.renderPollToggle()} - +
) } } From 94905009cef66865d92287d23dae4436037e6cf7 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Tue, 29 May 2018 10:20:51 -0700 Subject: [PATCH 15/55] add show_results field to model and can change it in the front end, new migrations as well for the new field --- dispatch/api/serializers.py | 4 +-- dispatch/api/views.py | 3 +- dispatch/migrations/0011_polls.py | 3 +- dispatch/modules/content/models.py | 2 +- .../src/js/components/PollEditor/PollForm.js | 29 ++++++++++++++++++- .../src/styles/components/poll_form.scss | 3 ++ 6 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 dispatch/static/manager/src/styles/components/poll_form.scss diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index e0e3f40d1..a45b4043f 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -779,13 +779,13 @@ class PollSerializer(DispatchModelSerializer): total_votes = serializers.IntegerField(source='get_total_votes', read_only=True) question = serializers.CharField(required=True) name = serializers.CharField(required=True) - # id = serializers.IntegerField(read_only=True) class Meta: model = Poll fields = ( 'id', 'is_open', + 'show_results', 'name', 'question', 'answers', @@ -805,7 +805,7 @@ def update(self, instance, validated_data): 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() diff --git a/dispatch/api/views.py b/dispatch/api/views.py index 7b408cde9..5558d23ff 100644 --- a/dispatch/api/views.py +++ b/dispatch/api/views.py @@ -267,7 +267,6 @@ class PollVoteViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): model = PollVote serializer_class = PollVoteSerializer - queryset = PollVote.objects.all() class TemplateViewSet(viewsets.GenericViewSet): """Viewset for Template views""" @@ -401,7 +400,7 @@ def widgets(self, request, pk=None): serializer = WidgetSerializer(zone.widgets, many=True) return self.get_paginated_response(serializer.data) - + class DashboardViewSet(viewsets.GenericViewSet): permission_classes = (IsAuthenticated,) diff --git a/dispatch/migrations/0011_polls.py b/dispatch/migrations/0011_polls.py index c75e6ac75..94a8e139b 100644 --- a/dispatch/migrations/0011_polls.py +++ b/dispatch/migrations/0011_polls.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-05-25 19:17 +# Generated by Django 1.11 on 2018-05-29 16:55 from __future__ import unicode_literals from django.db import migrations, models @@ -20,6 +20,7 @@ class Migration(migrations.Migration): ('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( diff --git a/dispatch/modules/content/models.py b/dispatch/modules/content/models.py index c98e43d35..2a12b03ce 100644 --- a/dispatch/modules/content/models.py +++ b/dispatch/modules/content/models.py @@ -504,10 +504,10 @@ class Issue(Model): date = DateTimeField() class Poll(Model): - # id = UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = CharField(max_length=255) question = CharField(max_length=255) is_open = BooleanField(default=True) + show_results = BooleanField(default=True) def save_answers(self, answers): PollAnswer.objects.filter(poll=self).delete() diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index f1b951310..639860a6d 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -2,7 +2,9 @@ import React from 'react' import { Button, Intent } from '@blueprintjs/core' import { FormInput, TextInput } from '../inputs' +import SelectInput from '../inputs/selects/SelectInput' +require('../../../styles/components/poll_form.scss') export default class PollForm extends React.Component { listPolls(query) { @@ -143,6 +145,29 @@ export default class PollForm extends React.Component { ) } + renderOptions() { + const OPTIONS = [ + [true, 'Show results'], + [false, 'Hide results'], + ] + + return ( +
+ +
+ this.props.update('show_results', e.target.value)}/> +
+
+ {(this.props.listItem.id === 'new') ? null : this.renderPollToggle()} +
+ ) + } + render() { return (
@@ -169,8 +194,10 @@ export default class PollForm extends React.Component { {this.renderAnswers()} {this.renderAddAnswerButton()} + {this.renderOptions()} - {(this.props.listItem.id === 'new') ? null : this.renderPollToggle()} +
+
) } diff --git a/dispatch/static/manager/src/styles/components/poll_form.scss b/dispatch/static/manager/src/styles/components/poll_form.scss new file mode 100644 index 000000000..5cc465862 --- /dev/null +++ b/dispatch/static/manager/src/styles/components/poll_form.scss @@ -0,0 +1,3 @@ +.c-poll-form__results-select { + width: 9em; +} From b0c4ec34eab99f54a759db1c65a69615f6412443 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Tue, 29 May 2018 10:45:07 -0700 Subject: [PATCH 16/55] change open/close poll to select input --- .../src/js/components/PollEditor/PollForm.js | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index 639860a6d..0d53c3445 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -126,29 +126,28 @@ export default class PollForm extends React.Component { } renderPollToggle() { - if (this.props.listItem.is_open) { - return ( - - ) - } - else - return ( - - ) + const OPTIONS = [ + [true, 'Poll Open'], + [false, 'Poll Closed'] + ] + return ( + +
+ this.props.update('is_open', e.target.value)}/> +
+
+ ) } renderOptions() { const OPTIONS = [ - [true, 'Show results'], - [false, 'Hide results'], + [true, 'Show Results'], + [false, 'Hide Results'], ] return ( From cc0fe209c7173bad0aeaf4668b146ea35fc47079 Mon Sep 17 00:00:00 2001 From: rowansdabomb Date: Tue, 29 May 2018 11:05:23 -0700 Subject: [PATCH 17/55] add poll preview to poll editor --- .../src/js/components/PollEditor/Poll.js | 103 +++++++++ .../src/js/components/PollEditor/PollForm.js | 66 +++--- .../manager/src/styles/components/poll.scss | 197 ++++++++++++++++++ .../src/styles/components/poll_form.scss | 17 ++ 4 files changed, 357 insertions(+), 26 deletions(-) create mode 100644 dispatch/static/manager/src/js/components/PollEditor/Poll.js create mode 100644 dispatch/static/manager/src/styles/components/poll.scss create mode 100644 dispatch/static/manager/src/styles/components/poll_form.scss diff --git a/dispatch/static/manager/src/js/components/PollEditor/Poll.js b/dispatch/static/manager/src/js/components/PollEditor/Poll.js new file mode 100644 index 000000000..88b193054 --- /dev/null +++ b/dispatch/static/manager/src/js/components/PollEditor/Poll.js @@ -0,0 +1,103 @@ +import React, { Component } from 'react' +import { Button, Intent } from '@blueprintjs/core' + +require('../../../styles/components/poll.scss') + +const COLOR_OPACITY = .8 + +class Poll extends Component { + constructor(props){ + super(props) + this.state = { + answers: [], + votes: [], + voting: false, + pollQuestion: this.props.question, + loading: true, + } + } + + componentDidMount() { + let answers = [] + let votes = [] + for(let answer of this.props.answers){ + answers.push(answer['name']) + votes.push(answer['vote_count']) + } + this.setState({ + answers: answers, + votes: votes, + loading: false, + }) + this.forceUpdate() + } + + getPollResult(index) { + if(!this.state.voting){ + let total = this.state.votes.reduce((acc, val) => { return acc + val }) + let width = String((100*this.state.votes[index]/total).toFixed(0)) + '%' + return width + } + return 0 + } + + toggleResults() { + this.setState(prevstate => ({ + voting: !prevstate.voting + })) + } + + render() { + const showResult = this.state.voting ? 0 : COLOR_OPACITY + const notShowResult = this.state.voting ? COLOR_OPACITY : 0 + return ( +
+ {!this.state.loading && +
+
+

{this.state.pollQuestion}

+
+ {this.state.answers.map((answer, index) => { + return( + + )}) + } +
+
+ +
+ } + {this.state.loading && 'Loading Poll...'} +
+ ) + } +} + +export default Poll diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index a69256078..1405ed9e8 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -2,7 +2,9 @@ import React from 'react' import { Button, Intent } from '@blueprintjs/core' import { FormInput, TextInput } from '../inputs' +import Poll from './Poll' +require('../../../styles/components/poll_form.scss') export default class PollForm extends React.Component { listPolls(query) { @@ -145,33 +147,45 @@ export default class PollForm extends React.Component { render() { return ( -
e.preventDefault()}> - - this.props.update('name', e.target.value) } /> - - - this.props.update('question', e.target.value) } /> - -
- {this.renderAnswers()} - {this.renderAddAnswerButton()} +
+
+ e.preventDefault()}> + + this.props.update('name', e.target.value) } /> + + + this.props.update('question', e.target.value) } /> + +
+ {this.renderAnswers()} + {this.renderAddAnswerButton()} +
+ {(this.props.listItem.id === 'new') ? null : this.renderPollToggle()} +
- {(this.props.listItem.id === 'new') ? null : this.renderPollToggle()} - +
+ +
+
) } } diff --git a/dispatch/static/manager/src/styles/components/poll.scss b/dispatch/static/manager/src/styles/components/poll.scss new file mode 100644 index 000000000..a394da442 --- /dev/null +++ b/dispatch/static/manager/src/styles/components/poll.scss @@ -0,0 +1,197 @@ +@import '../utilities/colors'; + +.poll-container { + //structure + padding-left: 1.5rem; + padding-right: 1.5rem; + margin: 15px 0; + max-width: 500px; + + + // Border + border: 2px solid black; + + h1 { + display: block; + font-size: 2em; + -webkit-margin-before: 0.67em; + -webkit-margin-after: 0.67em; + -webkit-margin-start: 0px; + -webkit-margin-end: 0px; + font-weight: bold; + } +} + +.poll-answer-form { + // Structure + display: flex; + flex-direction: column; + max-width: 95%; + margin-bottom: 15px; +} + +.poll-button-label { + // Structure + position: relative; + display: flex; + align-items: center; + margin: 5px 0px; + padding: 8px; + + // Text + color: inherit; + font-size: 16px; + font-family: "ub-lft-etica", "LFT Etica", "Helvetica Neue", "Helvetica", "Arial", sans-serif; + + // Extra + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + &:hover .poll-button .poll-button-inner { + // Extra + opacity: 1; + } +} + +.poll-results{ + .poll-button-label{ + // Extra + cursor: auto; + } +} +.poll-voting{ + .poll-button-label{ + // Extra + cursor: pointer; + } +} + +.poll-result-bar { + // Background + background-color: $color-steel-blue; + border-radius: 5px; + + + // Structure + position: absolute; + display: flex; + left: 0; + height: 100%; + z-index: -1; + + // Extra + transition: width .5s, opacity .25s; +} + +.poll-container input { + // Structure + position: absolute; + + // Extra + cursor: pointer; + opacity: 0; +} + +.poll-percentage { + // Structure + position: absolute; + margin-left: 3px; + + // Text + font-size: 14px; +} + +.poll-button { + // Structure + position: absolute; + height: 15px; + width: 15px; + margin-left: 3px; + display: flex; + align-items: center; + justify-content: center; + + // Border + border-radius: 15px; + border: 3px solid $color-steel-blue; + + // Extra + transition: opacity .1s; + + .poll-button-inner { + // Structure + height: 11px; + width: 11px; + + // Border + border-radius: 11px; + + // Background + background-color: $color-steel-blue; + + // Extra + opacity: 0; + transition: opacity .1s; + } +} + +.poll-selected { + // Structure + width: 15px; + height: 15px; + align-self: center; + position: absolute; + right: -25px; + display: flex; + align-items: center; + justify-content: center; + + // Border + border: 3px solid $color-steel-blue; + border-radius: 15px; + + // Extra + opacity: 1; + + &:after{ + // Structure + left: 9px; + top: 5px; + width: 5px; + height: 10px; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); + + // Border + border: solid white; + border-width: 0 3px 3px 0; + } + + .poll-checkmark { + // Structure + position:relative; + top: -1px; + width: 4px; + height: 9px; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); + + // Border + border: solid $color-steel-blue; + border-width: 0 3px 3px 0; + } +} + +.poll-answer-text { + // Structure + margin-left: 3rem; +} + +.poll-edit-button { + // Structure + margin-bottom: 15px; +} \ No newline at end of file diff --git a/dispatch/static/manager/src/styles/components/poll_form.scss b/dispatch/static/manager/src/styles/components/poll_form.scss new file mode 100644 index 000000000..8bf619d29 --- /dev/null +++ b/dispatch/static/manager/src/styles/components/poll_form.scss @@ -0,0 +1,17 @@ +.c-poll-form-container{ + // Structure + display: flex; + flex-direction: row; +} +.c-poll-container { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} +.c-equal-width{ + // Structure + flex: 1; + padding: 0 1.5rem; +} \ No newline at end of file From 822f8845d29d44d2283fdc25514fd4ee0a706d51 Mon Sep 17 00:00:00 2001 From: rowansdabomb Date: Tue, 29 May 2018 11:24:51 -0700 Subject: [PATCH 18/55] merge attempt 3 --- .../src/js/components/PollEditor/PollForm.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index 4eaff8093..a42fa4c92 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -131,6 +131,28 @@ export default class PollForm extends React.Component { [true, 'Poll Open'], [false, 'Poll Closed'] ] + return ( + +
+ this.props.update('is_open', e.target.value)}/> +
+
+ ) + } + + + renderOptions() { + const OPTIONS = [ + [true, 'Show results'], + [false, 'Hide results'], + [true, 'Show Results'], + [false, 'Hide Results'], + ] } render() { From 6ef9f4663d59c8a14854720a12d8275c97fc693b Mon Sep 17 00:00:00 2001 From: rowansdabomb Date: Tue, 29 May 2018 13:21:18 -0700 Subject: [PATCH 19/55] -fix poll-preview update issues -restyle remove answer button --- .../src/js/components/PollEditor/Poll.js | 68 ++++++++++++++++--- .../src/js/components/PollEditor/PollForm.js | 8 +-- .../manager/src/styles/components/poll.scss | 9 ++- .../src/styles/components/poll_form.scss | 20 ++++++ 4 files changed, 88 insertions(+), 17 deletions(-) diff --git a/dispatch/static/manager/src/js/components/PollEditor/Poll.js b/dispatch/static/manager/src/js/components/PollEditor/Poll.js index 88b193054..847d0b9eb 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/Poll.js +++ b/dispatch/static/manager/src/js/components/PollEditor/Poll.js @@ -11,31 +11,69 @@ class Poll extends Component { this.state = { answers: [], votes: [], - voting: false, - pollQuestion: this.props.question, + showResults: true, + pollQuestion: '', loading: true, + noVotes: false, } } componentDidMount() { + this.update() + this.forceUpdate() + } + + componentDidUpdate() { + this.update() + } + + shouldComponentUpdate(nextProps, nextState){ + if(this.state.showResults !== nextState.showResults){ + return true + } + if(this.props.answers.length === nextProps.answers.length){ + setTimeout(()=> { + this.forceUpdate() + }, 250) + return false + } + return true + } + + update() { let answers = [] let votes = [] for(let answer of this.props.answers){ answers.push(answer['name']) votes.push(answer['vote_count']) } + let temp = votes.filter((item) => {return item === 0}) + let noVotes = false + if(temp.length === votes.length){ + //no votes yet, populate with dummy data for better pole visualization + votes[0] = 2 + votes[1] = 1 + noVotes = true + } this.setState({ answers: answers, votes: votes, loading: false, + pollQuestion: this.props.question, + noVotes: noVotes, }) - this.forceUpdate() } getPollResult(index) { - if(!this.state.voting){ + + if(this.state.showResults){ + let width = 0 let total = this.state.votes.reduce((acc, val) => { return acc + val }) - let width = String((100*this.state.votes[index]/total).toFixed(0)) + '%' + + if(total !== 0){ + width = String((100*this.state.votes[index]/total).toFixed(0)) + '%' + } + return width } return 0 @@ -43,17 +81,23 @@ class Poll extends Component { toggleResults() { this.setState(prevstate => ({ - voting: !prevstate.voting + showResults: !prevstate.showResults })) } + componentWillReceiveProps(nextProps){ + if(this.props !== nextProps){ + this.forceUpdate() + } + } + render() { - const showResult = this.state.voting ? 0 : COLOR_OPACITY - const notShowResult = this.state.voting ? COLOR_OPACITY : 0 + const notShowResult= this.state.showResults ? 0 : COLOR_OPACITY + const showResult = this.state.showResults ? COLOR_OPACITY : 0 return (
{!this.state.loading && -
+

{this.state.pollQuestion}

@@ -86,12 +130,14 @@ class Poll extends Component { }
+ {this.state.noVotes && No votes yet, poll data above is for visualization purposes only!} +
+
} {this.state.loading && 'Loading Poll...'} diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index a42fa4c92..c017da5d5 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -100,11 +100,11 @@ export default class PollForm extends React.Component { value={name || ''} fill={true} onChange={ e => this.handleUpdateAnswer(answer.id, e) } /> - + Remove answer + ) } diff --git a/dispatch/static/manager/src/styles/components/poll.scss b/dispatch/static/manager/src/styles/components/poll.scss index a394da442..d71685ecf 100644 --- a/dispatch/static/manager/src/styles/components/poll.scss +++ b/dispatch/static/manager/src/styles/components/poll.scss @@ -1,5 +1,10 @@ @import '../utilities/colors'; +.poll-preview{ + i { + font-style: italic; + } +} .poll-container { //structure padding-left: 1.5rem; @@ -191,7 +196,7 @@ margin-left: 3rem; } -.poll-edit-button { +.poll-results-button { // Structure - margin-bottom: 15px; + margin-top: 15px; } \ No newline at end of file diff --git a/dispatch/static/manager/src/styles/components/poll_form.scss b/dispatch/static/manager/src/styles/components/poll_form.scss index 7fe4a9d3c..3561038e1 100644 --- a/dispatch/static/manager/src/styles/components/poll_form.scss +++ b/dispatch/static/manager/src/styles/components/poll_form.scss @@ -4,6 +4,7 @@ flex-direction: row; } .c-poll-container { + // Structure width: 100%; height: 100%; display: flex; @@ -16,5 +17,24 @@ padding: 0 1.5rem; } .c-poll-form__results-select { + // Structure width: 9em; } +.poll-form.pt-icon-standard { + // Structure + margin-right: 0.25rem; + position: absolute; + top: 0; + left: 140px; + cursor: pointer; + &:hover { + text-decoration: underline; + opacity: .8; + } +} +.poll-form .pt-icon-standard-text{ + // Structure + position: relative; + top: 2px; + margin-left: .25rem; +} From d5f9403c0029258b3782c3a83678967e3476e6dd Mon Sep 17 00:00:00 2001 From: JamieRL Date: Tue, 29 May 2018 13:37:43 -0700 Subject: [PATCH 20/55] updating or deleting answers no longer deletes all the votes, fixed issue with removing answers --- dispatch/api/serializers.py | 14 +++++------ dispatch/modules/content/models.py | 17 ++++++++++--- .../src/js/components/PollEditor/PollForm.js | 24 ++++++------------- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index a45b4043f..00a1f46f6 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -743,10 +743,10 @@ def update(self, instance, validated_data): except PollAnswer.DoesNotExist: answer = None # Set the vote's answer - #if poll['is_open']: - if answer is not None: - instance.answer = answer - instance.save() + if poll.is_open: + if answer is not None: + instance.answer = answer + instance.save() return instance @@ -798,9 +798,9 @@ def create(self, validated_data): instance = Poll() # Then save as usual - return self.update(instance, validated_data) + return self.update(instance, validated_data, True) - def update(self, instance, validated_data): + 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) @@ -813,6 +813,6 @@ def update(self, instance, validated_data): answers = validated_data.get('answers_json', False) if isinstance(answers, list): - instance.save_answers(answers) + instance.save_answers(answers, is_new) return instance diff --git a/dispatch/modules/content/models.py b/dispatch/modules/content/models.py index 2a12b03ce..55153880d 100644 --- a/dispatch/modules/content/models.py +++ b/dispatch/modules/content/models.py @@ -509,12 +509,23 @@ class Poll(Model): is_open = BooleanField(default=True) show_results = BooleanField(default=True) - def save_answers(self, answers): - PollAnswer.objects.filter(poll=self).delete() + def save_answers(self, answers, is_new): + if is_new is False: + self.delete_old_answers(answers) for answer in answers: - answer_obj = PollAnswer(poll=self, name=answer['name']) + try: + answer_obj = PollAnswer.objects.get(poll=self, id=answer['id']) + except PollAnswer.DoesNotExist: + answer_obj = PollAnswer(poll=self, name=answer['name']) answer_obj.save() + def delete_old_answers(self, answers): + old_answers = PollAnswer.objects.filter(poll=self) + for answer in answers: + old_answers = old_answers.exclude(id=answer['id']) + old_answers.delete() + + def get_total_votes(self): return PollVote.objects.all().filter(answer__poll=self).count() diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index 0d53c3445..6a48f3ec6 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -7,17 +7,8 @@ import SelectInput from '../inputs/selects/SelectInput' require('../../../styles/components/poll_form.scss') export default class PollForm extends React.Component { - listPolls(query) { - let queryObj = {} - if (query) { - queryObj['q'] = query - } - - this.props.listPolls(this.props.token, queryObj) - } - - constructor(props){ + constructor(props) { super(props) if(props.listItem.id === 'new') { this.state = { @@ -45,7 +36,7 @@ export default class PollForm extends React.Component { } addAnswer() { - let answers = this.props.listItem.answers || [] + let answers = this.state.answers let id = answers[answers.length - 1] ? answers[answers.length - 1].id + 1 : 1 let answer = { @@ -59,9 +50,9 @@ export default class PollForm extends React.Component { } removeAnswer(id) { - var answers = this.props.listItem.answers + let answers = this.state.answers answers.splice(id, 1) - this.props.update('answer', answers) + this.props.update('answers', answers) } handleUpdateAnswer(id, e) { @@ -125,11 +116,12 @@ export default class PollForm extends React.Component { ) } - renderPollToggle() { + renderPollOpenSelect() { const OPTIONS = [ [true, 'Poll Open'], [false, 'Poll Closed'] ] + return ( this.props.update('show_results', e.target.value)}/>
- {(this.props.listItem.id === 'new') ? null : this.renderPollToggle()} + {(this.props.listItem.id === 'new') ? null : this.renderPollOpenSelect()}
) } @@ -195,8 +187,6 @@ export default class PollForm extends React.Component { {this.renderAddAnswerButton()} {this.renderOptions()} -
-
) } From 6b8d8582ebd638c1add42c56a001357e7521ac6f Mon Sep 17 00:00:00 2001 From: JamieRL Date: Tue, 29 May 2018 14:17:10 -0700 Subject: [PATCH 21/55] fix poll vote update to not error out if there is no poll, set up pollvoteviewset to get vote_id from cookie and delete vote --- dispatch/api/serializers.py | 3 ++- dispatch/api/views.py | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index 00a1f46f6..406841af0 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -742,8 +742,9 @@ def update(self, instance, validated_data): poll = Poll.objects.get(id=poll_id) except PollAnswer.DoesNotExist: answer = None + poll = None # Set the vote's answer - if poll.is_open: + if poll is not None and poll.is_open: if answer is not None: instance.answer = answer instance.save() diff --git a/dispatch/api/views.py b/dispatch/api/views.py index 5558d23ff..41d4ea919 100644 --- a/dispatch/api/views.py +++ b/dispatch/api/views.py @@ -268,6 +268,15 @@ class PollVoteViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): model = PollVote serializer_class = PollVoteSerializer + def create(self, request): + vote_id = request.COOKIES.get('vote_id', None) + print('cookie', vote_id) + if vote_id is not None: + vote = PollVote.objects.get(id=vote_id) + vote.delete() + return super(PollVoteViewSet, self).create(request) + + class TemplateViewSet(viewsets.GenericViewSet): """Viewset for Template views""" permission_classes = (IsAuthenticated,) From 6d68fba21691d8b5eadb91aa5e47cbb0adf6beeb Mon Sep 17 00:00:00 2001 From: rowansdabomb Date: Tue, 29 May 2018 16:29:30 -0700 Subject: [PATCH 22/55] optimized poll.js --- .../src/js/components/PollEditor/Poll.js | 68 +++++-------------- .../manager/src/styles/components/poll.scss | 1 + 2 files changed, 18 insertions(+), 51 deletions(-) diff --git a/dispatch/static/manager/src/js/components/PollEditor/Poll.js b/dispatch/static/manager/src/js/components/PollEditor/Poll.js index 847d0b9eb..fd46ee12f 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/Poll.js +++ b/dispatch/static/manager/src/js/components/PollEditor/Poll.js @@ -6,38 +6,9 @@ require('../../../styles/components/poll.scss') const COLOR_OPACITY = .8 class Poll extends Component { - constructor(props){ - super(props) - this.state = { - answers: [], - votes: [], - showResults: true, - pollQuestion: '', - loading: true, - noVotes: false, - } - } componentDidMount() { this.update() - this.forceUpdate() - } - - componentDidUpdate() { - this.update() - } - - shouldComponentUpdate(nextProps, nextState){ - if(this.state.showResults !== nextState.showResults){ - return true - } - if(this.props.answers.length === nextProps.answers.length){ - setTimeout(()=> { - this.forceUpdate() - }, 250) - return false - } - return true } update() { @@ -50,28 +21,28 @@ class Poll extends Component { let temp = votes.filter((item) => {return item === 0}) let noVotes = false if(temp.length === votes.length){ - //no votes yet, populate with dummy data for better pole visualization + //no votes yet, populate with dummy data for better poll visualization votes[0] = 2 votes[1] = 1 noVotes = true } - this.setState({ + + return { answers: answers, votes: votes, loading: false, pollQuestion: this.props.question, noVotes: noVotes, - }) + } } - getPollResult(index) { - - if(this.state.showResults){ + getPollResult(index, votes) { + if(this.props.showResults){ let width = 0 - let total = this.state.votes.reduce((acc, val) => { return acc + val }) + let total = votes.reduce((acc, val) => { return acc + val }) if(total !== 0){ - width = String((100*this.state.votes[index]/total).toFixed(0)) + '%' + width = String((100*votes[index]/total).toFixed(0)) + '%' } return width @@ -85,23 +56,19 @@ class Poll extends Component { })) } - componentWillReceiveProps(nextProps){ - if(this.props !== nextProps){ - this.forceUpdate() - } - } - render() { + const { answers, votes, loading, pollQuestion, noVotes } = this.update() + const notShowResult= this.state.showResults ? 0 : COLOR_OPACITY const showResult = this.state.showResults ? COLOR_OPACITY : 0 return (
- {!this.state.loading && + {!loading &&
-

{this.state.pollQuestion}

+

{pollQuestion}

- {this.state.answers.map((answer, index) => { + {answers.map((answer, index) => { return( @@ -130,7 +96,7 @@ class Poll extends Component { }
- {this.state.noVotes && No votes yet, poll data above is for visualization purposes only!} + {noVotes && No votes yet, poll data above is for visualization purposes only!}
} - {this.state.loading && 'Loading Poll...'} + {loading && 'Loading Poll...'}
) } diff --git a/dispatch/static/manager/src/styles/components/poll.scss b/dispatch/static/manager/src/styles/components/poll.scss index d71685ecf..fda59416f 100644 --- a/dispatch/static/manager/src/styles/components/poll.scss +++ b/dispatch/static/manager/src/styles/components/poll.scss @@ -58,6 +58,7 @@ // Extra opacity: 1; } + } .poll-results{ From df1d2a69b767d63b21c118c1c2a395fa5901f2d1 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Tue, 29 May 2018 17:00:41 -0700 Subject: [PATCH 23/55] only delete votes if the vote_id exists --- dispatch/api/views.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dispatch/api/views.py b/dispatch/api/views.py index 41d4ea919..6826fe5e7 100644 --- a/dispatch/api/views.py +++ b/dispatch/api/views.py @@ -270,10 +270,12 @@ class PollVoteViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): def create(self, request): vote_id = request.COOKIES.get('vote_id', None) - print('cookie', vote_id) if vote_id is not None: - vote = PollVote.objects.get(id=vote_id) - vote.delete() + try: + vote = PollVote.objects.get(id=vote_id) + vote.delete() + except PollVote.DoesNotExist: + pass return super(PollVoteViewSet, self).create(request) From 6e22e781b1ed80a5f7f1755a81b07afa3a175de3 Mon Sep 17 00:00:00 2001 From: rowansdabomb Date: Wed, 30 May 2018 17:03:14 -0700 Subject: [PATCH 24/55] polls --- dispatch/api/serializers.py | 3 ++- dispatch/api/views.py | 23 +++++++++++----- .../src/js/components/PollEditor/Poll.js | 27 ++++++++++++++----- .../src/js/components/PollEditor/PollForm.js | 1 + .../src/js/components/PollEditor/index.js | 1 + 5 files changed, 41 insertions(+), 14 deletions(-) diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index 406841af0..e2771086e 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -727,7 +727,7 @@ class Meta: ) def create(self, validated_data): - # Create new ImageGallery instance + # Create new PollVote instance instance = PollVote() # Then save as usual @@ -736,6 +736,7 @@ def create(self, validated_data): def update(self, instance, validated_data): # Get the proper Poll Answer answer_id = validated_data.get('answer_id', False) + print('answer_id: ', answer_id) try: answer = PollAnswer.objects.get(id=answer_id) poll_id = answer.poll.id diff --git a/dispatch/api/views.py b/dispatch/api/views.py index 41d4ea919..a44fffd73 100644 --- a/dispatch/api/views.py +++ b/dispatch/api/views.py @@ -27,6 +27,9 @@ from dispatch.theme import ThemeManager from dispatch.theme.exceptions import ZoneNotFound, TemplateNotFound +import json +import pdb + class SectionViewSet(DispatchModelViewSet): """Viewset for Section model views.""" model = Section @@ -269,12 +272,20 @@ class PollVoteViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): serializer_class = PollVoteSerializer def create(self, request): - vote_id = request.COOKIES.get('vote_id', None) - print('cookie', vote_id) - if vote_id is not None: - vote = PollVote.objects.get(id=vote_id) - vote.delete() - return super(PollVoteViewSet, self).create(request) + print(request.data) + print(request.data.keys()) + if 'vote_id' in request.data.keys(): + vote_id = request.data['vote_id'] + if vote_id is not None: + vote = PollVote.objects.get(id=vote_id) + vote.delete() + else: + print('vote with id: ' + str(vote_id) + ' does not exist') + + return super(PollVoteViewSet, self).create(request) + else: + print('vote_id not found') + return super(PollVoteViewSet, self).create(request) class TemplateViewSet(viewsets.GenericViewSet): diff --git a/dispatch/static/manager/src/js/components/PollEditor/Poll.js b/dispatch/static/manager/src/js/components/PollEditor/Poll.js index fd46ee12f..72e550b73 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/Poll.js +++ b/dispatch/static/manager/src/js/components/PollEditor/Poll.js @@ -6,6 +6,12 @@ require('../../../styles/components/poll.scss') const COLOR_OPACITY = .8 class Poll extends Component { + constructor(props){ + super(props) + this.state = { + showResults: true, + } + } componentDidMount() { this.update() @@ -14,10 +20,17 @@ class Poll extends Component { update() { let answers = [] let votes = [] - for(let answer of this.props.answers){ - answers.push(answer['name']) - votes.push(answer['vote_count']) + let pollQuestion = this.props.question ? this.props.question : 'Poll Question' + if(this.props.answers){ + for(let answer of this.props.answers){ + answers.push(answer['name']) + votes.push(answer['vote_count']) + } + } else { + answers.push('First answer') + answers.push('Second answer') } + let temp = votes.filter((item) => {return item === 0}) let noVotes = false if(temp.length === votes.length){ @@ -26,21 +39,21 @@ class Poll extends Component { votes[1] = 1 noVotes = true } - + console.log(answers, votes, noVotes) return { answers: answers, votes: votes, loading: false, - pollQuestion: this.props.question, + pollQuestion: pollQuestion, noVotes: noVotes, } } getPollResult(index, votes) { - if(this.props.showResults){ + if(this.state.showResults){ let width = 0 let total = votes.reduce((acc, val) => { return acc + val }) - + console.log(total) if(total !== 0){ width = String((100*votes[index]/total).toFixed(0)) + '%' } diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index b734e58d6..9e8e0b3fc 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -162,6 +162,7 @@ export default class PollForm extends React.Component { } render() { + console.log(this.props) return (
diff --git a/dispatch/static/manager/src/js/components/PollEditor/index.js b/dispatch/static/manager/src/js/components/PollEditor/index.js index c71e8b69c..384e5052b 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/index.js +++ b/dispatch/static/manager/src/js/components/PollEditor/index.js @@ -36,6 +36,7 @@ const mapDispatchToProps = (dispatch) => { saveListItem: (token, pollId, data) => { dispatch(pollsActions.save(token, pollId, prepareJSONData(data))) }, + // grapefruit createListItem: (token, data) => { dispatch(pollsActions.create(token, prepareJSONData(data), AFTER_DELETE)) }, From f26d8b78d262b691364ba160907fd2e7b819a1be Mon Sep 17 00:00:00 2001 From: JamieRL Date: Thu, 31 May 2018 11:44:05 -0700 Subject: [PATCH 25/55] list polls now only returns polls who are not hiding results if you are unauthenticated, retrieve removes result fields if you are unauthenticated --- dispatch/api/serializers.py | 2 -- dispatch/api/views.py | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index 406841af0..fe22a7984 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -767,8 +767,6 @@ class Meta: 'poll_id' ) - def get_vote_count(self, obj): - return obj.votes.count() class PollSerializer(DispatchModelSerializer): """Serializes the Poll model.""" diff --git a/dispatch/api/views.py b/dispatch/api/views.py index 6826fe5e7..ff934d195 100644 --- a/dispatch/api/views.py +++ b/dispatch/api/views.py @@ -259,8 +259,34 @@ def get_queryset(self): q = self.request.query_params.get('q', None) if q is not None: queryset = queryset.filter(Q(name__icontains=q) | Q(question__icontains=q) ) + + if not self.request.user.is_authenticated(): + queryset = queryset.filter(show_results=True) + return queryset + def retrieve(self, request, pk=None): + queryset = Poll.objects.all() + + poll = get_object_or_404(queryset, pk=pk) + serializer = PollSerializer(poll) + + serialized_data = serializer.data + if not request.user.is_authenticated(): + if serializer.data['show_results'] is False: + serialized_data = self.hide_results(serializer.data) + + return Response(serialized_data) + + def hide_results(self, serialized_data): + serialized_data['total_votes'] = 0 + answers = [] + for answer in serialized_data['answers']: + answers.append({'id': answer['id'], 'name': answer['name']}) + serialized_data['answers'] = answers + return serialized_data + + class PollVoteViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): """Viewset for the PollVote Model""" permission_classes = (AllowAny,) From 6f75502d4f0d72edb4f70d20a6e157631ed581ac Mon Sep 17 00:00:00 2001 From: JamieRL Date: Thu, 31 May 2018 12:18:55 -0700 Subject: [PATCH 26/55] remove votes field from pollanswer serializer, change vote ids to be uuids so that they are not easily guessable --- dispatch/api/serializers.py | 3 --- dispatch/api/views.py | 10 ++++------ dispatch/migrations/0011_polls.py | 5 +++-- dispatch/modules/content/models.py | 1 + 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index da34a51c1..69f144e37 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -736,7 +736,6 @@ def create(self, validated_data): def update(self, instance, validated_data): # Get the proper Poll Answer answer_id = validated_data.get('answer_id', False) - print('answer_id: ', answer_id) try: answer = PollAnswer.objects.get(id=answer_id) poll_id = answer.poll.id @@ -754,7 +753,6 @@ def update(self, instance, validated_data): class PollAnswerSerializer(DispatchModelSerializer): """Serializes the PollAnswer model""" - votes = PollVoteSerializer(many=True, required=False, read_only=True) poll_id = serializers.IntegerField(write_only=True) vote_count = serializers.IntegerField(source='get_votes', read_only=True) @@ -763,7 +761,6 @@ class Meta: fields = ( 'id', 'name', - 'votes', 'vote_count', 'poll_id' ) diff --git a/dispatch/api/views.py b/dispatch/api/views.py index cc3dcc942..a3015a6ed 100644 --- a/dispatch/api/views.py +++ b/dispatch/api/views.py @@ -298,16 +298,14 @@ class PollVoteViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): serializer_class = PollVoteSerializer def create(self, request): - print(request.data) - print(request.data.keys()) if 'vote_id' in request.data.keys(): vote_id = request.data['vote_id'] - if vote_id is not None: + try: vote = PollVote.objects.get(id=vote_id) vote.delete() - else: - print('vote with id: ' + str(vote_id) + ' does not exist') - + except PollVote.DoesNotExist: + pass + return super(PollVoteViewSet, self).create(request) else: print('vote_id not found') diff --git a/dispatch/migrations/0011_polls.py b/dispatch/migrations/0011_polls.py index 94a8e139b..04879c3c1 100644 --- a/dispatch/migrations/0011_polls.py +++ b/dispatch/migrations/0011_polls.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-05-29 16:55 +# Generated by Django 1.11 on 2018-05-31 18:54 from __future__ import unicode_literals from django.db import migrations, models import django.db.models.deletion +import uuid class Migration(migrations.Migration): @@ -34,7 +35,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='PollVote', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('answer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='dispatch.PollAnswer')), ], ), diff --git a/dispatch/modules/content/models.py b/dispatch/modules/content/models.py index 55153880d..1b7f23da8 100644 --- a/dispatch/modules/content/models.py +++ b/dispatch/modules/content/models.py @@ -538,4 +538,5 @@ def get_votes(self): return PollVote.objects.all().filter(answer=self).count() class PollVote(Model): + id = UUIDField(default=uuid.uuid4, primary_key=True) answer = ForeignKey(PollAnswer, related_name='votes', on_delete=CASCADE) From 7311aa80e2eab3a0268a6754fde27afdb935bf05 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Thu, 31 May 2018 17:00:46 -0700 Subject: [PATCH 27/55] fix backend to only show results if user is authenticated or show results is true, removed unused methods from poll form --- dispatch/api/serializers.py | 27 ++++++++++++++++--- dispatch/api/views.py | 27 ------------------- .../src/js/components/PollEditor/PollForm.js | 10 ------- .../src/js/components/PollEditor/index.js | 1 - 4 files changed, 24 insertions(+), 41 deletions(-) diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index 69f144e37..2c514ffc3 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -754,7 +754,7 @@ def update(self, instance, validated_data): class PollAnswerSerializer(DispatchModelSerializer): """Serializes the PollAnswer model""" poll_id = serializers.IntegerField(write_only=True) - vote_count = serializers.IntegerField(source='get_votes', read_only=True) + vote_count = serializers.SerializerMethodField() class Meta: model = PollAnswer @@ -765,15 +765,23 @@ class Meta: 'poll_id' ) + def get_vote_count(self, obj): + vote_count = 0 + poll = Poll.objects.get(id=obj.poll_id) + if self.is_authenticated(): + vote_count = obj.get_votes() + if poll.show_results is True: + vote_count = obj.get_votes() + return vote_count class PollSerializer(DispatchModelSerializer): """Serializes the Poll model.""" - answers = PollAnswerSerializer(many=True, read_only=True, required=False) + answers = serializers.SerializerMethodField() answers_json = JSONField( required=False, write_only=True ) - total_votes = serializers.IntegerField(source='get_total_votes', read_only=True) + total_votes = serializers.SerializerMethodField() question = serializers.CharField(required=True) name = serializers.CharField(required=True) @@ -790,6 +798,19 @@ class Meta: 'total_votes' ) + def get_total_votes(self,obj): + total_votes = 0 + if self.is_authenticated(): + total_votes = obj.get_total_votes() + if obj.show_results is True: + total_votes = obj.get_total_votes() + return total_votes + + def get_answers(self, obj): + answers = PollAnswer.objects.all().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() diff --git a/dispatch/api/views.py b/dispatch/api/views.py index a3015a6ed..5474257da 100644 --- a/dispatch/api/views.py +++ b/dispatch/api/views.py @@ -262,34 +262,8 @@ def get_queryset(self): q = self.request.query_params.get('q', None) if q is not None: queryset = queryset.filter(Q(name__icontains=q) | Q(question__icontains=q) ) - - if not self.request.user.is_authenticated(): - queryset = queryset.filter(show_results=True) - return queryset - def retrieve(self, request, pk=None): - queryset = Poll.objects.all() - - poll = get_object_or_404(queryset, pk=pk) - serializer = PollSerializer(poll) - - serialized_data = serializer.data - if not request.user.is_authenticated(): - if serializer.data['show_results'] is False: - serialized_data = self.hide_results(serializer.data) - - return Response(serialized_data) - - def hide_results(self, serialized_data): - serialized_data['total_votes'] = 0 - answers = [] - for answer in serialized_data['answers']: - answers.append({'id': answer['id'], 'name': answer['name']}) - serialized_data['answers'] = answers - return serialized_data - - class PollVoteViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): """Viewset for the PollVote Model""" permission_classes = (AllowAny,) @@ -308,7 +282,6 @@ def create(self, request): return super(PollVoteViewSet, self).create(request) else: - print('vote_id not found') return super(PollVoteViewSet, self).create(request) diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index 9e8e0b3fc..4514f9d65 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -66,14 +66,6 @@ export default class PollForm extends React.Component { this.props.update('answers', answers) } - openPoll() { - this.props.update('is_open', true) - } - - closePoll() { - this.props.update('is_open', false) - } - renderAnswers() { let answers = this.state.answers.map( (answer, index) => { @@ -137,7 +129,6 @@ export default class PollForm extends React.Component { ) } - renderOptions() { const OPTIONS = [ [true, 'Show results'], @@ -162,7 +153,6 @@ export default class PollForm extends React.Component { } render() { - console.log(this.props) return (
diff --git a/dispatch/static/manager/src/js/components/PollEditor/index.js b/dispatch/static/manager/src/js/components/PollEditor/index.js index 384e5052b..c71e8b69c 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/index.js +++ b/dispatch/static/manager/src/js/components/PollEditor/index.js @@ -36,7 +36,6 @@ const mapDispatchToProps = (dispatch) => { saveListItem: (token, pollId, data) => { dispatch(pollsActions.save(token, pollId, prepareJSONData(data))) }, - // grapefruit createListItem: (token, data) => { dispatch(pollsActions.create(token, prepareJSONData(data), AFTER_DELETE)) }, From a7c1a4186a699e1b13d536b2f959e0c2dfaf597a Mon Sep 17 00:00:00 2001 From: rowansdabomb Date: Thu, 31 May 2018 17:03:14 -0700 Subject: [PATCH 28/55] formating --- dispatch/static/manager/src/js/components/PollEditor/Poll.js | 4 ++-- .../static/manager/src/js/components/PollEditor/PollForm.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/dispatch/static/manager/src/js/components/PollEditor/Poll.js b/dispatch/static/manager/src/js/components/PollEditor/Poll.js index 72e550b73..bec542cd7 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/Poll.js +++ b/dispatch/static/manager/src/js/components/PollEditor/Poll.js @@ -39,7 +39,7 @@ class Poll extends Component { votes[1] = 1 noVotes = true } - console.log(answers, votes, noVotes) + return { answers: answers, votes: votes, @@ -53,7 +53,7 @@ class Poll extends Component { if(this.state.showResults){ let width = 0 let total = votes.reduce((acc, val) => { return acc + val }) - console.log(total) + if(total !== 0){ width = String((100*votes[index]/total).toFixed(0)) + '%' } diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index 9e8e0b3fc..b734e58d6 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -162,7 +162,6 @@ export default class PollForm extends React.Component { } render() { - console.log(this.props) return (
From 4318b023b460bd2f518e144a8e02d7d73b1b4e04 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Fri, 1 Jun 2018 09:50:40 -0700 Subject: [PATCH 29/55] add empty test signatures --- dispatch/tests/test_api_polls.py | 70 ++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 dispatch/tests/test_api_polls.py diff --git a/dispatch/tests/test_api_polls.py b/dispatch/tests/test_api_polls.py new file mode 100644 index 000000000..138a0bc37 --- /dev/null +++ b/dispatch/tests/test_api_polls.py @@ -0,0 +1,70 @@ +from os.path import join +from rest_framework import status + +from django.core.urlresolvers import reverse +from django.conf import settings + +from dispatch.tests.cases import DispatchAPITestCase, DispatchMediaTestMixin +from dispatch.tests.helpers import DispatchTestHelpers +from dispatch.models import Poll, PollAnswer, PollVote + + +class PollsTests(DispatchAPITestCase): + """Class to test the poll API methods""" + + def test_poll_creation(self): + """Test simple poll creation, checks the response and database""" + + pass + + def test_poll_update(self): + """Test updating a poll works, check the response and database""" + + pass + + def test_unauthorized_poll_creation(self): + """Create poll should fail with unauthenticated request""" + + pass + + def test_unauthorized_poll_update(self): + """Updating a poll should fail with unauthenticated request""" + + pass + + def test_delete_poll(self): + """Simple poll deletion test and throw error if trying to delete item + does not exist""" + + pass + + def test_unauthorized_poll_deletion(self): + """Unauthorized deletion of a poll isn't allowed""" + + pass + + def test_hide_poll_results(self): + """Unauthorized request should not be able to see results on polls + where show results is false""" + + pass + + def test_poll_search(self): + """Should be able to search poll by name and question""" + + pass + + def test_poll_vote(self): + """Any user should be able to vote in a poll""" + + pass + + def test_poll_change_vote(self): + """A user should be able to change their vote""" + + pass + + def test_poll_closed_vote(self): + """A user should not be able to vote in a closed poll""" + + pass From e6f665f691b7bb04ffd9cf75bee10e8ce66c1775 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Fri, 1 Jun 2018 12:47:19 -0700 Subject: [PATCH 30/55] fixed save_answers to actually update the answer name --- dispatch/modules/content/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dispatch/modules/content/models.py b/dispatch/modules/content/models.py index 1b7f23da8..be2ec8878 100644 --- a/dispatch/modules/content/models.py +++ b/dispatch/modules/content/models.py @@ -515,6 +515,7 @@ def save_answers(self, answers, is_new): for answer in answers: try: 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() From cf6ffc49d4fa46aa0c4e779b394314695a733135 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Fri, 1 Jun 2018 12:48:00 -0700 Subject: [PATCH 31/55] Added api tests for polls --- dispatch/tests/helpers.py | 16 +++ dispatch/tests/test_api_polls.py | 237 +++++++++++++++++++++++++++++-- 2 files changed, 239 insertions(+), 14 deletions(-) diff --git a/dispatch/tests/helpers.py b/dispatch/tests/helpers.py index ff717b71e..a48261df9 100644 --- a/dispatch/tests/helpers.py +++ b/dispatch/tests/helpers.py @@ -208,3 +208,19 @@ def create_video(cls, client, title='testVideo', url='testVideoURL'): url = reverse('api-videos-list') return client.post(url, data, format='json') + + @classmethod + def create_poll(cls, client, name='test name', question='test question', answers=[{'id':1,'name':'answer1'},{'id':2,'name':'answer2'}], is_open=True, show_results=True): + """Create a dummy poll instance""" + + data = { + 'name': name, + 'question': question, + 'answers_json': answers, + 'is_open': is_open, + 'show_results': show_results + } + + url = reverse('api-polls-list') + + return client.post(url, data, format='json') diff --git a/dispatch/tests/test_api_polls.py b/dispatch/tests/test_api_polls.py index 138a0bc37..d1f6a8abe 100644 --- a/dispatch/tests/test_api_polls.py +++ b/dispatch/tests/test_api_polls.py @@ -1,10 +1,9 @@ -from os.path import join +from collections import OrderedDict from rest_framework import status from django.core.urlresolvers import reverse -from django.conf import settings -from dispatch.tests.cases import DispatchAPITestCase, DispatchMediaTestMixin +from dispatch.tests.cases import DispatchAPITestCase from dispatch.tests.helpers import DispatchTestHelpers from dispatch.models import Poll, PollAnswer, PollVote @@ -15,56 +14,266 @@ class PollsTests(DispatchAPITestCase): def test_poll_creation(self): """Test simple poll creation, checks the response and database""" - pass + response = DispatchTestHelpers.create_poll(self.client) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + answers = [ OrderedDict([('id',1), ('name','answer1'), ('vote_count',0)]), OrderedDict([('id',2), ('name','answer2'), ('vote_count',0)]) ] + # Check data + self.assertEqual(response.data['name'], 'test name') + self.assertEqual(response.data['question'], 'test question') + self.assertEqual(response.data['is_open'], True) + self.assertEqual(response.data['show_results'], True) + self.assertEqual(response.data['answers'], answers) + + poll = Poll.objects.get(pk=response.data['id']) + self.assertEqual(poll.name, 'test name') + self.assertEqual(poll.question, 'test question') + self.assertTrue(poll.is_open) + self.assertTrue(poll.show_results) def test_poll_update(self): """Test updating a poll works, check the response and database""" - pass + # Create original poll + response = DispatchTestHelpers.create_poll(self.client) + + # Update this poll + url = reverse('api-polls-detail', args=[response.data['id']]) + updated_answers = [{'id':1,'name':'updated answer'},{'id':2,'name':'answer2'}] + data = { + 'name': 'updated name', + 'answers_json': updated_answers + } + answers = [ OrderedDict([('id',1), ('name','updated answer'), ('vote_count',0)]), OrderedDict([('id',2), ('name','answer2'), ('vote_count',0)]) ] + response = self.client.patch(url, data, format='json') + + # Check data + self.assertEqual(response.data['name'], 'updated name') + self.assertEqual(response.data['question'], 'test question') + self.assertEqual(response.data['is_open'], True) + self.assertEqual(response.data['show_results'], True) + self.assertEqual(response.data['answers'], answers) + + poll = Poll.objects.get(pk=response.data['id']) + self.assertEqual(poll.name, 'updated name') + self.assertEqual(poll.question, 'test question') + self.assertTrue(poll.is_open) + self.assertTrue(poll.show_results) def test_unauthorized_poll_creation(self): """Create poll should fail with unauthenticated request""" - pass + # Clear authentication credentials + self.client.credentials() + + response = DispatchTestHelpers.create_poll(self.client) + + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_unauthorized_poll_update(self): """Updating a poll should fail with unauthenticated request""" - pass + # Create original poll + response = DispatchTestHelpers.create_poll(self.client) + + # Clear authentication credentials + self.client.credentials() + + # Attempt to update this poll + url = reverse('api-polls-detail', args=[response.data['id']]) + data = { + 'name': 'updated name' + } + + response = self.client.patch(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_delete_poll(self): """Simple poll deletion test and throw error if trying to delete item does not exist""" - pass + # Create original poll + response = DispatchTestHelpers.create_poll(self.client) + poll_id = response.data['id'] + url = reverse('api-polls-detail', args=[poll_id]) + + # Successful deletion should return 204 + response = self.client.delete(url, format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertFalse(Poll.objects.filter(id=poll_id).exists()) + self.assertFalse(PollAnswer.objects.filter(poll_id=poll_id).exists()) + + # Can't delete an person that has already been deleted + response = self.client.delete(url, format='json') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_unauthorized_poll_deletion(self): """Unauthorized deletion of a poll isn't allowed""" - pass + response = DispatchTestHelpers.create_poll(self.client) + + # Generate detail URL + url = reverse('api-polls-detail', args=[response.data['id']]) + + # Clear authentication credentials + self.client.credentials() + + # Attempt to delete the poll + response = self.client.delete(url, format='json') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_hide_poll_results(self): """Unauthorized request should not be able to see results on polls where show results is false""" - pass + # Create a poll to vote in + response = DispatchTestHelpers.create_poll(self.client, show_results=False) + + poll = Poll.objects.get(pk=response.data['id']) + answer1 = PollAnswer.objects.filter(poll_id=poll.id).first() + answer2 = PollAnswer.objects.filter(poll_id=poll.id).last() + # Clear credentials + self.client.credentials() + url = reverse('api-votes-list') + + data = { + 'answer_id': answer1.id + } + + # Vote in the poll + response = self.client.post(url, data, format='json') + # Confirm the vote was successful + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + data = { + 'answer_id': answer2.id + } + + # Vote in the poll + response = self.client.post(url, data, format='json') + # Confirm the vote was successful + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + url = reverse('api-polls-detail', args=[poll.id]) + + response = self.client.get(url, format='json') + self.assertEqual(response.data['total_votes'], 0) + self.assertEqual(response.data['answers'][0]['vote_count'], 0) + self.assertEqual(response.data['answers'][1]['vote_count'], 0) + def test_poll_search(self): """Should be able to search poll by name and question""" - pass + poll_1 = DispatchTestHelpers.create_poll(self.client, name='Poll 1', question='question 1') + poll_2 = DispatchTestHelpers.create_poll(self.client, name='Poll 1 and 2', question='question 1 and 2') + poll_3 = DispatchTestHelpers.create_poll(self.client, name='Poll 3', question='question 3') + + url = '%s?q=%s' % (reverse('api-polls-list'), 'Poll 1') + response = self.client.get(url, format='json') + + self.assertEqual(response.data['results'][0]['name'], 'Poll 1') + self.assertEqual(response.data['results'][1]['name'], 'Poll 1 and 2') + self.assertEqual(response.data['count'], 2) def test_poll_vote(self): """Any user should be able to vote in a poll""" - pass + # Create a poll to vote in + response = DispatchTestHelpers.create_poll(self.client, show_results=False) + + poll_id = response.data['id'] + answer = PollAnswer.objects.filter(poll_id=poll_id).first() + + # Clear credentials + url = reverse('api-votes-list') + + data = { + 'answer_id': answer.id + } + + response = self.client.post(url, data, format='json') + + # Check that the vote was successful + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + url = reverse('api-polls-detail', args=[poll_id]) + + response = self.client.get(url) + + # Check that the results are correct + self.assertEqual(response.data['total_votes'], 1) + self.assertEqual(response.data['answers'][0]['vote_count'], 1) def test_poll_change_vote(self): """A user should be able to change their vote""" - pass + # Create a poll to vote in + response = DispatchTestHelpers.create_poll(self.client) + + poll_id = response.data['id'] + answer_1 = PollAnswer.objects.filter(poll_id=poll_id).first() + answer_2 = PollAnswer.objects.filter(poll_id=poll_id).last() + + url = reverse('api-votes-list') + + data = { + 'answer_id': answer_1.id + } + + response = self.client.post(url, data, format='json') + vote_id = response.data['id'] + + # Check that the vote was successful + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + data = { + 'answer_id': answer_2.id, + 'vote_id': vote_id + } + + response = self.client.post(url, data, format='json') + + # Check that the vote change was successful + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertNotEqual(response.data['id'], vote_id) + + # Get the poll + url = reverse('api-polls-detail', args=[poll_id]) + + response = self.client.get(url) + + # Check that the results are correct + self.assertEqual(response.data['total_votes'], 1) + self.assertEqual(response.data['answers'][0]['vote_count'], 0) + self.assertEqual(response.data['answers'][1]['vote_count'], 1) def test_poll_closed_vote(self): """A user should not be able to vote in a closed poll""" - pass + # Create a poll to vote in + response = DispatchTestHelpers.create_poll(self.client, is_open=False) + + poll_id = response.data['id'] + answer = PollAnswer.objects.filter(poll_id=poll_id).first() + + url = reverse('api-votes-list') + + data = { + 'answer_id': answer.id + } + response = self.client.post(url, data, format='json') + vote_id = response.data['id'] + + self.assertFalse(PollVote.objects.filter(id=vote_id).exists()) + + # Get the poll + url = reverse('api-polls-detail', args=[poll_id]) + + response = self.client.get(url) + + # Check that the results are correct + self.assertEqual(response.data['total_votes'], 0) + self.assertEqual(response.data['answers'][0]['vote_count'], 0) + self.assertEqual(response.data['answers'][1]['vote_count'], 0) From 955a441f4b808e4b4bd43a97f2579e07b2f6aa74 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Fri, 1 Jun 2018 13:37:58 -0700 Subject: [PATCH 32/55] remove unused poll embed --- .../ArticleEditor/ArticleContentEditor.js | 6 +- .../dispatch-editor/embeds/PollEmbed.js | 61 ------------------- .../js/vendor/dispatch-editor/embeds/index.js | 4 +- 3 files changed, 3 insertions(+), 68 deletions(-) delete mode 100644 dispatch/static/manager/src/js/vendor/dispatch-editor/embeds/PollEmbed.js diff --git a/dispatch/static/manager/src/js/components/ArticleEditor/ArticleContentEditor.js b/dispatch/static/manager/src/js/components/ArticleEditor/ArticleContentEditor.js index ebc7e9025..6d6e346cf 100644 --- a/dispatch/static/manager/src/js/components/ArticleEditor/ArticleContentEditor.js +++ b/dispatch/static/manager/src/js/components/ArticleEditor/ArticleContentEditor.js @@ -9,8 +9,7 @@ import { PullQuoteEmbed, GalleryEmbed, CodeEmbed, - WidgetEmbed, - PollEmbed + WidgetEmbed } from '../../vendor/dispatch-editor' const embeds = [ @@ -19,8 +18,7 @@ const embeds = [ CodeEmbed, PullQuoteEmbed, GalleryEmbed, - WidgetEmbed, - PollEmbed + WidgetEmbed ] export default class ArticleContentEditor extends React.Component { diff --git a/dispatch/static/manager/src/js/vendor/dispatch-editor/embeds/PollEmbed.js b/dispatch/static/manager/src/js/vendor/dispatch-editor/embeds/PollEmbed.js deleted file mode 100644 index 82fc56b8a..000000000 --- a/dispatch/static/manager/src/js/vendor/dispatch-editor/embeds/PollEmbed.js +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react' - -import { FormInput, TextInput } from '../../../components/inputs' - -function PollEmbedComponent(props) { - return ( -
-
- - props.updateField('question', e.target.value)} /> - - - props.updateField('choice1', e.target.value)} /> - - - props.updateField('choice2', e.target.value)} /> - - - props.updateField('choice3', e.target.value)} /> - - - props.updateField('choice4', e.target.value)} /> - - - props.updateField('choice5', e.target.value)} /> - -
-
- ) -} - -export default { - type: 'poll', - component: PollEmbedComponent, - defaultData: { - question: '', - choice1: '', - choice2: '', - choice3: '', - choice4: '', - choice5: '' - } -} diff --git a/dispatch/static/manager/src/js/vendor/dispatch-editor/embeds/index.js b/dispatch/static/manager/src/js/vendor/dispatch-editor/embeds/index.js index b7585e9cd..d4db1b36a 100644 --- a/dispatch/static/manager/src/js/vendor/dispatch-editor/embeds/index.js +++ b/dispatch/static/manager/src/js/vendor/dispatch-editor/embeds/index.js @@ -4,7 +4,6 @@ import CodeEmbed from './CodeEmbed' import PullQuoteEmbed from './PullQuoteEmbed' import GalleryEmbed from './GalleryEmbed' import WidgetEmbed from './WidgetEmbed' -import PollEmbed from './PollEmbed' export { ImageEmbed, @@ -12,6 +11,5 @@ export { PullQuoteEmbed, CodeEmbed, GalleryEmbed, - WidgetEmbed, - PollEmbed + WidgetEmbed } From e3b1efc79a8a906cad6575a211777bdad60a6a91 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Fri, 1 Jun 2018 13:48:12 -0700 Subject: [PATCH 33/55] resolve migration conflicts --- dispatch/migrations/{0011_polls.py => 0012_polls.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename dispatch/migrations/{0011_polls.py => 0012_polls.py} (96%) diff --git a/dispatch/migrations/0011_polls.py b/dispatch/migrations/0012_polls.py similarity index 96% rename from dispatch/migrations/0011_polls.py rename to dispatch/migrations/0012_polls.py index 04879c3c1..0c99bed33 100644 --- a/dispatch/migrations/0011_polls.py +++ b/dispatch/migrations/0012_polls.py @@ -10,7 +10,7 @@ class Migration(migrations.Migration): dependencies = [ - ('dispatch', '0010_image_tags'), + ('dispatch', '0011_article_featured_video'), ] operations = [ From 1543647708a4c450b9f2aeacae8300b5313f1920 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Fri, 1 Jun 2018 14:24:12 -0700 Subject: [PATCH 34/55] change default ids in poll form constructor to not coincide with the database ids --- dispatch/modules/content/models.py | 2 +- .../static/manager/src/js/components/PollEditor/PollForm.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dispatch/modules/content/models.py b/dispatch/modules/content/models.py index a3091c1f6..1c2146215 100644 --- a/dispatch/modules/content/models.py +++ b/dispatch/modules/content/models.py @@ -222,7 +222,7 @@ def save_featured_image(self, data): if data is None: if attachment: attachment.delete() - + self.featured_image = None return diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index 4514f9d65..a2a1f3b92 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -11,17 +11,18 @@ export default class PollForm extends React.Component { constructor(props) { super(props) + //Ids are set to be negative to avoid clashing with the database ids if(props.listItem.id === 'new') { this.state = { answers : [ { - 'id': 0, + 'id': -1, 'name': '', 'votes': [], 'vote_count': 0 }, { - 'id': 1, + 'id': -2, 'name': '', 'votes': [], 'vote_count': 0 From 23f087c97c1a97c7e56cbaa027d61d2bda216157 Mon Sep 17 00:00:00 2001 From: rowansdabomb Date: Mon, 4 Jun 2018 09:13:47 -0700 Subject: [PATCH 35/55] remove componentdidupate from poll.js --- .../manager/src/js/components/PollEditor/Poll.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/dispatch/static/manager/src/js/components/PollEditor/Poll.js b/dispatch/static/manager/src/js/components/PollEditor/Poll.js index bec542cd7..b1d830c2a 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/Poll.js +++ b/dispatch/static/manager/src/js/components/PollEditor/Poll.js @@ -13,16 +13,12 @@ class Poll extends Component { } } - componentDidMount() { - this.update() - } - update() { let answers = [] let votes = [] let pollQuestion = this.props.question ? this.props.question : 'Poll Question' - if(this.props.answers){ - for(let answer of this.props.answers){ + if(this.props.answers) { + for(let answer of this.props.answers) { answers.push(answer['name']) votes.push(answer['vote_count']) } @@ -33,7 +29,7 @@ class Poll extends Component { let temp = votes.filter((item) => {return item === 0}) let noVotes = false - if(temp.length === votes.length){ + if(temp.length === votes.length) { //no votes yet, populate with dummy data for better poll visualization votes[0] = 2 votes[1] = 1 @@ -50,11 +46,11 @@ class Poll extends Component { } getPollResult(index, votes) { - if(this.state.showResults){ + if(this.state.showResults) { let width = 0 let total = votes.reduce((acc, val) => { return acc + val }) - if(total !== 0){ + if(total !== 0) { width = String((100*votes[index]/total).toFixed(0)) + '%' } From a36394004dd0a91a9ed4f9b8a598a587d5632261 Mon Sep 17 00:00:00 2001 From: rowansdabomb Date: Mon, 4 Jun 2018 09:32:18 -0700 Subject: [PATCH 36/55] spacing changes --- dispatch/api/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index eb11648ad..702cedd7b 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -748,7 +748,9 @@ def update(self, instance, validated_data): class PollVoteSerializer(DispatchModelSerializer): """Serializes the PollVote model""" answer_id = serializers.IntegerField(write_only=True) + class Meta: + model = PollVote fields = ( 'id', From b0bac56a06af19c1e1a23ec27bd2151cb17ded1b Mon Sep 17 00:00:00 2001 From: JamieRL Date: Mon, 4 Jun 2018 11:57:26 -0700 Subject: [PATCH 37/55] refactor poll vote and changes as per Rowans reviwe --- dispatch/api/serializers.py | 33 ++++----------- dispatch/api/urls.py | 1 - dispatch/api/views.py | 40 +++++++++---------- dispatch/modules/content/models.py | 1 - .../manager/src/js/actions/PollsActions.js | 2 +- .../src/js/components/PollEditor/PollForm.js | 4 +- .../src/styles/components/poll_form.scss | 2 +- dispatch/tests/test_api_polls.py | 27 +++++++------ 8 files changed, 45 insertions(+), 65 deletions(-) diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index eb11648ad..b6f31d12e 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -747,7 +747,9 @@ def update(self, instance, validated_data): class PollVoteSerializer(DispatchModelSerializer): """Serializes the PollVote model""" + answer_id = serializers.IntegerField(write_only=True) + class Meta: model = PollVote fields = ( @@ -755,33 +757,9 @@ class Meta: 'answer_id', ) - def create(self, validated_data): - # Create new PollVote instance - instance = PollVote() - - # Then save as usual - return self.update(instance, validated_data) - - def update(self, instance, validated_data): - # Get the proper Poll Answer - answer_id = validated_data.get('answer_id', False) - try: - answer = PollAnswer.objects.get(id=answer_id) - poll_id = answer.poll.id - poll = Poll.objects.get(id=poll_id) - except PollAnswer.DoesNotExist: - answer = None - poll = None - # Set the vote's answer - if poll is not None and poll.is_open: - if answer is not None: - instance.answer = answer - instance.save() - - return instance - class PollAnswerSerializer(DispatchModelSerializer): """Serializes the PollAnswer model""" + poll_id = serializers.IntegerField(write_only=True) vote_count = serializers.SerializerMethodField() @@ -794,6 +772,7 @@ class Meta: 'poll_id' ) +#TODO: verify vote count is working def get_vote_count(self, obj): vote_count = 0 poll = Poll.objects.get(id=obj.poll_id) @@ -805,6 +784,7 @@ def get_vote_count(self, obj): class PollSerializer(DispatchModelSerializer): """Serializes the Poll model.""" + answers = serializers.SerializerMethodField() answers_json = JSONField( required=False, @@ -814,6 +794,7 @@ class PollSerializer(DispatchModelSerializer): question = serializers.CharField(required=True) name = serializers.CharField(required=True) +#TODO: Look at removing answers_json class Meta: model = Poll fields = ( @@ -857,7 +838,7 @@ def update(self, instance, validated_data, is_new=False): # save associations to correct ID instance.save() - answers = validated_data.get('answers_json', False) + answers = validated_data.get('answers_json') if isinstance(answers, list): instance.save_answers(answers, is_new) diff --git a/dispatch/api/urls.py b/dispatch/api/urls.py index ea609b489..442ff9569 100644 --- a/dispatch/api/urls.py +++ b/dispatch/api/urls.py @@ -25,7 +25,6 @@ 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') -router.register(r'vote', views.PollVoteViewSet, base_name='api-votes') dashboard_recent_articles = views.DashboardViewSet.as_view({'get': 'list_recent_articles'}) dashboard_user_actions = views.DashboardViewSet.as_view({'get': 'list_actions'}) diff --git a/dispatch/api/views.py b/dispatch/api/views.py index 5474257da..c05d2e574 100644 --- a/dispatch/api/views.py +++ b/dispatch/api/views.py @@ -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 @@ -14,7 +15,7 @@ from dispatch.models import ( Article, File, Image, ImageAttachment, ImageGallery, Issue, - Page, Author, Person, Section, Tag, Topic, User, Video, Poll, PollVote) + Page, Author, Person, Section, Tag, Topic, User, Video, Poll, PollAnswer, PollVote) from dispatch.api.mixins import DispatchModelViewSet, DispatchPublishableMixin from dispatch.api.serializers import ( @@ -27,9 +28,6 @@ from dispatch.theme import ThemeManager from dispatch.theme.exceptions import ZoneNotFound, TemplateNotFound -import json -import pdb - class SectionViewSet(DispatchModelViewSet): """Viewset for Section model views.""" model = Section @@ -264,26 +262,28 @@ def get_queryset(self): queryset = queryset.filter(Q(name__icontains=q) | Q(question__icontains=q) ) return queryset -class PollVoteViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): - """Viewset for the PollVote Model""" - permission_classes = (AllowAny,) + @detail_route(permission_classes=[AllowAny], methods=['post'],) + def vote(self, request, pk=None): + poll = get_object_or_404(Poll.objects.all(), pk=pk) - model = PollVote - serializer_class = PollVoteSerializer + if not poll.is_open: + return Response({ 'Detail': 'this poll is closed'}, status.HTTP_400_BAD_REQUEST) - def create(self, request): - if 'vote_id' in request.data.keys(): - vote_id = request.data['vote_id'] - try: - vote = PollVote.objects.get(id=vote_id) - vote.delete() - except PollVote.DoesNotExist: - pass + answer = get_object_or_404(PollAnswer.objects.all(), pk=request.data['answer_id']) - return super(PollVoteViewSet, self).create(request) - else: - return super(PollVoteViewSet, self).create(request) + if answer.poll != poll: + return Response({ 'Detail': 'invalid answer for this poll'}) + + serializer = PollVoteSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + + if 'vote_id' in request.data: + vote_id = request.data['vote_id'] + vote = PollVote.objects.filter(id=vote_id).delete() + + return Response(serializer.data) class TemplateViewSet(viewsets.GenericViewSet): """Viewset for Template views""" diff --git a/dispatch/modules/content/models.py b/dispatch/modules/content/models.py index 1c2146215..6f0a2b7de 100644 --- a/dispatch/modules/content/models.py +++ b/dispatch/modules/content/models.py @@ -577,7 +577,6 @@ def delete_old_answers(self, answers): old_answers = old_answers.exclude(id=answer['id']) old_answers.delete() - def get_total_votes(self): return PollVote.objects.all().filter(answer__poll=self).count() diff --git a/dispatch/static/manager/src/js/actions/PollsActions.js b/dispatch/static/manager/src/js/actions/PollsActions.js index f562cdefd..d78f94dbf 100644 --- a/dispatch/static/manager/src/js/actions/PollsActions.js +++ b/dispatch/static/manager/src/js/actions/PollsActions.js @@ -23,7 +23,7 @@ class PollsActions extends ResourceActions { } -export default new PollsActions( +export default new PollsActions ( types.POLLS, DispatchAPI.polls, pollSchema diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index a2a1f3b92..918cf3642 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -57,7 +57,7 @@ export default class PollForm extends React.Component { this.props.update('answers', answers) } - handleUpdateAnswer(id, e) { + handleUpdateAnswer(e, id) { var answers = this.state.answers for(var i = 0; i < answers.length; i++) { if(answers[i].id === id){ @@ -83,7 +83,7 @@ export default class PollForm extends React.Component { placeholder='Answer' value={name || ''} fill={true} - onChange={ e => this.handleUpdateAnswer(answer.id, e) } /> + onChange={ e => this.handleUpdateAnswer(e, answer.id) } /> this.removeAnswer(id)}> diff --git a/dispatch/static/manager/src/styles/components/poll_form.scss b/dispatch/static/manager/src/styles/components/poll_form.scss index 3561038e1..a01c46fea 100644 --- a/dispatch/static/manager/src/styles/components/poll_form.scss +++ b/dispatch/static/manager/src/styles/components/poll_form.scss @@ -21,7 +21,7 @@ width: 9em; } .poll-form.pt-icon-standard { - // Structure + // Structure margin-right: 0.25rem; position: absolute; top: 0; diff --git a/dispatch/tests/test_api_polls.py b/dispatch/tests/test_api_polls.py index d1f6a8abe..2f3e94377 100644 --- a/dispatch/tests/test_api_polls.py +++ b/dispatch/tests/test_api_polls.py @@ -104,7 +104,7 @@ def test_delete_poll(self): self.assertFalse(Poll.objects.filter(id=poll_id).exists()) self.assertFalse(PollAnswer.objects.filter(poll_id=poll_id).exists()) - # Can't delete an person that has already been deleted + # Can't delete an poll that has already been deleted response = self.client.delete(url, format='json') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) @@ -135,7 +135,8 @@ def test_hide_poll_results(self): answer2 = PollAnswer.objects.filter(poll_id=poll.id).last() # Clear credentials self.client.credentials() - url = reverse('api-votes-list') + + url = reverse('api-polls-vote', args=[poll.id]) data = { 'answer_id': answer1.id @@ -144,7 +145,7 @@ def test_hide_poll_results(self): # Vote in the poll response = self.client.post(url, data, format='json') # Confirm the vote was successful - self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.status_code, status.HTTP_200_OK) data = { 'answer_id': answer2.id @@ -153,10 +154,11 @@ def test_hide_poll_results(self): # Vote in the poll response = self.client.post(url, data, format='json') # Confirm the vote was successful - self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.status_code, status.HTTP_200_OK) url = reverse('api-polls-detail', args=[poll.id]) +#TODO if serializer is changed to return none this will have to change response = self.client.get(url, format='json') self.assertEqual(response.data['total_votes'], 0) self.assertEqual(response.data['answers'][0]['vote_count'], 0) @@ -187,7 +189,7 @@ def test_poll_vote(self): answer = PollAnswer.objects.filter(poll_id=poll_id).first() # Clear credentials - url = reverse('api-votes-list') + url = reverse('api-polls-vote', args=[poll_id]) data = { 'answer_id': answer.id @@ -196,7 +198,7 @@ def test_poll_vote(self): response = self.client.post(url, data, format='json') # Check that the vote was successful - self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.status_code, status.HTTP_200_OK) url = reverse('api-polls-detail', args=[poll_id]) @@ -216,7 +218,7 @@ def test_poll_change_vote(self): answer_1 = PollAnswer.objects.filter(poll_id=poll_id).first() answer_2 = PollAnswer.objects.filter(poll_id=poll_id).last() - url = reverse('api-votes-list') + url = reverse('api-polls-vote', args=[poll_id]) data = { 'answer_id': answer_1.id @@ -226,7 +228,7 @@ def test_poll_change_vote(self): vote_id = response.data['id'] # Check that the vote was successful - self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.status_code, status.HTTP_200_OK) data = { 'answer_id': answer_2.id, @@ -236,7 +238,7 @@ def test_poll_change_vote(self): response = self.client.post(url, data, format='json') # Check that the vote change was successful - self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertNotEqual(response.data['id'], vote_id) # Get the poll @@ -249,7 +251,7 @@ def test_poll_change_vote(self): self.assertEqual(response.data['answers'][0]['vote_count'], 0) self.assertEqual(response.data['answers'][1]['vote_count'], 1) - def test_poll_closed_vote(self): + def test_poll_vote_closed(self): """A user should not be able to vote in a closed poll""" # Create a poll to vote in @@ -258,15 +260,14 @@ def test_poll_closed_vote(self): poll_id = response.data['id'] answer = PollAnswer.objects.filter(poll_id=poll_id).first() - url = reverse('api-votes-list') + url = reverse('api-polls-vote', args=[poll_id]) data = { 'answer_id': answer.id } response = self.client.post(url, data, format='json') - vote_id = response.data['id'] - self.assertFalse(PollVote.objects.filter(id=vote_id).exists()) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # Get the poll url = reverse('api-polls-detail', args=[poll_id]) From 47c545eaa417a274bb5995df2dd21acca5317fc5 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Mon, 4 Jun 2018 12:17:04 -0700 Subject: [PATCH 38/55] more changes as per Peter and ben's review, added timestamp field to votes --- dispatch/api/serializers.py | 16 ++++++++-------- dispatch/migrations/0012_polls.py | 3 ++- dispatch/modules/content/models.py | 12 ++++++++---- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index b6f31d12e..439913697 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -776,10 +776,10 @@ class Meta: def get_vote_count(self, obj): vote_count = 0 poll = Poll.objects.get(id=obj.poll_id) - if self.is_authenticated(): - vote_count = obj.get_votes() - if poll.show_results is True: - vote_count = obj.get_votes() + + if self.is_authenticated() or poll.show_results: + vote_count = obj.get_vote_count() + return vote_count class PollSerializer(DispatchModelSerializer): @@ -810,14 +810,14 @@ class Meta: def get_total_votes(self,obj): total_votes = 0 - if self.is_authenticated(): - total_votes = obj.get_total_votes() - if obj.show_results is True: + + 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.all().filter(poll_id=obj.id) + answers = PollAnswer.objects.filter(poll_id=obj.id) serializer = PollAnswerSerializer(answers, many=True, context=self.context) return serializer.data diff --git a/dispatch/migrations/0012_polls.py b/dispatch/migrations/0012_polls.py index 0c99bed33..d6e249912 100644 --- a/dispatch/migrations/0012_polls.py +++ b/dispatch/migrations/0012_polls.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-05-31 18:54 +# Generated by Django 1.11 on 2018-06-04 19:15 from __future__ import unicode_literals from django.db import migrations, models @@ -36,6 +36,7 @@ class Migration(migrations.Migration): 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')), ], ), diff --git a/dispatch/modules/content/models.py b/dispatch/modules/content/models.py index 6f0a2b7de..a0af04adc 100644 --- a/dispatch/modules/content/models.py +++ b/dispatch/modules/content/models.py @@ -7,6 +7,8 @@ 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, @@ -560,8 +562,9 @@ class Poll(Model): is_open = BooleanField(default=True) show_results = BooleanField(default=True) + @transaction.atomic def save_answers(self, answers, is_new): - if is_new is False: + if not is_new: self.delete_old_answers(answers) for answer in answers: try: @@ -578,16 +581,17 @@ def delete_old_answers(self, answers): old_answers.delete() def get_total_votes(self): - return PollVote.objects.all().filter(answer__poll=self).count() + 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_votes(self): + def get_vote_count(self): """Return the number of votes for this answer""" - return PollVote.objects.all().filter(answer=self).count() + 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) From ee88b4573d3fc65efe612a097a3156dc86a2f6ab Mon Sep 17 00:00:00 2001 From: rowansdabomb Date: Mon, 4 Jun 2018 12:49:03 -0700 Subject: [PATCH 39/55] fix initial answer typing for polls in dispatch --- .../manager/src/js/components/PollEditor/Poll.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/dispatch/static/manager/src/js/components/PollEditor/Poll.js b/dispatch/static/manager/src/js/components/PollEditor/Poll.js index b1d830c2a..e873eb79b 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/Poll.js +++ b/dispatch/static/manager/src/js/components/PollEditor/Poll.js @@ -19,12 +19,16 @@ class Poll extends Component { let pollQuestion = this.props.question ? this.props.question : 'Poll Question' if(this.props.answers) { for(let answer of this.props.answers) { - answers.push(answer['name']) + if(answer['name'] !== '') { + answers.push(answer['name']) + } else { + answers.push('Answer') + } votes.push(answer['vote_count']) } } else { - answers.push('First answer') - answers.push('Second answer') + answers.push('Answer') + answers.push('Answer') } let temp = votes.filter((item) => {return item === 0}) From 81150e492cc72d78b4bf068258125267e281c4e4 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Mon, 4 Jun 2018 13:12:57 -0700 Subject: [PATCH 40/55] When re-voting you now just change the votes choice instead of deleting and re-voting, more changes as per Peter and Ben's reviews --- dispatch/api/views.py | 16 ++++++++++++---- dispatch/modules/content/models.py | 7 +++---- dispatch/tests/test_api_polls.py | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/dispatch/api/views.py b/dispatch/api/views.py index c05d2e574..b49105ef0 100644 --- a/dispatch/api/views.py +++ b/dispatch/api/views.py @@ -274,14 +274,22 @@ def vote(self, request, pk=None): if answer.poll != poll: return Response({ 'Detail': 'invalid answer for this poll'}) - serializer = PollVoteSerializer(data=request.data) + if 'vote_id' in request.data: + vote_id = request.data['vote_id'] + try: + vote = PollVote.objects.filter(answer__poll=poll).get(id=vote_id) + serializer = PollVoteSerializer(vote, request.data) + except PollVote.DoesNotExist: + return Response({ 'Detail': 'vote id provided is not for this poll'}, status.HTTP_400_BAD_REQUEST) + else: + serializer = PollVoteSerializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.save() - if 'vote_id' in request.data: - vote_id = request.data['vote_id'] - vote = PollVote.objects.filter(id=vote_id).delete() + # if 'vote_id' in request.data: + # vote_id = request.data['vote_id'] + # vote = PollVote.objects.filter(id=vote_id).delete() return Response(serializer.data) diff --git a/dispatch/modules/content/models.py b/dispatch/modules/content/models.py index a0af04adc..3df009212 100644 --- a/dispatch/modules/content/models.py +++ b/dispatch/modules/content/models.py @@ -575,10 +575,9 @@ def save_answers(self, answers, is_new): answer_obj.save() def delete_old_answers(self, answers): - old_answers = PollAnswer.objects.filter(poll=self) - for answer in answers: - old_answers = old_answers.exclude(id=answer['id']) - old_answers.delete() + PollAnswer.objects.filter(poll=self) \ + .exclude(id__in=[answer['id'] for answer in answers]) \ + .delete() def get_total_votes(self): return PollVote.objects.filter(answer__poll=self).count() diff --git a/dispatch/tests/test_api_polls.py b/dispatch/tests/test_api_polls.py index 2f3e94377..aa5234e51 100644 --- a/dispatch/tests/test_api_polls.py +++ b/dispatch/tests/test_api_polls.py @@ -239,7 +239,7 @@ def test_poll_change_vote(self): # Check that the vote change was successful self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertNotEqual(response.data['id'], vote_id) + self.assertEqual(response.data['id'], vote_id) # Get the poll url = reverse('api-polls-detail', args=[poll_id]) From 1573076ae219932b67fe2edd9c38a971947130ec Mon Sep 17 00:00:00 2001 From: JamieRL Date: Mon, 4 Jun 2018 13:42:21 -0700 Subject: [PATCH 41/55] refactor poll form to not use hacky ids in initial state and generally be more clean. Change model save answers to access id safely --- dispatch/api/serializers.py | 1 - dispatch/modules/content/models.py | 3 +- .../src/js/components/PollEditor/PollForm.js | 49 ++++++++----------- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index 439913697..1d37f5285 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -794,7 +794,6 @@ class PollSerializer(DispatchModelSerializer): question = serializers.CharField(required=True) name = serializers.CharField(required=True) -#TODO: Look at removing answers_json class Meta: model = Poll fields = ( diff --git a/dispatch/modules/content/models.py b/dispatch/modules/content/models.py index 3df009212..127b7431a 100644 --- a/dispatch/modules/content/models.py +++ b/dispatch/modules/content/models.py @@ -568,7 +568,8 @@ def save_answers(self, answers, is_new): self.delete_old_answers(answers) for answer in answers: try: - answer_obj = PollAnswer.objects.get(poll=self, id=answer['id']) + 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']) diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index 918cf3642..7d136c5f9 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -7,28 +7,28 @@ import SelectInput from '../inputs/selects/SelectInput' require('../../../styles/components/poll_form.scss') +const DEFAULT_ANSWERS = { + answers : [ + { + 'name': '', + 'votes': [], + 'vote_count': 0 + }, + { + 'name': '', + 'votes': [], + 'vote_count': 0 + } + ] +} + export default class PollForm extends React.Component { constructor(props) { super(props) - //Ids are set to be negative to avoid clashing with the database ids + if(props.listItem.id === 'new') { - this.state = { - answers : [ - { - 'id': -1, - 'name': '', - 'votes': [], - 'vote_count': 0 - }, - { - 'id': -2, - 'name': '', - 'votes': [], - 'vote_count': 0 - } - ] - } + this.state = DEFAULT_ANSWERS } else { this.state = { @@ -40,13 +40,12 @@ export default class PollForm extends React.Component { addAnswer() { let answers = this.state.answers - let id = answers[answers.length - 1] ? answers[answers.length - 1].id + 1 : 1 let answer = { - 'id': id, 'name': '', 'votes': [], 'vote_count': 0 } + answers.push(answer) this.props.update('answers', answers) } @@ -59,11 +58,8 @@ export default class PollForm extends React.Component { handleUpdateAnswer(e, id) { var answers = this.state.answers - for(var i = 0; i < answers.length; i++) { - if(answers[i].id === id){ - answers[i].name = e.target.value - } - } + answers[id].name = e.target.value + this.props.update('answers', answers) } @@ -83,7 +79,7 @@ export default class PollForm extends React.Component { placeholder='Answer' value={name || ''} fill={true} - onChange={ e => this.handleUpdateAnswer(e, answer.id) } /> + onChange={ e => this.handleUpdateAnswer(e, id) } /> this.removeAnswer(id)}> @@ -191,9 +187,6 @@ export default class PollForm extends React.Component { question={this.props.listItem.question} />
- -
-
) } From 73d0a1bd645963da5b4ea86b5613c3657d38418b Mon Sep 17 00:00:00 2001 From: JamieRL Date: Mon, 4 Jun 2018 13:43:04 -0700 Subject: [PATCH 42/55] remove TODOs --- dispatch/api/serializers.py | 1 - dispatch/tests/test_api_polls.py | 1 - 2 files changed, 2 deletions(-) diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index 1d37f5285..b12530e3e 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -772,7 +772,6 @@ class Meta: 'poll_id' ) -#TODO: verify vote count is working def get_vote_count(self, obj): vote_count = 0 poll = Poll.objects.get(id=obj.poll_id) diff --git a/dispatch/tests/test_api_polls.py b/dispatch/tests/test_api_polls.py index aa5234e51..ee3f7d810 100644 --- a/dispatch/tests/test_api_polls.py +++ b/dispatch/tests/test_api_polls.py @@ -158,7 +158,6 @@ def test_hide_poll_results(self): url = reverse('api-polls-detail', args=[poll.id]) -#TODO if serializer is changed to return none this will have to change response = self.client.get(url, format='json') self.assertEqual(response.data['total_votes'], 0) self.assertEqual(response.data['answers'][0]['vote_count'], 0) From d8716553c2cc236bd6391192d8e317ba4a01c576 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Mon, 4 Jun 2018 14:08:20 -0700 Subject: [PATCH 43/55] change delete old answers in poll model to access answer ids safely and correctly delete answers. Changed test_poll_update so that it tests this funcitonality --- dispatch/modules/content/models.py | 2 +- dispatch/tests/test_api_polls.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dispatch/modules/content/models.py b/dispatch/modules/content/models.py index 127b7431a..ff687a7d9 100644 --- a/dispatch/modules/content/models.py +++ b/dispatch/modules/content/models.py @@ -577,7 +577,7 @@ def save_answers(self, answers, is_new): def delete_old_answers(self, answers): PollAnswer.objects.filter(poll=self) \ - .exclude(id__in=[answer['id'] for answer in answers]) \ + .exclude(id__in=[answer.get('id', 0) for answer in answers]) \ .delete() def get_total_votes(self): diff --git a/dispatch/tests/test_api_polls.py b/dispatch/tests/test_api_polls.py index ee3f7d810..1a7624088 100644 --- a/dispatch/tests/test_api_polls.py +++ b/dispatch/tests/test_api_polls.py @@ -40,12 +40,12 @@ def test_poll_update(self): # Update this poll url = reverse('api-polls-detail', args=[response.data['id']]) - updated_answers = [{'id':1,'name':'updated answer'},{'id':2,'name':'answer2'}] + updated_answers = [{'id':1,'name':'updated answer'}] data = { 'name': 'updated name', 'answers_json': updated_answers } - answers = [ OrderedDict([('id',1), ('name','updated answer'), ('vote_count',0)]), OrderedDict([('id',2), ('name','answer2'), ('vote_count',0)]) ] + answers = [ OrderedDict([('id',1), ('name','updated answer'), ('vote_count',0)]) ] response = self.client.patch(url, data, format='json') # Check data From 60c58701f0c7f8c467d47fed9cf1269745f5c7f3 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Mon, 4 Jun 2018 14:33:10 -0700 Subject: [PATCH 44/55] revert to deleting old votes when re-voting, looks cleaner --- dispatch/api/views.py | 18 +++++------------- dispatch/tests/test_api_polls.py | 2 +- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/dispatch/api/views.py b/dispatch/api/views.py index b49105ef0..d23066a48 100644 --- a/dispatch/api/views.py +++ b/dispatch/api/views.py @@ -273,23 +273,15 @@ def vote(self, request, pk=None): if answer.poll != poll: return Response({ 'Detail': 'invalid answer for this poll'}) - - if 'vote_id' in request.data: - vote_id = request.data['vote_id'] - try: - vote = PollVote.objects.filter(answer__poll=poll).get(id=vote_id) - serializer = PollVoteSerializer(vote, request.data) - except PollVote.DoesNotExist: - return Response({ 'Detail': 'vote id provided is not for this poll'}, status.HTTP_400_BAD_REQUEST) - else: - serializer = PollVoteSerializer(data=request.data) + + serializer = PollVoteSerializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.save() - # if 'vote_id' in request.data: - # vote_id = request.data['vote_id'] - # vote = PollVote.objects.filter(id=vote_id).delete() + if 'vote_id' in request.data: + vote_id = request.data['vote_id'] + vote = PollVote.objects.filter(answer__poll=poll, id=vote_id).delete() return Response(serializer.data) diff --git a/dispatch/tests/test_api_polls.py b/dispatch/tests/test_api_polls.py index 1a7624088..6b47f6bd6 100644 --- a/dispatch/tests/test_api_polls.py +++ b/dispatch/tests/test_api_polls.py @@ -238,7 +238,7 @@ def test_poll_change_vote(self): # Check that the vote change was successful self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['id'], vote_id) + self.assertNotEqual(response.data['id'], vote_id) # Get the poll url = reverse('api-polls-detail', args=[poll_id]) From 2b338a17b9344f5ed2f818d158827063159fbc84 Mon Sep 17 00:00:00 2001 From: rowansdabomb Date: Tue, 5 Jun 2018 16:47:28 -0700 Subject: [PATCH 45/55] code streamline for polls --- .../src/js/components/Header/HeaderButtons.js | 49 ++++++ .../src/js/components/PollEditor/Poll.js | 140 +++++++----------- .../src/js/components/PollEditor/PollForm.js | 85 ++++------- 3 files changed, 135 insertions(+), 139 deletions(-) create mode 100644 dispatch/static/manager/src/js/components/Header/HeaderButtons.js diff --git a/dispatch/static/manager/src/js/components/Header/HeaderButtons.js b/dispatch/static/manager/src/js/components/Header/HeaderButtons.js new file mode 100644 index 000000000..baab65359 --- /dev/null +++ b/dispatch/static/manager/src/js/components/Header/HeaderButtons.js @@ -0,0 +1,49 @@ +import React, { Component } from 'react' +import { Link } from 'react-router' + +require('../../../styles/components/header.scss') +require('../../../styles/utilities/_pseudo_bootstrap.scss') + +const links = { + Content: [['Articles', ''], 'Pages'], + Widgets: ['Polls', 'Zones', 'Events'], + Media: ['Galleris', 'Files', 'Images', 'Videos'], + Misc: ['Issues', 'People'] +} + +const renderLinks = (type, key, items) => { + console.log(key) + console.log(items) + console.log('render links') + return( +
+
+ + {String(key)} +
+
+ { + items.map((item, index) => { + {item} + }) + } +
+
+ ) +} +const HeaderButtons = (props) => { + const dropdownType = props.isDesktop ? 'nav-dropdown-container' : 'nav-dropdown-container-mobile' + return ( +
+
+ { + Object.keys(links).map((key, index)=> { + return( renderLinks(dropdownType, key, links[key])) + }) + } +
+
+ ) +} + +export default HeaderButtons \ No newline at end of file diff --git a/dispatch/static/manager/src/js/components/PollEditor/Poll.js b/dispatch/static/manager/src/js/components/PollEditor/Poll.js index e873eb79b..6491eaa51 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/Poll.js +++ b/dispatch/static/manager/src/js/components/PollEditor/Poll.js @@ -13,53 +13,13 @@ class Poll extends Component { } } - update() { - let answers = [] - let votes = [] - let pollQuestion = this.props.question ? this.props.question : 'Poll Question' - if(this.props.answers) { - for(let answer of this.props.answers) { - if(answer['name'] !== '') { - answers.push(answer['name']) - } else { - answers.push('Answer') - } - votes.push(answer['vote_count']) - } - } else { - answers.push('Answer') - answers.push('Answer') - } - - let temp = votes.filter((item) => {return item === 0}) - let noVotes = false - if(temp.length === votes.length) { - //no votes yet, populate with dummy data for better poll visualization - votes[0] = 2 - votes[1] = 1 - noVotes = true - } + getPollResult(voteCount) { + const total = this.props.answers.reduce((acc, answer) => acc + answer.vote_count, 0) - return { - answers: answers, - votes: votes, - loading: false, - pollQuestion: pollQuestion, - noVotes: noVotes, + if(this.state.showResults && total) { + return String((100*voteCount/total).toFixed(0)) + '%' } - } - getPollResult(index, votes) { - if(this.state.showResults) { - let width = 0 - let total = votes.reduce((acc, val) => { return acc + val }) - - if(total !== 0) { - width = String((100*votes[index]/total).toFixed(0)) + '%' - } - - return width - } return 0 } @@ -70,59 +30,69 @@ class Poll extends Component { } render() { - const { answers, votes, loading, pollQuestion, noVotes } = this.update() - + const { answers, question } = this.props const notShowResult= this.state.showResults ? 0 : COLOR_OPACITY const showResult = this.state.showResults ? COLOR_OPACITY : 0 return (
- {!loading && -
-
-

{pollQuestion}

-
- {answers.map((answer, index) => { - return( -
) } } +Poll.defaultProps = { + question: 'Default question', + answers: [ + { + name: 'Answer 1', + vote_count: 2, + votes: [] + }, + { + name: 'Answer 2', + vote_count: 1, + votes: [] + }, + ] +} + export default Poll diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index 7d136c5f9..17a02d188 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -1,5 +1,6 @@ import React from 'react' import { Button, Intent } from '@blueprintjs/core' +import R from 'ramda' import { FormInput, TextInput } from '../inputs' import Poll from './Poll' @@ -7,82 +8,58 @@ import SelectInput from '../inputs/selects/SelectInput' require('../../../styles/components/poll_form.scss') -const DEFAULT_ANSWERS = { - answers : [ - { - 'name': '', - 'votes': [], - 'vote_count': 0 - }, - { - 'name': '', - 'votes': [], - 'vote_count': 0 - } - ] -} +const DEFAULT_ANSWERS = [ + { + 'name': '', + 'votes': [], + 'vote_count': 0 + }, + { + 'name': '', + 'votes': [], + 'vote_count': 0 + } +] export default class PollForm extends React.Component { - constructor(props) { - super(props) - - if(props.listItem.id === 'new') { - this.state = DEFAULT_ANSWERS - } - else { - this.state = { - answers: props.listItem.answers - } - } - } - addAnswer() { - let answers = this.state.answers - - let answer = { - 'name': '', - 'votes': [], - 'vote_count': 0 - } - - answers.push(answer) - this.props.update('answers', answers) + this.props.update('answers', this.getAnswers() + DEFAULT_ANSWERS[0]) } - removeAnswer(id) { - let answers = this.state.answers - answers.splice(id, 1) - this.props.update('answers', answers) + removeAnswer(index) { + this.props.update('answers', R.remove(index, 1, this.getAnswers())) } - handleUpdateAnswer(e, id) { - var answers = this.state.answers - answers[id].name = e.target.value + handleUpdateAnswer(e, index) { + this.props.update('answers', + R.adjust(R.assoc('name', e.target.value), index, this.getAnswers()) + ) + } - this.props.update('answers', answers) + getAnswers() { + return this.props.listItem.answers || DEFAULT_ANSWERS } renderAnswers() { - let answers = this.state.answers.map( + const answers = this.getAnswers().map( (answer, index) => { - let name = answer.name - let votes = answer.vote_count - let key = index + 1 - let id = index + const name = answer.name + const votes = answer.vote_count + const key = index + 1 return ( this.handleUpdateAnswer(e, id) } /> + onChange={ e => this.handleUpdateAnswer(e, index) } /> this.removeAnswer(id)}> + onClick={() => this.removeAnswer(index)}> Remove answer @@ -190,4 +167,4 @@ export default class PollForm extends React.Component {
) } -} +} \ No newline at end of file From 0d7156217003dcc0229c7f35ac3ffd2da985b604 Mon Sep 17 00:00:00 2001 From: Peter Siemens Date: Tue, 5 Jun 2018 23:46:13 -0700 Subject: [PATCH 46/55] Upgrade packages, add new linter rules --- dispatch/static/manager/.eslintrc.js | 7 +- dispatch/static/manager/package.json | 29 +- .../static/manager/src/js/api/dispatch.js | 16 +- .../ArticleEditor/ArticleContentEditor.js | 2 +- .../ArticleEditor/ArticleSidebar.js | 12 +- .../ArticleEditor/ArticleToolbar.js | 8 +- .../src/js/components/ArticleEditor/index.js | 4 +- .../ArticleEditor/tabs/BasicFieldsTab.js | 12 +- .../js/components/Editor/tabs/DeliveryTab.js | 6 +- .../Editor/tabs/FeaturedImageTab.js | 2 +- .../src/js/components/Editor/tabs/SEOTab.js | 4 +- .../js/components/Editor/tabs/TemplateTab.js | 4 +- .../Editor/toolbar/VersionsDropdown.js | 19 +- .../js/components/EventEditor/EventCard.js | 3 +- .../js/components/EventEditor/EventForm.js | 40 +- .../components/EventEditor/EventPendingTag.js | 2 +- .../src/js/components/EventEditor/index.js | 12 +- .../GalleryEditor/AttachmentForm.js | 4 +- .../js/components/GalleryEditor/DnDZone.js | 2 +- .../components/GalleryEditor/GalleryForm.js | 35 +- .../manager/src/js/components/Header.js | 9 +- .../js/components/ImageEditor/ImageForm.js | 6 +- .../manager/src/js/components/ImageStore.js | 128 ++-- .../components/ItemEditor/ListItemToolbar.js | 6 +- .../src/js/components/ItemEditor/index.js | 4 +- .../js/components/ItemList/ItemListHeader.js | 3 +- .../components/ItemList/ItemListSearchBar.js | 2 +- .../src/js/components/ModalContainer.js | 2 +- .../PageEditor/PageContentEditor.js | 2 +- .../js/components/PageEditor/PageSidebar.js | 8 +- .../js/components/PageEditor/PageToolbar.js | 2 +- .../src/js/components/PageEditor/index.js | 6 +- .../PageEditor/tabs/BasicFieldsTab.js | 4 +- .../js/components/PersonEditor/PersonForm.js | 10 +- .../src/js/components/PlaceholderBar.js | 2 +- .../src/js/components/PollEditor/Poll.js | 33 +- .../src/js/components/PollEditor/PollForm.js | 11 +- .../components/SectionEditor/SectionForm.js | 4 +- .../src/js/components/TagEditor/TagForm.js | 2 +- .../js/components/TopicEditor/TopicForm.js | 2 +- .../js/components/VideoEditor/VideoForm.js | 4 +- .../manager/src/js/components/WidgetFields.js | 2 +- .../src/js/components/ZoneEditor/index.js | 2 +- .../src/js/components/fields/IntegerField.js | 8 +- .../src/js/components/fields/WidgetField.js | 40 +- .../src/js/components/inputs/DateTimeInput.js | 2 +- .../src/js/components/inputs/ImageInput.js | 10 +- .../src/js/components/inputs/TextInput.js | 6 +- .../inputs/filters/AuthorFilterInput.js | 3 +- .../inputs/filters/TagsFilterInput.js | 3 +- .../inputs/selects/ItemSelectInput.js | 30 +- .../integrations/FBInstantArticles.js | 136 ---- .../modals/ImageManager/ImagePanel.js | 2 +- .../modals/ImageManager/ImageThumb.js | 5 +- .../components/modals/ImageManager/index.js | 10 +- .../manager/src/js/containers/AppContainer.js | 2 +- .../src/js/containers/BasicContainer.js | 2 +- .../src/js/containers/MainContainer.js | 14 +- dispatch/static/manager/src/js/index.js | 4 - .../src/js/pages/Articles/ArticleIndexPage.js | 6 +- .../manager/src/js/pages/DashboardPage.js | 8 +- .../src/js/pages/Events/EventAuditPage.js | 14 +- .../manager/src/js/pages/Events/EventPage.js | 2 +- .../src/js/pages/Events/NewEventPage.js | 2 +- .../static/manager/src/js/pages/FilesPage.js | 10 +- .../src/js/pages/Galleries/GalleryPage.js | 2 +- .../src/js/pages/Galleries/NewGalleryPage.js | 2 +- .../src/js/pages/Images/EditImagePage.js | 2 +- .../src/js/pages/Images/ImagesIndexPage.js | 21 +- .../FBInstantArticlesIntegrationPage.js | 191 ----- .../Integrations/IntegrationsIndexPage.js | 19 - .../src/js/pages/Integrations/index.js | 7 - .../manager/src/js/pages/Issues/IssuePage.js | 2 +- .../src/js/pages/Issues/NewIssuePage.js | 2 +- .../manager/src/js/pages/ItemIndexPage.js | 4 +- .../src/js/pages/Pages/EditPagePage.js | 2 +- .../manager/src/js/pages/Pages/NewPagePage.js | 2 +- .../src/js/pages/Persons/NewPersonPage.js | 2 +- .../src/js/pages/Persons/PersonPage.js | 2 +- .../manager/src/js/pages/Polls/NewPollPage.js | 2 +- .../manager/src/js/pages/Polls/PollPage.js | 2 +- .../manager/src/js/pages/ProfilePage.js | 8 +- .../src/js/pages/Sections/NewSectionPage.js | 2 +- .../src/js/pages/Sections/SectionPage.js | 2 +- .../manager/src/js/pages/Tags/NewTagPage.js | 2 +- .../manager/src/js/pages/Tags/TagPage.js | 2 +- .../src/js/pages/Topics/NewTopicPage.js | 2 +- .../manager/src/js/pages/Topics/TopicPage.js | 2 +- .../src/js/pages/Videos/NewVideoPage.js | 2 +- .../src/js/pages/Videos/VideoIndexPage.js | 4 +- .../manager/src/js/pages/Videos/VideoPage.js | 2 +- .../src/js/pages/Widgets/ZoneIndexPage.js | 5 +- .../manager/src/js/pages/Widgets/ZonePage.js | 2 +- dispatch/static/manager/src/js/pages/index.js | 2 - .../manager/src/js/util/redux/actions.js | 20 +- .../dispatch-editor/components/Editor.js | 27 +- .../dispatch-editor/components/Embed.js | 2 +- .../components/EmbedToolbar.js | 4 +- .../components/FormatPopover.js | 24 +- .../dispatch-editor/components/LinkEditor.js | 10 +- .../dispatch-editor/embeds/CodeEmbed.js | 5 +- .../dispatch-editor/embeds/WidgetEmbed.js | 30 +- .../dispatch-editor/helpers/convertJSON.js | 2 +- dispatch/static/manager/yarn-error.log | 439 +++++++---- dispatch/static/manager/yarn.lock | 705 +++++++++--------- 105 files changed, 1084 insertions(+), 1281 deletions(-) delete mode 100644 dispatch/static/manager/src/js/components/integrations/FBInstantArticles.js delete mode 100644 dispatch/static/manager/src/js/pages/Integrations/FBInstantArticlesIntegrationPage.js delete mode 100644 dispatch/static/manager/src/js/pages/Integrations/IntegrationsIndexPage.js delete mode 100644 dispatch/static/manager/src/js/pages/Integrations/index.js diff --git a/dispatch/static/manager/.eslintrc.js b/dispatch/static/manager/.eslintrc.js index d47e6ac51..a1e1e1b8c 100644 --- a/dispatch/static/manager/.eslintrc.js +++ b/dispatch/static/manager/.eslintrc.js @@ -19,9 +19,14 @@ 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, }, "plugins": [ "react" diff --git a/dispatch/static/manager/package.json b/dispatch/static/manager/package.json index 16798377b..47f32b1a6 100644 --- a/dispatch/static/manager/package.json +++ b/dispatch/static/manager/package.json @@ -12,7 +12,7 @@ "@blueprintjs/datetime": "^1.16.0", "babel": "^6.23.0", "babel-core": "^6.21.0", - "babel-loader": "^6.2.10", + "babel-loader": "^7.1.4", "babel-preset-es2015": "^6.13.2", "babel-preset-react": "^6.11.1", "class-autobind": "^0.1.4", @@ -21,37 +21,38 @@ "es6-promise": "^3.2.1", "eslint": "^3.17.1", "eslint-loader": "^1.6.3", - "eslint-plugin-react": "^6.10.0", + "eslint-plugin-react": "^7.9.1", "extract-text-webpack-plugin": "^3.0.0", "file-loader": "^0.10.0", - "history": "^2.1.2", + "history": "^3.0.0", "immutable": "^3.8.1", "isomorphic-fetch": "^2.2.1", "jquery": "^3.1.1", "js-cookie": "^2.1.2", "moment": "^2.17.1", - "node-sass": "^4.5.0", + "node-sass": "^4.9.0", "normalizr": "^2.2.1", "qwery": "^4.0.0", "ramda": "^0.22.1", - "react": "^15.4.2", - "react-ace": "^5.0.1", + "react": "^16.4.0", + "react-ace": "^6.1.1", "react-addons-css-transition-group": "^15.4.1", - "react-dnd": "^2.4.0", - "react-dnd-html5-backend": "^2.4.1", + "react-day-picker": "^7.1.9", + "react-dnd": "^4.0.2", + "react-dnd-html5-backend": "^4.0.2", "react-document-title": "^2.0.2", - "react-dom": "^15.3.1", - "react-dropzone": "^3.12.3", + "react-dom": "^16.4.0", + "react-dropzone": "^4.2.11", "react-measure": "^1.4.7", "react-redux": "^4.4.5", - "react-redux-loading-bar": "^2.7.2", - "react-router": "^2.7.0", - "react-router-redux": "^4.0.5", + "react-redux-loading-bar": "^4.0.5", + "react-router": "^3.2.1", + "react-router-redux": "^4.0.8", "redux": "^3.5.2", "redux-logger": "^3.0.6", "redux-promise-middleware": "^4.0.0", "redux-thunk": "^2.1.0", - "sass-loader": "^6.0.2", + "sass-loader": "^7.0.3", "style-loader": "^0.13.1", "url": "^0.11.0", "url-loader": "^0.5.7", diff --git a/dispatch/static/manager/src/js/api/dispatch.js b/dispatch/static/manager/src/js/api/dispatch.js index 6b5931a60..daf9f8748 100644 --- a/dispatch/static/manager/src/js/api/dispatch.js +++ b/dispatch/static/manager/src/js/api/dispatch.js @@ -15,7 +15,7 @@ function prepareMultipartPayload(payload) { if (payload[key] && payload[key].constructor === File) { formData.append(key, payload[key]) } else if (typeof payload[key] !== 'undefined') { - if(payload[key] === null) { + if (payload[key] === null) { formData.append(key, '') } else { @@ -78,7 +78,7 @@ function getRequest(route, id=null, query={}, token=null) { headers: buildHeaders(token) } ) - .then(parseJSON) + .then(parseJSON) } function getPageRequest(uri, token=null) { @@ -89,7 +89,7 @@ function getPageRequest(uri, token=null) { headers: buildHeaders(token) } ) - .then(parseJSON) + .then(parseJSON) } function postRequest(route, id=null, payload={}, token=null) { @@ -101,7 +101,7 @@ function postRequest(route, id=null, payload={}, token=null) { body: JSON.stringify(payload) } ) - .then(parseJSON) + .then(parseJSON) } function postMultipartRequest(route, id=null, payload={}, token=null) { @@ -113,7 +113,7 @@ function postMultipartRequest(route, id=null, payload={}, token=null) { body: prepareMultipartPayload(payload) } ) - .then(parseJSON) + .then(parseJSON) } function patchMultipartRequest(route, id=null, payload={}, token=null) { @@ -125,7 +125,7 @@ function patchMultipartRequest(route, id=null, payload={}, token=null) { body: prepareMultipartPayload(payload) } ) - .then(parseJSON) + .then(parseJSON) } function deleteRequest(route, id=null, payload={}, token=null) { @@ -137,7 +137,7 @@ function deleteRequest(route, id=null, payload={}, token=null) { body: JSON.stringify(payload) } ) - .then(handleError) + .then(handleError) } function patchRequest(route, id=null, payload={}, token=null) { @@ -149,7 +149,7 @@ function patchRequest(route, id=null, payload={}, token=null) { body: JSON.stringify(payload) } ) - .then(parseJSON) + .then(parseJSON) } const DispatchAPI = { diff --git a/dispatch/static/manager/src/js/components/ArticleEditor/ArticleContentEditor.js b/dispatch/static/manager/src/js/components/ArticleEditor/ArticleContentEditor.js index 6d6e346cf..af8385739 100644 --- a/dispatch/static/manager/src/js/components/ArticleEditor/ArticleContentEditor.js +++ b/dispatch/static/manager/src/js/components/ArticleEditor/ArticleContentEditor.js @@ -24,7 +24,7 @@ const embeds = [ export default class ArticleContentEditor extends React.Component { render() { return ( -
+
- Basic fields - Featured image - Featured video - Delivery - Template - SEO + Basic fields + Featured image + Featured video + Delivery + Template + SEO diff --git a/dispatch/static/manager/src/js/components/ArticleEditor/ArticleToolbar.js b/dispatch/static/manager/src/js/components/ArticleEditor/ArticleToolbar.js index 190d03295..2e211f3f3 100644 --- a/dispatch/static/manager/src/js/components/ArticleEditor/ArticleToolbar.js +++ b/dispatch/static/manager/src/js/components/ArticleEditor/ArticleToolbar.js @@ -12,7 +12,7 @@ export default function ArticleToolbar(props) { intent={Intent.PRIMARY} onClick={() => props.publishArticle()} disabled={props.isNew}> - Publish + Publish ) @@ -21,7 +21,7 @@ export default function ArticleToolbar(props) { intent={Intent.PRIMARY} onClick={() => props.unpublishArticle()} disabled={props.isNew}> - Unpublish + Unpublish ) @@ -32,13 +32,13 @@ export default function ArticleToolbar(props) { props.saveArticle()}> - Update + Update {props.article.is_published ? unpublish : publish} props.previewArticle()}> - Preview + Preview props.update('slug', e.target.value) } /> + onChange={e => props.update('slug', e.target.value)} /> props.update('section', section) } /> + update={section => props.update('section', section)} /> props.update('authors', authors) } /> + update={authors => props.update('authors', authors)} /> props.update('tags', tags) } /> + update={tags => props.update('tags', tags)} /> props.update('topic', topic) } /> + update={topic => props.update('topic', topic)} /> props.update('snippet', e.target.value) } /> + onChange={e => props.update('snippet', e.target.value)} />
diff --git a/dispatch/static/manager/src/js/components/Editor/tabs/DeliveryTab.js b/dispatch/static/manager/src/js/components/Editor/tabs/DeliveryTab.js index d35a0a1b9..5034ff09a 100644 --- a/dispatch/static/manager/src/js/components/Editor/tabs/DeliveryTab.js +++ b/dispatch/static/manager/src/js/components/Editor/tabs/DeliveryTab.js @@ -57,14 +57,14 @@ export default function DeliveryTab(props) { props.update('importance', e.target.value) } /> + onChange={e => props.update('importance', e.target.value)} /> props.update('reading_time', e.target.value) } /> + onChange={e => props.update('reading_time', e.target.value)} /> @@ -72,7 +72,7 @@ export default function DeliveryTab(props) { className='pt-large' disabled={!isInstantArticlesEnabled} checked={R.path(['fb-instant-articles', 'enabled'], props.integrations)} - onChange={ e => updateInstantArticle(props.update, props.integrations, e.target.checked) } /> + onChange={e => updateInstantArticle(props.update, props.integrations, e.target.checked)} /> {warningMessage} diff --git a/dispatch/static/manager/src/js/components/Editor/tabs/FeaturedImageTab.js b/dispatch/static/manager/src/js/components/Editor/tabs/FeaturedImageTab.js index b35f9b946..941849c22 100644 --- a/dispatch/static/manager/src/js/components/Editor/tabs/FeaturedImageTab.js +++ b/dispatch/static/manager/src/js/components/Editor/tabs/FeaturedImageTab.js @@ -6,7 +6,7 @@ import { FormInput, TextAreaInput, ImageInput } from '../../inputs' export default function FeaturedImageTab(props) { function updateImage(imageId) { - if(imageId){ + if (imageId){ return props.update( 'featured_image', R.merge(props.featured_image, { image: imageId }) diff --git a/dispatch/static/manager/src/js/components/Editor/tabs/SEOTab.js b/dispatch/static/manager/src/js/components/Editor/tabs/SEOTab.js index 0c4e18ce4..1882d0451 100644 --- a/dispatch/static/manager/src/js/components/Editor/tabs/SEOTab.js +++ b/dispatch/static/manager/src/js/components/Editor/tabs/SEOTab.js @@ -59,7 +59,7 @@ export default function SEOTab(props) { placeholder='Focus Keywords' value={props.seo_keyword} fill={true} - onChange={ e => props.update('seo_keyword', e.target.value) } /> + onChange={e => props.update('seo_keyword', e.target.value)} />
props.update('seo_description', e.target.value) } /> + onChange={e => props.update('seo_description', e.target.value)} />
this.props.update('template', template) } /> + update={template => this.props.update('template', template)} />
{fields}
diff --git a/dispatch/static/manager/src/js/components/Editor/toolbar/VersionsDropdown.js b/dispatch/static/manager/src/js/components/Editor/toolbar/VersionsDropdown.js index 29adc6f10..a7e462489 100644 --- a/dispatch/static/manager/src/js/components/Editor/toolbar/VersionsDropdown.js +++ b/dispatch/static/manager/src/js/components/Editor/toolbar/VersionsDropdown.js @@ -8,6 +8,12 @@ require('../../../../styles/components/versions_dropdown.scss') export default class VersionsDropdown extends React.Component { + constructor(props) { + super(props) + + this.dropdown = React.createRef() + } + getVersions() { let versions = [] @@ -20,11 +26,10 @@ export default class VersionsDropdown extends React.Component { selectVersion(version) { this.props.getVersion(version) - this.refs.dropdown.close() + this.dropdown.current.close() } - - renderDropdown() { + renderDropdown() { let versions = this.getVersions().map( (version) => { @@ -37,7 +42,7 @@ export default class VersionsDropdown extends React.Component { className={`o-dropdown-list__item${selectedClassName}`} key={version} onClick={() => this.selectVersion(version)}> - + {`Version ${version}${published}`} ) @@ -56,11 +61,11 @@ export default class VersionsDropdown extends React.Component { if (this.props.latest_version) { return ( - this.refs.dropdown.open()}> + this.dropdown.current.open()}> {`Version ${this.props.current_version}`} - + ) diff --git a/dispatch/static/manager/src/js/components/EventEditor/EventCard.js b/dispatch/static/manager/src/js/components/EventEditor/EventCard.js index 75ff75d78..6051c9b32 100644 --- a/dispatch/static/manager/src/js/components/EventEditor/EventCard.js +++ b/dispatch/static/manager/src/js/components/EventEditor/EventCard.js @@ -24,8 +24,7 @@ export default function EventCard(props) { const image = (
-
+ style={{backgroundImage: `url('${props.event.image}')`}} /> ) return ( diff --git a/dispatch/static/manager/src/js/components/EventEditor/EventForm.js b/dispatch/static/manager/src/js/components/EventEditor/EventForm.js index 00a52de4f..b369ad198 100644 --- a/dispatch/static/manager/src/js/components/EventEditor/EventForm.js +++ b/dispatch/static/manager/src/js/components/EventEditor/EventForm.js @@ -5,14 +5,14 @@ import { AnchorButton } from '@blueprintjs/core' import { FormInput, TextInput, TextAreaInput, DateTimeInput, SelectInput } from '../inputs' const CATEGORY_CHOICES = [ - ['sports', 'Sports'], - ['music', 'Music'], - ['academic', 'Academic'], - ['party', 'Party'], - ['business', 'Business'], - ['ceremony', 'Ceremony'], - ['workshop', 'Workshop'], - ['other', 'Other'] + ['sports', 'Sports'], + ['music', 'Music'], + ['academic', 'Academic'], + ['party', 'Party'], + ['business', 'Business'], + ['ceremony', 'Ceremony'], + ['workshop', 'Workshop'], + ['other', 'Other'] ] export default class EventForm extends React.Component { @@ -40,7 +40,7 @@ export default class EventForm extends React.Component { placeholder='Name' value={this.props.listItem.title || ''} fill={true} - onChange={ e => this.props.update('title', e.target.value) } /> + onChange={e => this.props.update('title', e.target.value)} /> this.props.update('description', e.target.value) } /> + onChange={e => this.props.update('description', e.target.value)} /> this.props.update('host', e.target.value) } /> + onChange={e => this.props.update('host', e.target.value)} /> this.props.update('start_time', dt)} /> + onChange={dt => this.props.update('start_time', dt)} /> this.props.update('end_time', dt)} /> + onChange={dt => this.props.update('end_time', dt)} /> this.props.update('location', e.target.value) } /> + onChange={e => this.props.update('location', e.target.value)} /> this.props.update('address', e.target.value) } /> + onChange={e => this.props.update('address', e.target.value)} /> this.props.update('category', e.target.value)} /> + onChange={e => this.props.update('category', e.target.value)} /> this.props.update('event_url', e.target.value) } /> + onChange={e => this.props.update('event_url', e.target.value)} /> this.props.update('ticket_url', e.target.value) } /> + onChange={e => this.props.update('ticket_url', e.target.value)} /> this.props.update('submitter_email', e.target.value) } /> + onChange={e => this.props.update('submitter_email', e.target.value)} /> this.props.update('submitter_phone', e.target.value) } /> + onChange={e => this.props.update('submitter_phone', e.target.value)} /> ) diff --git a/dispatch/static/manager/src/js/components/EventEditor/EventPendingTag.js b/dispatch/static/manager/src/js/components/EventEditor/EventPendingTag.js index 6d49ede97..8be64ce88 100644 --- a/dispatch/static/manager/src/js/components/EventEditor/EventPendingTag.js +++ b/dispatch/static/manager/src/js/components/EventEditor/EventPendingTag.js @@ -8,7 +8,7 @@ require('../../../styles/components/event_audit.scss') class EventPendingTagComponent extends React.Component { - componentWillMount() { + componentDidMount() { this.props.countPending(this.props.token, { pending: 1, limit: 0 }) } diff --git a/dispatch/static/manager/src/js/components/EventEditor/index.js b/dispatch/static/manager/src/js/components/EventEditor/index.js index 0a16244e4..d637655b2 100644 --- a/dispatch/static/manager/src/js/components/EventEditor/index.js +++ b/dispatch/static/manager/src/js/components/EventEditor/index.js @@ -79,19 +79,21 @@ const mapDispatchToProps = (dispatch) => { function EventEditorComponent(props) { const publishButton = ( - props.publishEvent(props.token, props.listItem.id)} + props.publishEvent(props.token, props.listItem.id)} intent={Intent.PRIMARY} disabled={props.isNew} > - + Publish ) const unpublishButton = ( - props.unpublishEvent(props.token, props.listItem.id)} + props.unpublishEvent(props.token, props.listItem.id)} intent={Intent.PRIMARY} disabled={props.isNew} > - + Unpublish ) @@ -109,7 +111,7 @@ function EventEditorComponent(props) { const isPublished = (obj) => { return obj.entities.local && obj.listItem.id - ? obj.entities.local[obj.listItem.id].is_published : false + ? obj.entities.local[obj.listItem.id].is_published : false } const EventEditor = connect( diff --git a/dispatch/static/manager/src/js/components/GalleryEditor/AttachmentForm.js b/dispatch/static/manager/src/js/components/GalleryEditor/AttachmentForm.js index 22244792f..2dacd5e10 100644 --- a/dispatch/static/manager/src/js/components/GalleryEditor/AttachmentForm.js +++ b/dispatch/static/manager/src/js/components/GalleryEditor/AttachmentForm.js @@ -14,7 +14,7 @@ export default function AttachmentForm(props) { placeholder='Caption' value={props.caption || ''} fill={true} - onChange={ e => props.update('caption', e.target.value) } /> + onChange={e => props.update('caption', e.target.value)} /> props.update('credit', e.target.value) } /> + onChange={e => props.update('credit', e.target.value)} /> diff --git a/dispatch/static/manager/src/js/components/GalleryEditor/DnDZone.js b/dispatch/static/manager/src/js/components/GalleryEditor/DnDZone.js index f207699e2..47ba6d402 100644 --- a/dispatch/static/manager/src/js/components/GalleryEditor/DnDZone.js +++ b/dispatch/static/manager/src/js/components/GalleryEditor/DnDZone.js @@ -25,7 +25,7 @@ function collect(connect, monitor) { } class DnDZone extends React.Component { - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.isOver && !nextProps.isOver) { this.props.endDrag() } diff --git a/dispatch/static/manager/src/js/components/GalleryEditor/GalleryForm.js b/dispatch/static/manager/src/js/components/GalleryEditor/GalleryForm.js index bda77f0c8..8f29ffc6a 100644 --- a/dispatch/static/manager/src/js/components/GalleryEditor/GalleryForm.js +++ b/dispatch/static/manager/src/js/components/GalleryEditor/GalleryForm.js @@ -125,9 +125,9 @@ class GalleryFormComponent extends React.Component { let newImages if (images && images.length) { newImages = R.filter(id => R.findIndex(R.either( - R.propSatisfies(image => image && image.id == id, 'image'), - R.propEq('image_id', id) - ), images) === -1, ids) + R.propSatisfies(image => image && image.id == id, 'image'), + R.propEq('image_id', id) + ), images) === -1, ids) .map(makeNewImage) } else { newImages = R.map(makeNewImage, ids) @@ -206,18 +206,17 @@ class GalleryFormComponent extends React.Component { onClose={() => { this.props.selectImage(0) }} // put the rightmost thumbs' popover on the left position={(i % this.getNumPerRow() == 0) - ? Position.LEFT : Position.RIGHT} - > -
-
- {i++} -
+ ? Position.LEFT : Position.RIGHT}> +
+
+ {i++}
+
this.props.update('title', e.target.value) } /> + onChange={e => this.props.update('title', e.target.value)} />

Gallery

+ endDrag={this.endDrag}> {inGalleryImages} + }}>↵ diff --git a/dispatch/static/manager/src/js/components/Header.js b/dispatch/static/manager/src/js/components/Header.js index 7dc567c91..08dd5a0a8 100644 --- a/dispatch/static/manager/src/js/components/Header.js +++ b/dispatch/static/manager/src/js/components/Header.js @@ -11,7 +11,7 @@ export default function Header(props) { diff --git a/dispatch/static/manager/src/js/components/ImageEditor/ImageForm.js b/dispatch/static/manager/src/js/components/ImageEditor/ImageForm.js index 92698c3ae..fc164e7a0 100644 --- a/dispatch/static/manager/src/js/components/ImageEditor/ImageForm.js +++ b/dispatch/static/manager/src/js/components/ImageEditor/ImageForm.js @@ -37,7 +37,7 @@ export default class ImageForm extends React.Component { placeholder='Image title' value={this.props.listItem.title || ''} fill={true} - onChange={ e => this.props.update('title', e.target.value) } /> + onChange={e => this.props.update('title', e.target.value)} /> this.props.update('tags', tags) } /> - + update={tags => this.props.update('tags', tags)} /> +
diff --git a/dispatch/static/manager/src/js/components/ImageStore.js b/dispatch/static/manager/src/js/components/ImageStore.js index 421f4ee8d..6058a5e83 100644 --- a/dispatch/static/manager/src/js/components/ImageStore.js +++ b/dispatch/static/manager/src/js/components/ImageStore.js @@ -1,69 +1,69 @@ -var _ = require('lodash'); +var _ = require('lodash') var ImageStore = function(){ - return { - images: [], - dump: function(images){ - this.images = images; - }, - append: function(images){ - this.images = this.images.concat(images); - }, - addTemp: function(name, thumb){ - var tempImage = { - tempName: name, - thumb: thumb, - } - this.images.unshift(tempImage); - }, - updateProgress: function(name, progress){ - var i = _.findIndex(this.images, {tempName: name}) - this.images[i].progress = progress; - }, - updateImage: function(id, callback){ - dispatch.find('image', id, function(data){ - var i = _.findIndex(this.images, {id: id}); - this.images[i] = data; - callback(); - }.bind(this)) - }, - updateImageWithData: function(data){ - var i = _.findIndex(this.images, {id: data.id}); - this.images[i] = data; - }, - updateAttachment: function(attachment_id, data){ - var i = _.findIndex(this.images, {attachment_id: attachment_id}); - this.images[i] = data; - }, - replaceTemp: function(name, image){ - var i = _.findIndex(this.images, {tempName: name}); - this.images[i] = image; - }, - getImage: function(id){ - var i = _.findIndex(this.images, {id: id}); - return this.images[i]; - }, - getImages: function(ids){ - var images = []; - _.forEach(ids, function(id, index){ - images.push(this.getImage(id)); - }.bind(this)); - return images; - }, - removeImage: function(id){ - _.remove(this.images, function(n) { - return n.id == id; - }); - }, - removeAttachment: function(attachment_id) { - _.remove(this.images, function(n) { - return n.attachment_id == attachment_id; - }); - }, - all: function(){ - return this.images; - } + return { + images: [], + dump: function(images){ + this.images = images + }, + append: function(images){ + this.images = this.images.concat(images) + }, + addTemp: function(name, thumb){ + var tempImage = { + tempName: name, + thumb: thumb, + } + this.images.unshift(tempImage) + }, + updateProgress: function(name, progress){ + var i = _.findIndex(this.images, {tempName: name}) + this.images[i].progress = progress + }, + updateImage: function(id, callback){ + dispatch.find('image', id, function(data){ + var i = _.findIndex(this.images, {id: id}) + this.images[i] = data + callback() + }.bind(this)) + }, + updateImageWithData: function(data){ + var i = _.findIndex(this.images, {id: data.id}) + this.images[i] = data + }, + updateAttachment: function(attachment_id, data){ + var i = _.findIndex(this.images, {attachment_id: attachment_id}) + this.images[i] = data + }, + replaceTemp: function(name, image){ + var i = _.findIndex(this.images, {tempName: name}) + this.images[i] = image + }, + getImage: function(id){ + var i = _.findIndex(this.images, {id: id}) + return this.images[i] + }, + getImages: function(ids){ + var images = [] + _.forEach(ids, function(id, index){ + images.push(this.getImage(id)) + }.bind(this)) + return images + }, + removeImage: function(id){ + _.remove(this.images, function(n) { + return n.id == id + }) + }, + removeAttachment: function(attachment_id) { + _.remove(this.images, function(n) { + return n.attachment_id == attachment_id + }) + }, + all: function(){ + return this.images } + } } -module.exports = ImageStore; +module.exports = ImageStore diff --git a/dispatch/static/manager/src/js/components/ItemEditor/ListItemToolbar.js b/dispatch/static/manager/src/js/components/ItemEditor/ListItemToolbar.js index 8fad89262..311af1da2 100644 --- a/dispatch/static/manager/src/js/components/ItemEditor/ListItemToolbar.js +++ b/dispatch/static/manager/src/js/components/ItemEditor/ListItemToolbar.js @@ -20,7 +20,7 @@ export default function ListItemToolbar(props) { intent={Intent.DANGER} disabled={props.isNew} onConfirm={() => props.deleteListItem()}> - Delete + Delete ) @@ -29,7 +29,7 @@ export default function ListItemToolbar(props) { props.goBack()}> - Back + Back {props.isNew ? newTitle : editTitle} @@ -37,7 +37,7 @@ export default function ListItemToolbar(props) { props.saveListItem()}> - {props.isNew ? 'Save' : 'Update'} + {props.isNew ? 'Save' : 'Update'} {props.extraButton} {props.deleteListItem ? deleteButton : null} diff --git a/dispatch/static/manager/src/js/components/ItemEditor/index.js b/dispatch/static/manager/src/js/components/ItemEditor/index.js index 26d7ef43d..ec87aa5e6 100644 --- a/dispatch/static/manager/src/js/components/ItemEditor/index.js +++ b/dispatch/static/manager/src/js/components/ItemEditor/index.js @@ -11,7 +11,7 @@ const NEW_LISTITEM_ID = 'new' class ItemEditor extends React.Component { - componentWillMount() { + componentDidMount() { if (this.props.isNew) { // Create empty listItem this.props.setListItem({ id: NEW_LISTITEM_ID }) @@ -19,9 +19,7 @@ class ItemEditor extends React.Component { // Fetch listItem this.props.getListItem(this.props.token, this.props.itemId) } - } - componentDidMount() { if (this.props.route) { confirmNavigation( this.props.router, diff --git a/dispatch/static/manager/src/js/components/ItemList/ItemListHeader.js b/dispatch/static/manager/src/js/components/ItemList/ItemListHeader.js index e53278e59..08d384019 100644 --- a/dispatch/static/manager/src/js/components/ItemList/ItemListHeader.js +++ b/dispatch/static/manager/src/js/components/ItemList/ItemListHeader.js @@ -21,7 +21,8 @@ export default function ItemListHeader(props) { const toolbarLeft = (
- props.actions.toggleAllItems(props.items.ids)} />
diff --git a/dispatch/static/manager/src/js/components/ItemList/ItemListSearchBar.js b/dispatch/static/manager/src/js/components/ItemList/ItemListSearchBar.js index fd1d33a08..f39b1069c 100644 --- a/dispatch/static/manager/src/js/components/ItemList/ItemListSearchBar.js +++ b/dispatch/static/manager/src/js/components/ItemList/ItemListSearchBar.js @@ -13,7 +13,7 @@ export default class ItemListSearchBar extends React.Component { this.handleSubmit = this.handleSubmit.bind(this) } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { this.setState({ query: nextProps.query || '' }) } diff --git a/dispatch/static/manager/src/js/components/ModalContainer.js b/dispatch/static/manager/src/js/components/ModalContainer.js index 7a09a6432..caa1238a7 100644 --- a/dispatch/static/manager/src/js/components/ModalContainer.js +++ b/dispatch/static/manager/src/js/components/ModalContainer.js @@ -5,7 +5,7 @@ require('../../styles/components/modal_container.scss') export default function ModalContainer(props) { return (
-
+
{props.children}
) diff --git a/dispatch/static/manager/src/js/components/PageEditor/PageContentEditor.js b/dispatch/static/manager/src/js/components/PageEditor/PageContentEditor.js index fd66859ab..3f9d5656b 100644 --- a/dispatch/static/manager/src/js/components/PageEditor/PageContentEditor.js +++ b/dispatch/static/manager/src/js/components/PageEditor/PageContentEditor.js @@ -18,7 +18,7 @@ const embeds = [ export default class PageContentEditor extends React.Component { render() { return ( -
+
- Basic fields - Featured image - Template - SEO + Basic fields + Featured image + Template + SEO diff --git a/dispatch/static/manager/src/js/components/PageEditor/PageToolbar.js b/dispatch/static/manager/src/js/components/PageEditor/PageToolbar.js index 28a74d9f4..361046342 100644 --- a/dispatch/static/manager/src/js/components/PageEditor/PageToolbar.js +++ b/dispatch/static/manager/src/js/components/PageEditor/PageToolbar.js @@ -30,7 +30,7 @@ export default function PageToolbar(props) { props.previewPage()}> - Preview + Preview props.update('slug', e.target.value) } /> + onChange={e => props.update('slug', e.target.value)} /> props.update('snippet', e.target.value) } /> + onChange={e => props.update('snippet', e.target.value)} />
diff --git a/dispatch/static/manager/src/js/components/PersonEditor/PersonForm.js b/dispatch/static/manager/src/js/components/PersonEditor/PersonForm.js index c2c900931..21dbca988 100644 --- a/dispatch/static/manager/src/js/components/PersonEditor/PersonForm.js +++ b/dispatch/static/manager/src/js/components/PersonEditor/PersonForm.js @@ -41,7 +41,7 @@ export default class PersonForm extends React.Component { placeholder='Full Name' value={this.props.listItem.full_name || ''} fill={true} - onChange={ e => this.props.update('full_name', e.target.value) } /> + onChange={e => this.props.update('full_name', e.target.value)} /> this.props.update('slug', e.target.value) } /> + onChange={e => this.props.update('slug', e.target.value)} /> this.props.update('facebook_url', e.target.value) } /> + onChange={e => this.props.update('facebook_url', e.target.value)} /> this.props.update('twitter_url', e.target.value) } /> + onChange={e => this.props.update('twitter_url', e.target.value)} /> this.props.update('description', e.target.value) } /> + onChange={e => this.props.update('description', e.target.value)} /> ) diff --git a/dispatch/static/manager/src/js/components/PlaceholderBar.js b/dispatch/static/manager/src/js/components/PlaceholderBar.js index 38b64ce8f..2e32ac556 100644 --- a/dispatch/static/manager/src/js/components/PlaceholderBar.js +++ b/dispatch/static/manager/src/js/components/PlaceholderBar.js @@ -4,6 +4,6 @@ require('../../styles/components/placeholder_bar.scss') export default function PlaceholderBar() { return ( -
+
) } diff --git a/dispatch/static/manager/src/js/components/PollEditor/Poll.js b/dispatch/static/manager/src/js/components/PollEditor/Poll.js index e873eb79b..392642e07 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/Poll.js +++ b/dispatch/static/manager/src/js/components/PollEditor/Poll.js @@ -17,9 +17,9 @@ class Poll extends Component { let answers = [] let votes = [] let pollQuestion = this.props.question ? this.props.question : 'Poll Question' - if(this.props.answers) { - for(let answer of this.props.answers) { - if(answer['name'] !== '') { + if (this.props.answers) { + for (let answer of this.props.answers) { + if (answer['name'] !== '') { answers.push(answer['name']) } else { answers.push('Answer') @@ -33,7 +33,7 @@ class Poll extends Component { let temp = votes.filter((item) => {return item === 0}) let noVotes = false - if(temp.length === votes.length) { + if (temp.length === votes.length) { //no votes yet, populate with dummy data for better poll visualization votes[0] = 2 votes[1] = 1 @@ -50,11 +50,11 @@ class Poll extends Component { } getPollResult(index, votes) { - if(this.state.showResults) { + if (this.state.showResults) { let width = 0 let total = votes.reduce((acc, val) => { return acc + val }) - if(total !== 0) { + if (total !== 0) { width = String((100*votes[index]/total).toFixed(0)) + '%' } @@ -82,27 +82,30 @@ class Poll extends Component {

{pollQuestion}

{answers.map((answer, index) => { - return( + return (
) diff --git a/dispatch/static/manager/src/js/components/SectionEditor/SectionForm.js b/dispatch/static/manager/src/js/components/SectionEditor/SectionForm.js index 984867c1c..b07d474de 100644 --- a/dispatch/static/manager/src/js/components/SectionEditor/SectionForm.js +++ b/dispatch/static/manager/src/js/components/SectionEditor/SectionForm.js @@ -15,7 +15,7 @@ export default function SectionForm(props) { placeholder='Name' value={props.listItem.name || ''} fill={true} - onChange={ e => props.update('name', e.target.value) } /> + onChange={e => props.update('name', e.target.value)} /> props.update('slug', e.target.value) } /> + onChange={e => props.update('slug', e.target.value)} /> diff --git a/dispatch/static/manager/src/js/components/TagEditor/TagForm.js b/dispatch/static/manager/src/js/components/TagEditor/TagForm.js index 1f2b4637b..b417742d5 100644 --- a/dispatch/static/manager/src/js/components/TagEditor/TagForm.js +++ b/dispatch/static/manager/src/js/components/TagEditor/TagForm.js @@ -14,7 +14,7 @@ export default function TagForm(props) { placeholder='Name' value={props.listItem.name || ''} fill={true} - onChange={ e => props.update('name', e.target.value) } /> + onChange={e => props.update('name', e.target.value)} /> ) diff --git a/dispatch/static/manager/src/js/components/TopicEditor/TopicForm.js b/dispatch/static/manager/src/js/components/TopicEditor/TopicForm.js index 29550b2f1..19067fc83 100644 --- a/dispatch/static/manager/src/js/components/TopicEditor/TopicForm.js +++ b/dispatch/static/manager/src/js/components/TopicEditor/TopicForm.js @@ -14,7 +14,7 @@ export default function TopicForm(props) { placeholder='Name' value={props.listItem.name || ''} fill={true} - onChange={ e => props.update('name', e.target.value) } /> + onChange={e => props.update('name', e.target.value)} /> ) diff --git a/dispatch/static/manager/src/js/components/VideoEditor/VideoForm.js b/dispatch/static/manager/src/js/components/VideoEditor/VideoForm.js index 5024c6a6d..1493fd7fa 100644 --- a/dispatch/static/manager/src/js/components/VideoEditor/VideoForm.js +++ b/dispatch/static/manager/src/js/components/VideoEditor/VideoForm.js @@ -15,7 +15,7 @@ export default function VideoForm(props) { placeholder='Title' value={props.listItem.title || ''} fill={true} - onChange={ e => props.update('title', e.target.value) } /> + onChange={e => props.update('title', e.target.value)} /> props.update('url', e.target.value) } /> + onChange={e => props.update('url', e.target.value)} /> diff --git a/dispatch/static/manager/src/js/components/WidgetFields.js b/dispatch/static/manager/src/js/components/WidgetFields.js index c9989bfca..7e5cb7c59 100644 --- a/dispatch/static/manager/src/js/components/WidgetFields.js +++ b/dispatch/static/manager/src/js/components/WidgetFields.js @@ -16,7 +16,7 @@ class WidgetFieldsComponent extends React.Component { return (
- {fields} + {fields}
) } diff --git a/dispatch/static/manager/src/js/components/ZoneEditor/index.js b/dispatch/static/manager/src/js/components/ZoneEditor/index.js index bd6cd96b9..fd9d360c2 100644 --- a/dispatch/static/manager/src/js/components/ZoneEditor/index.js +++ b/dispatch/static/manager/src/js/components/ZoneEditor/index.js @@ -14,7 +14,7 @@ import FieldGroup from '../fields/FieldGroup' class ZoneEditorComponent extends React.Component { - componentWillMount() { + componentDidMount() { this.props.getZone(this.props.token, this.props.zoneId) } diff --git a/dispatch/static/manager/src/js/components/fields/IntegerField.js b/dispatch/static/manager/src/js/components/fields/IntegerField.js index 191578052..64831b03c 100644 --- a/dispatch/static/manager/src/js/components/fields/IntegerField.js +++ b/dispatch/static/manager/src/js/components/fields/IntegerField.js @@ -6,6 +6,8 @@ export default class IntegerField extends React.Component { constructor(props) { super(props) + this.textInput = React.createRef() + this.state = { selectionStart: 0, selectionEnd: 0 @@ -13,7 +15,7 @@ export default class IntegerField extends React.Component { } onChange(val) { - let { selectionStart, selectionEnd } = this.refs.input.refs.input + let { selectionStart, selectionEnd } = this.textInput.current.input.current const initialLength = val.length val = val.replace(/[^\d-]+/g, '') @@ -36,7 +38,7 @@ export default class IntegerField extends React.Component { componentDidUpdate() { //put the caret back in the correct place - this.refs.input.refs.input.setSelectionRange( + this.textInput.current.input.current.setSelectionRange( this.state.selectionStart, this.state.selectionEnd) } @@ -44,7 +46,7 @@ export default class IntegerField extends React.Component { render() { return ( ( -// this.updateField(field.name, data)} /> -// )) : null -// -// if (fields) { -// fields = ( -// -// {fields} -// -// ) -// } -// ======= + // <<<<<<< HEAD + // let fields = widget ? widget.fields.map((field) => ( + // this.updateField(field.name, data)} /> + // )) : null + // + // if (fields) { + // fields = ( + // + // {fields} + // + // ) + // } + // ======= const fields = widget && widget.fields.length ? ( this.updateField(name, data)} /> ) : null -// >>>>>>> develop + // >>>>>>> develop return (
@@ -81,7 +81,7 @@ export default class WidgetFieldComponent extends React.Component { selected={this.getWidgetId()} update={widgetId => this.handleWidgetChange(widgetId)} />
- {fields} + {fields}
) } diff --git a/dispatch/static/manager/src/js/components/inputs/DateTimeInput.js b/dispatch/static/manager/src/js/components/inputs/DateTimeInput.js index be1cc097e..8428b23bd 100644 --- a/dispatch/static/manager/src/js/components/inputs/DateTimeInput.js +++ b/dispatch/static/manager/src/js/components/inputs/DateTimeInput.js @@ -12,7 +12,7 @@ function ensureDate(date) { let ret = date if (!(date instanceof Date)) { const time_ms = Date.parse(date) - if(isNaN(time_ms)) { + if (isNaN(time_ms)) { ret = null } else { ret = new Date(time_ms) diff --git a/dispatch/static/manager/src/js/components/inputs/ImageInput.js b/dispatch/static/manager/src/js/components/inputs/ImageInput.js index 8d24645d8..aeba9d135 100644 --- a/dispatch/static/manager/src/js/components/inputs/ImageInput.js +++ b/dispatch/static/manager/src/js/components/inputs/ImageInput.js @@ -17,7 +17,7 @@ function Image(props) {
- +
) @@ -90,11 +90,11 @@ class ImageInputComponent extends React.Component { onClick={() => this.removeImage(image.id)} /> )} /> this.chooseImage() }>{this.props.many ? 'Add image' : (this.props.selected ? 'Change image' : 'Select image')} - + onClick={() => this.chooseImage()}>{this.props.many ? 'Add image' : (this.props.selected ? 'Change image' : 'Select image')} + {(this.props.selected && this.props.removable) && this.removeImage() }>{'Remove image'} - } + onClick={() => this.removeImage()}>{'Remove image'} + }
) } diff --git a/dispatch/static/manager/src/js/components/inputs/TextInput.js b/dispatch/static/manager/src/js/components/inputs/TextInput.js index c1dc1026a..c62eb563f 100644 --- a/dispatch/static/manager/src/js/components/inputs/TextInput.js +++ b/dispatch/static/manager/src/js/components/inputs/TextInput.js @@ -7,6 +7,8 @@ export default class TextInput extends React.Component { this.handleOnChange = this.handleOnChange.bind(this) + this.input = React.createRef() + this.saveTimeout = null this.saveNextUpdate = false } @@ -30,13 +32,13 @@ export default class TextInput extends React.Component { } focus() { - this.refs.input.focus() + this.input.current.focus() } render() { return ( + editMessage='Filter by author'/> ) } } diff --git a/dispatch/static/manager/src/js/components/inputs/filters/TagsFilterInput.js b/dispatch/static/manager/src/js/components/inputs/filters/TagsFilterInput.js index 96e4ff20f..07b7a6682 100644 --- a/dispatch/static/manager/src/js/components/inputs/filters/TagsFilterInput.js +++ b/dispatch/static/manager/src/js/components/inputs/filters/TagsFilterInput.js @@ -29,8 +29,7 @@ class TagsFilterInputComponent extends React.Component { attribute='name' label='Tag' icon='tag' - editMessage='Filter by tag' - /> + editMessage='Filter by tag'/> ) } } diff --git a/dispatch/static/manager/src/js/components/inputs/selects/ItemSelectInput.js b/dispatch/static/manager/src/js/components/inputs/selects/ItemSelectInput.js index a6aacb511..f76bb6d47 100644 --- a/dispatch/static/manager/src/js/components/inputs/selects/ItemSelectInput.js +++ b/dispatch/static/manager/src/js/components/inputs/selects/ItemSelectInput.js @@ -13,9 +13,9 @@ function Item(props) {
  • - + {props.text} - +
  • ) } else { @@ -23,9 +23,9 @@ function Item(props) {
  • - + {props.text} - +
  • ) } @@ -36,6 +36,8 @@ class ItemSelectInput extends React.Component { constructor(props) { super(props) + this.dropdown = React.createRef() + this.state = { query: '' } @@ -87,13 +89,13 @@ class ItemSelectInput extends React.Component { } closeDropdown() { - this.refs.dropdown.close() + this.dropdown.current.close() this.setState({ query: '' }) } getSelected() { - if(this.props.many){ - if(this.props.selected){ + if (this.props.many){ + if (this.props.selected){ return typeof this.props.selected !== 'object' ? [this.props.selected] : this.props.selected } else { return [] @@ -124,8 +126,8 @@ class ItemSelectInput extends React.Component { isSelected={true} text={item[this.props.attribute]} onClick={() => this.removeValue(item.id)} /> - )) - + )) + const results = this.props.results .filter(id => this.isNotSelected(id)) .map(id => this.props.entities[id]) @@ -153,7 +155,7 @@ class ItemSelectInput extends React.Component { value={this.state.query} fill={true} placeholder='Search' /> - {createButton} + {createButton}
    @@ -229,7 +231,7 @@ class ItemSelectInput extends React.Component { className='c-input c-input--item-select'> {this.props.showSortableList ? this.renderSortableList() : null } {this.props.filterButton ? filterButton : anchorButton} diff --git a/dispatch/static/manager/src/js/components/integrations/FBInstantArticles.js b/dispatch/static/manager/src/js/components/integrations/FBInstantArticles.js deleted file mode 100644 index e3c321480..000000000 --- a/dispatch/static/manager/src/js/components/integrations/FBInstantArticles.js +++ /dev/null @@ -1,136 +0,0 @@ -import React from 'react' - -import { AnchorButton, Intent } from '@blueprintjs/core' - -import { FormInput, TextInput, SelectInput } from '../inputs' - -function fbLoginURI(clientId, redirectURI) { - return `https://www.facebook.com/v2.8/dialog/oauth?client_id=${clientId}&redirect_uri=${window.location.origin}/admin${redirectURI}?callback=1&scope=pages_manage_instant_articles,pages_show_list` -} - -export function InstantArticlesClientEditor(props) { - /* Renders a panel with fields to edit the Facebook client information */ - - const cancelButton = ( - - Cancel - - ) - - return ( -
    - - props.onChange({ client_id: e.target.value }) } /> - - - - props.onChange({ client_secret: e.target.value }) } /> - - - - - Save settings - - {props.editMode ? cancelButton : null} - -
    - ) -} - -export function InstantArticlesPageEditor(props) { - /* Renders a panel that either shows a Facebook login button or a dropdown list of pages */ - - const pages = - - const login = - - return ( -
    - - - - - Change app information - - - - {props.pages ? pages : login} - -
    - ) -} - -export function InstantArticlesEnabledEditor(props) { - /* Renders a panel that shows the currently enabled Facebook Page, with option to remove */ - - return ( - -
    - - Remove integration - -
    - ) -} - -function InstanceArticlesLogin(props) { - /* Renders a button that opens a Facebook login dialog */ - - return ( - -
    - Authenticate with Facebook -
    - ) -} - -function InstantArticlesPageDropdown(props) { - /* Renders a dropdown list to select a Facebook page */ - - const pageMap = props.pages.reduce((pageMap, page) => { - pageMap[page.id] = page - return pageMap - }, {}) - - const options = props.pages.map(page => [page.id, page.name]) - - return ( - - - props.onChange(pageMap[e.target.value])} /> - - Enable Instant Articles - - - - ) -} diff --git a/dispatch/static/manager/src/js/components/modals/ImageManager/ImagePanel.js b/dispatch/static/manager/src/js/components/modals/ImageManager/ImagePanel.js index 4b63c08b2..d67609098 100644 --- a/dispatch/static/manager/src/js/components/modals/ImageManager/ImagePanel.js +++ b/dispatch/static/manager/src/js/components/modals/ImageManager/ImagePanel.js @@ -39,7 +39,7 @@ export default function ImagePanel(props) { props.update('tags', tags) } /> + update={tags => props.update('tags', tags)} />
    diff --git a/dispatch/static/manager/src/js/components/modals/ImageManager/ImageThumb.js b/dispatch/static/manager/src/js/components/modals/ImageManager/ImageThumb.js index b2dff5be0..9b9941107 100644 --- a/dispatch/static/manager/src/js/components/modals/ImageManager/ImageThumb.js +++ b/dispatch/static/manager/src/js/components/modals/ImageManager/ImageThumb.js @@ -14,12 +14,11 @@ export default function ImageThumb(props) { return (
    + style={props.width ? { width: props.width} : {}}>
    props.selectImage(props.image.id)} - style={style}>
    + style={style} />
    ) } diff --git a/dispatch/static/manager/src/js/components/modals/ImageManager/index.js b/dispatch/static/manager/src/js/components/modals/ImageManager/index.js index 15aadd7e5..d1e6be318 100644 --- a/dispatch/static/manager/src/js/components/modals/ImageManager/index.js +++ b/dispatch/static/manager/src/js/components/modals/ImageManager/index.js @@ -44,7 +44,7 @@ class ImageManagerComponent extends React.Component { } loadMore() { - if(this.props.images.count > this.state.limit){ + if (this.props.images.count > this.state.limit){ this.setState(prevState => ({ limit: prevState.limit + 10 }), this.props.listImages(this.props.token, {limit: this.state.limit, ordering: '-created_at'})) @@ -150,13 +150,13 @@ class ImageManagerComponent extends React.Component { ref={(node) => { this.images = node }}>{images}
    {!this.props.many ? -
    - {image ? imagePanel : null} -
    : null} +
    + {image ? imagePanel : null} +
    : null}
    -
    +
    this.insertImage()}>Insert diff --git a/dispatch/static/manager/src/js/containers/AppContainer.js b/dispatch/static/manager/src/js/containers/AppContainer.js index 714f3aac6..aa1fb83ff 100644 --- a/dispatch/static/manager/src/js/containers/AppContainer.js +++ b/dispatch/static/manager/src/js/containers/AppContainer.js @@ -5,7 +5,7 @@ import * as userActions from '../actions/UserActions' class App extends React.Component { - componentWillMount() { + componentDidMount() { this.checkAuthenticated() } diff --git a/dispatch/static/manager/src/js/containers/BasicContainer.js b/dispatch/static/manager/src/js/containers/BasicContainer.js index 1836c8fbc..0c3058cc6 100644 --- a/dispatch/static/manager/src/js/containers/BasicContainer.js +++ b/dispatch/static/manager/src/js/containers/BasicContainer.js @@ -1,5 +1,5 @@ import React from 'react' export default function BasicContainer(props) { - return (
    {props.children}
    ) + return
    {props.children}
    } diff --git a/dispatch/static/manager/src/js/containers/MainContainer.js b/dispatch/static/manager/src/js/containers/MainContainer.js index 71de1a5b8..aeb4aed46 100644 --- a/dispatch/static/manager/src/js/containers/MainContainer.js +++ b/dispatch/static/manager/src/js/containers/MainContainer.js @@ -13,12 +13,18 @@ require('../../styles/components/toaster.scss') class Main extends React.Component { - componentWillMount() { - //this.props.countPending(this.props.token, { pending: 1, limit: 0 }) + // UNSAFE_componentWillMount() { + // this.props.countPending(this.props.token, { pending: 1, limit: 0 }) + // } + + constructor(props) { + super(props) + + this.toaster = React.createRef() } componentDidMount() { - this.props.setupToaster(this.refs.toaster) + this.props.setupToaster(this.toaster.current) } renderModal() { @@ -32,7 +38,7 @@ class Main extends React.Component { render() { return (
    - +
    {this.props.children} {this.props.modal.component ? this.renderModal() : null} diff --git a/dispatch/static/manager/src/js/index.js b/dispatch/static/manager/src/js/index.js index 1ced02066..263d804ac 100644 --- a/dispatch/static/manager/src/js/index.js +++ b/dispatch/static/manager/src/js/index.js @@ -45,10 +45,6 @@ render(( - - - - diff --git a/dispatch/static/manager/src/js/pages/Articles/ArticleIndexPage.js b/dispatch/static/manager/src/js/pages/Articles/ArticleIndexPage.js index bf9e62e5d..07ca9b413 100644 --- a/dispatch/static/manager/src/js/pages/Articles/ArticleIndexPage.js +++ b/dispatch/static/manager/src/js/pages/Articles/ArticleIndexPage.js @@ -53,13 +53,11 @@ function ArticlePageComponent(props) { props.searchArticles(props.location.query.author, section, props.location.query.q)} - />, + update={(section) => props.searchArticles(props.location.query.author, section, props.location.query.q)}/>, props.searchArticles(author, props.location.query.section, props.location.query.q)} - /> + update={(author) => props.searchArticles(author, props.location.query.section, props.location.query.q)}/> ] return ( diff --git a/dispatch/static/manager/src/js/pages/DashboardPage.js b/dispatch/static/manager/src/js/pages/DashboardPage.js index a4d8d2147..7ba928fb1 100644 --- a/dispatch/static/manager/src/js/pages/DashboardPage.js +++ b/dispatch/static/manager/src/js/pages/DashboardPage.js @@ -14,7 +14,7 @@ require('../../styles/components/dashboard.scss') class DashboardPageComponent extends React.Component { - componentWillMount() { + UNSAFE_componentWillMount() { this.props.fetchActions(this.props.token) this.props.fetchRecent(this.props.token) } @@ -44,7 +44,7 @@ class DashboardPageComponent extends React.Component { ) - ) + ) } render() { @@ -61,12 +61,12 @@ class DashboardPageComponent extends React.Component {
    • - New Article + New Article
    • - New Page + New Page
    diff --git a/dispatch/static/manager/src/js/pages/Events/EventAuditPage.js b/dispatch/static/manager/src/js/pages/Events/EventAuditPage.js index e2a24bb60..4f46ef52c 100644 --- a/dispatch/static/manager/src/js/pages/Events/EventAuditPage.js +++ b/dispatch/static/manager/src/js/pages/Events/EventAuditPage.js @@ -43,15 +43,15 @@ class EventAuditPage extends React.Component { } } - componentWillMount() { + UNSAFE_componentWillMount() { this.props.listEvents(this.props.token, this.getQuery()) } getDisplayedCount(props) { props = props || this.props return props.events.ids - .map(id => props.entities.events[id]) - .filter(event => event && event.is_submission).length + .map(id => props.entities.events[id]) + .filter(event => event && event.is_submission).length } checkPage() { @@ -108,18 +108,18 @@ class EventAuditPage extends React.Component { this.props.history.goBack()}> - Back + onClick={() => this.props.router.goBack()}> + Back {this.props.events.count} event{this.props.events.count == 1 ? '' : 's'} pending approval - {this.props.events.count ? pagination : null} + {this.props.events.count ? pagination : null}
    - {events} + {events}
    diff --git a/dispatch/static/manager/src/js/pages/Events/EventPage.js b/dispatch/static/manager/src/js/pages/Events/EventPage.js index df5f4cfb4..1d63e7154 100644 --- a/dispatch/static/manager/src/js/pages/Events/EventPage.js +++ b/dispatch/static/manager/src/js/pages/Events/EventPage.js @@ -6,6 +6,6 @@ export default function EventPage(props) { return ( + goBack={props.router.goBack} /> ) } diff --git a/dispatch/static/manager/src/js/pages/Events/NewEventPage.js b/dispatch/static/manager/src/js/pages/Events/NewEventPage.js index 47db5889d..adfe30967 100644 --- a/dispatch/static/manager/src/js/pages/Events/NewEventPage.js +++ b/dispatch/static/manager/src/js/pages/Events/NewEventPage.js @@ -6,6 +6,6 @@ export default function NewEventPage(props) { return ( + goBack={props.router.goBack} /> ) } diff --git a/dispatch/static/manager/src/js/pages/FilesPage.js b/dispatch/static/manager/src/js/pages/FilesPage.js index 61992a4b1..47077e8aa 100644 --- a/dispatch/static/manager/src/js/pages/FilesPage.js +++ b/dispatch/static/manager/src/js/pages/FilesPage.js @@ -14,7 +14,7 @@ const DEFAULT_LIMIT = 15 class FilesPageComponent extends React.Component { - componentWillMount() { + UNSAFE_componentWillMount() { this.props.clearAllFiles() this.props.clearSelectedFiles() this.props.listFiles(this.props.token, this.getQuery()) @@ -109,7 +109,10 @@ class FilesPageComponent extends React.Component { headers={['Filename', 'Created', 'Updated']} columns={[ - item => ({item.name}), + item => ({item.name}), item => humanizeDatetime(item.created_at), item => humanizeDatetime(item.updated_at), ]} @@ -122,8 +125,7 @@ class FilesPageComponent extends React.Component { toggleAllItems: this.props.toggleAllFiles, deleteItems: (fileIds) => this.handleDeleteFiles(fileIds), searchItems: (query) => this.handleSearchFiles(query) - }} - /> + }}/>
    this.onDropzoneClick()}>

    Drag files into window or click here to upload

    diff --git a/dispatch/static/manager/src/js/pages/Galleries/GalleryPage.js b/dispatch/static/manager/src/js/pages/Galleries/GalleryPage.js index fc2faba21..207d541fd 100644 --- a/dispatch/static/manager/src/js/pages/Galleries/GalleryPage.js +++ b/dispatch/static/manager/src/js/pages/Galleries/GalleryPage.js @@ -6,7 +6,7 @@ export default function TagPage(props) { return ( ) } diff --git a/dispatch/static/manager/src/js/pages/Galleries/NewGalleryPage.js b/dispatch/static/manager/src/js/pages/Galleries/NewGalleryPage.js index c387ae062..c3f71fba2 100644 --- a/dispatch/static/manager/src/js/pages/Galleries/NewGalleryPage.js +++ b/dispatch/static/manager/src/js/pages/Galleries/NewGalleryPage.js @@ -6,7 +6,7 @@ export default function NewTagPage(props) { return ( ) } diff --git a/dispatch/static/manager/src/js/pages/Images/EditImagePage.js b/dispatch/static/manager/src/js/pages/Images/EditImagePage.js index ae0394f32..bb1915318 100644 --- a/dispatch/static/manager/src/js/pages/Images/EditImagePage.js +++ b/dispatch/static/manager/src/js/pages/Images/EditImagePage.js @@ -7,7 +7,7 @@ export default function EditImagePage(props) { return ( ) } diff --git a/dispatch/static/manager/src/js/pages/Images/ImagesIndexPage.js b/dispatch/static/manager/src/js/pages/Images/ImagesIndexPage.js index e1d81efe0..9d9469c56 100644 --- a/dispatch/static/manager/src/js/pages/Images/ImagesIndexPage.js +++ b/dispatch/static/manager/src/js/pages/Images/ImagesIndexPage.js @@ -20,7 +20,7 @@ const DEFAULT_LIMIT = 15 class ImagesPageComponent extends React.Component { - componentWillMount() { + UNSAFE_componentWillMount() { this.props.clearAllImages() this.props.clearSelectedImages() this.props.listImages(this.props.token, this.getQuery()) @@ -45,7 +45,7 @@ class ImagesPageComponent extends React.Component { offset: (this.getCurrentPage() - 1) * DEFAULT_LIMIT } - + if (this.props.location.query.author) { query.author = this.props.location.query.author } @@ -100,10 +100,8 @@ class ImagesPageComponent extends React.Component { } renderThumb(url) { - return( -
    - -
    + return ( +
    ) } @@ -131,18 +129,16 @@ class ImagesPageComponent extends React.Component { item => humanizeDatetime(item.created_at, true), item => humanizeDatetime(item.updated_at, true) ]) - + const filters = [ this.props.searchImages(author, this.props.location.query.tags, this.props.location.query.q)} - />, + update={(author) => this.props.searchImages(author, this.props.location.query.tags, this.props.location.query.q)}/>, this.props.searchImages(this.props.location.query.author, tags, this.props.location.query.q)} - /> + update={(tags) => this.props.searchImages(this.props.location.query.author, tags, this.props.location.query.q)}/> ] return ( @@ -180,8 +176,7 @@ class ImagesPageComponent extends React.Component { toggleAllItems: this.props.toggleAllImages, deleteItems: (imageIds) => this.handleDeleteImages(imageIds), searchItems: (query) => this.handleSearchImages(query) - }} - /> + }}/>
    this.onDropzoneClick()}>

    Drag images into window or click here to upload

    diff --git a/dispatch/static/manager/src/js/pages/Integrations/FBInstantArticlesIntegrationPage.js b/dispatch/static/manager/src/js/pages/Integrations/FBInstantArticlesIntegrationPage.js deleted file mode 100644 index 87fbe602e..000000000 --- a/dispatch/static/manager/src/js/pages/Integrations/FBInstantArticlesIntegrationPage.js +++ /dev/null @@ -1,191 +0,0 @@ -import React from 'react' -import R from 'ramda' -import { connect } from 'react-redux' -import { push } from 'react-router-redux' -import DocumentTitle from 'react-document-title' - -import Panel from '../../components/Panel' - -import { - InstantArticlesClientEditor, - InstantArticlesPageEditor, - InstantArticlesEnabledEditor -} from '../../components/integrations/FBInstantArticles' - -import * as integrationActions from '../../actions/IntegrationActions' - -const INTEGRATION_ID = 'fb-instant-articles' - -class FBInstantArticlesIntegrationPageComponent extends React.Component { - - constructor(props) { - super(props) - - this.cachedSettings = null - - this.state = { - editMode: false - } - } - - componentWillMount() { - - this.props.fetchIntegration(this.props.token, INTEGRATION_ID) - - if (this.props.location.query.callback) { - this.props.integrationCallback( - this.props.token, - INTEGRATION_ID, - this.props.location.query - ) - } - - } - - componentWillReceiveProps(nextProps) { - - if (R.path(['integration', 'settings', 'client_configured'], nextProps)) { - this.setState({ editMode: false }) - } - - } - - enterEditMode() { - // Temporarily save old settings so we can revert - this.cachedSettings = this.props.integration.settings - - this.updateSettings({ client_configured: false }) - this.setState({ editMode: true }) - } - - exitEditMode() { - // Revert old settings - this.updateSettings(this.cachedSettings) - - this.setState({ editMode: false }) - } - - updateSettings(settings) { - return this.props.updateIntegration( - INTEGRATION_ID, - { settings: R.merge(this.props.integration.settings, settings) } - ) - } - - updateFacebookPage(page) { - return this.updateSettings({ - page_id: page.id, - page_name: page.name, - page_access_token: page.access_token - }) - } - - componentDidUpdate() { - - if (!this.props.integration.settings.page_id && R.path(['callback', 'pages', 'data'], this.props.integration)) { - const page = this.props.integration.callback.pages.data[0] - this.updateFacebookPage(page) - this.props.resetURI(this.props.location.pathname) - } - - } - - renderFacebookInstantArticles() { - - const settings = this.props.integration.settings - const callback = this.props.integration.callback || {} - - if (settings.page_configured) { - return ( - this.props.deleteIntegration(this.props.token, INTEGRATION_ID)} /> - ) - } - - if (settings.client_configured && !this.state.editMode) { - return ( - this.updateFacebookPage(data)} - onSave={() => this.props.saveIntegration(this.props.token, INTEGRATION_ID, this.props.integration)} - enterEditMode={() => this.enterEditMode()} /> - ) - } - - return ( - this.updateSettings(data)} - onSave={() => this.props.saveIntegration(this.props.token, INTEGRATION_ID, this.props.integration)} - editMode={this.state.editMode} - exitEditMode={() => this.exitEditMode()}/> - ) - } - - render() { - - if (this.props.integration) { - return ( - - - {this.renderFacebookInstantArticles()} - - - ) - } - - return ( -
    Loading
    - ) - - } - -} - -const mapStateToProps = (state) => { - - let integration = state.app.integrations.integrations[INTEGRATION_ID] || null - - if (integration) { - integration.settings = integration.settings || {} - } - - return { - token: state.app.auth.token, - integration: integration - } -} - -const mapDispatchToProps = (dispatch) => { - return { - integrationCallback: (token, integrationId, query) => { - dispatch(integrationActions.integrationCallback(token, integrationId, query)) - }, - fetchIntegration: (token, integrationId) => { - dispatch(integrationActions.fetchIntegration(token, integrationId)) - }, - saveIntegration: (token, integrationId, data) => { - dispatch(integrationActions.saveIntegration(token, integrationId, data)) - }, - deleteIntegration: (token, integrationId) => { - dispatch(integrationActions.deleteIntegration(token, integrationId)) - }, - updateIntegration: (integrationId, data) => { - dispatch(integrationActions.updateIntegration(integrationId, data)) - }, - resetURI: (pathname) => { - dispatch(push(pathname)) - } - } -} - -const FBInstantArticlesIntegrationPage = connect( - mapStateToProps, - mapDispatchToProps -)(FBInstantArticlesIntegrationPageComponent) - -export default FBInstantArticlesIntegrationPage diff --git a/dispatch/static/manager/src/js/pages/Integrations/IntegrationsIndexPage.js b/dispatch/static/manager/src/js/pages/Integrations/IntegrationsIndexPage.js deleted file mode 100644 index 8ac83cdf7..000000000 --- a/dispatch/static/manager/src/js/pages/Integrations/IntegrationsIndexPage.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import DocumentTitle from 'react-document-title' - -import SidebarNav from '../../components/SidebarNav' - -const NAV_ITEMS = [ - { path: '/integrations/fb-instant-articles/', label: 'Facebook Instant Articles' } -] - -export default function IntegrationsIndexPage(props) { - return ( - -
    - -
    {props.children}
    -
    -
    - ) -} diff --git a/dispatch/static/manager/src/js/pages/Integrations/index.js b/dispatch/static/manager/src/js/pages/Integrations/index.js deleted file mode 100644 index 47f7247b9..000000000 --- a/dispatch/static/manager/src/js/pages/Integrations/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import IntegrationsIndexPage from './IntegrationsIndexPage' -import FBInstantArticlesIntegrationPage from './FBInstantArticlesIntegrationPage' - -export { - IntegrationsIndexPage as Index, - FBInstantArticlesIntegrationPage as FBInstantArticles -} diff --git a/dispatch/static/manager/src/js/pages/Issues/IssuePage.js b/dispatch/static/manager/src/js/pages/Issues/IssuePage.js index 1362fc881..1fd8f6d52 100644 --- a/dispatch/static/manager/src/js/pages/Issues/IssuePage.js +++ b/dispatch/static/manager/src/js/pages/Issues/IssuePage.js @@ -6,7 +6,7 @@ export default function IssuePage(props) { return ( ) } diff --git a/dispatch/static/manager/src/js/pages/Issues/NewIssuePage.js b/dispatch/static/manager/src/js/pages/Issues/NewIssuePage.js index 9bf504a1f..e26424605 100644 --- a/dispatch/static/manager/src/js/pages/Issues/NewIssuePage.js +++ b/dispatch/static/manager/src/js/pages/Issues/NewIssuePage.js @@ -6,7 +6,7 @@ export default function NewIssuePage(props) { return ( ) } diff --git a/dispatch/static/manager/src/js/pages/ItemIndexPage.js b/dispatch/static/manager/src/js/pages/ItemIndexPage.js index f13793bf8..2de4c0910 100644 --- a/dispatch/static/manager/src/js/pages/ItemIndexPage.js +++ b/dispatch/static/manager/src/js/pages/ItemIndexPage.js @@ -45,7 +45,7 @@ export default class ListItemsPageComponent extends React.Component { ) } - componentWillMount() { + UNSAFE_componentWillMount() { // Fetch listItems this.props.clearListItems() this.props.clearSelectedListItems() @@ -121,7 +121,7 @@ export default class ListItemsPageComponent extends React.Component { emptyMessage={`You haven\'t created any ${this.props.typePlural} yet.`} createHandler={() => ( - Create {this.typeString} + Create {this.typeString} ) } diff --git a/dispatch/static/manager/src/js/pages/Pages/EditPagePage.js b/dispatch/static/manager/src/js/pages/Pages/EditPagePage.js index e530c42bf..6632780a1 100644 --- a/dispatch/static/manager/src/js/pages/Pages/EditPagePage.js +++ b/dispatch/static/manager/src/js/pages/Pages/EditPagePage.js @@ -6,7 +6,7 @@ export default function EditPagePage(props) { return ( ) diff --git a/dispatch/static/manager/src/js/pages/Pages/NewPagePage.js b/dispatch/static/manager/src/js/pages/Pages/NewPagePage.js index f6af72df9..fc92610b6 100644 --- a/dispatch/static/manager/src/js/pages/Pages/NewPagePage.js +++ b/dispatch/static/manager/src/js/pages/Pages/NewPagePage.js @@ -6,7 +6,7 @@ export default function NewPagePage(props) { return ( ) } diff --git a/dispatch/static/manager/src/js/pages/Persons/NewPersonPage.js b/dispatch/static/manager/src/js/pages/Persons/NewPersonPage.js index 86ea26626..715a8a30d 100644 --- a/dispatch/static/manager/src/js/pages/Persons/NewPersonPage.js +++ b/dispatch/static/manager/src/js/pages/Persons/NewPersonPage.js @@ -6,7 +6,7 @@ export default function NewPersonPage(props) { return ( ) } diff --git a/dispatch/static/manager/src/js/pages/Persons/PersonPage.js b/dispatch/static/manager/src/js/pages/Persons/PersonPage.js index 32f13be27..3ba972fd7 100644 --- a/dispatch/static/manager/src/js/pages/Persons/PersonPage.js +++ b/dispatch/static/manager/src/js/pages/Persons/PersonPage.js @@ -6,7 +6,7 @@ export default function PersonPage(props) { return ( ) } diff --git a/dispatch/static/manager/src/js/pages/Polls/NewPollPage.js b/dispatch/static/manager/src/js/pages/Polls/NewPollPage.js index 32b12c227..6c4fb6216 100644 --- a/dispatch/static/manager/src/js/pages/Polls/NewPollPage.js +++ b/dispatch/static/manager/src/js/pages/Polls/NewPollPage.js @@ -6,7 +6,7 @@ export default function NewPollPage(props) { return ( ) } diff --git a/dispatch/static/manager/src/js/pages/Polls/PollPage.js b/dispatch/static/manager/src/js/pages/Polls/PollPage.js index c7defd2a4..7c60ae769 100644 --- a/dispatch/static/manager/src/js/pages/Polls/PollPage.js +++ b/dispatch/static/manager/src/js/pages/Polls/PollPage.js @@ -6,7 +6,7 @@ export default function PollPage(props) { return ( ) } diff --git a/dispatch/static/manager/src/js/pages/ProfilePage.js b/dispatch/static/manager/src/js/pages/ProfilePage.js index 20248397e..82410b753 100644 --- a/dispatch/static/manager/src/js/pages/ProfilePage.js +++ b/dispatch/static/manager/src/js/pages/ProfilePage.js @@ -50,7 +50,7 @@ class ProfilePageComponent extends React.Component { const personEditor = personId ? ( ) : null @@ -66,7 +66,7 @@ class ProfilePageComponent extends React.Component { placeholder='name@domain.tld' value={user.email || ''} fill={true} - onChange={ e => this.handleUpdate('email', e.target.value) } /> + onChange={e => this.handleUpdate('email', e.target.value)} /> this.handleUpdate('password_a', e.target.value) } /> + onChange={e => this.handleUpdate('password_a', e.target.value)} /> this.handleUpdate('password_b', e.target.value) } /> + onChange={e => this.handleUpdate('password_b', e.target.value)} />
    {noVotes && No votes yet, poll data above is for visualization purposes only!} -
    +
    ) @@ -141,7 +141,7 @@ export default class PollForm extends React.Component { this.props.update('show_results', e.target.value)}/> + onChange={e => this.props.update('show_results', e.target.value)} />
    {(this.props.listItem.id === 'new') ? null : this.renderPollOpenSelect()} @@ -184,7 +184,7 @@ export default class PollForm extends React.Component { many={false} id={this.props.listItem.id} answers={this.props.listItem.answers} - question={this.props.listItem.question}/> + question={this.props.listItem.question} />
    ) diff --git a/dispatch/static/manager/src/js/components/fields/IntegerField.js b/dispatch/static/manager/src/js/components/fields/IntegerField.js index 64831b03c..6af753823 100644 --- a/dispatch/static/manager/src/js/components/fields/IntegerField.js +++ b/dispatch/static/manager/src/js/components/fields/IntegerField.js @@ -6,8 +6,6 @@ export default class IntegerField extends React.Component { constructor(props) { super(props) - this.textInput = React.createRef() - this.state = { selectionStart: 0, selectionEnd: 0 @@ -15,7 +13,7 @@ export default class IntegerField extends React.Component { } onChange(val) { - let { selectionStart, selectionEnd } = this.textInput.current.input.current + let { selectionStart, selectionEnd } = this.refs.textInput.refs.input const initialLength = val.length val = val.replace(/[^\d-]+/g, '') @@ -38,7 +36,7 @@ export default class IntegerField extends React.Component { componentDidUpdate() { //put the caret back in the correct place - this.textInput.current.input.current.setSelectionRange( + this.refs.textInput.refs.input.setSelectionRange( this.state.selectionStart, this.state.selectionEnd) } @@ -46,7 +44,7 @@ export default class IntegerField extends React.Component { render() { return ( + editMessage='Filter by author' /> ) } } diff --git a/dispatch/static/manager/src/js/components/inputs/filters/TagsFilterInput.js b/dispatch/static/manager/src/js/components/inputs/filters/TagsFilterInput.js index 07b7a6682..cf1943bdc 100644 --- a/dispatch/static/manager/src/js/components/inputs/filters/TagsFilterInput.js +++ b/dispatch/static/manager/src/js/components/inputs/filters/TagsFilterInput.js @@ -29,7 +29,7 @@ class TagsFilterInputComponent extends React.Component { attribute='name' label='Tag' icon='tag' - editMessage='Filter by tag'/> + editMessage='Filter by tag' /> ) } } diff --git a/dispatch/static/manager/src/js/components/inputs/selects/ItemSelectInput.js b/dispatch/static/manager/src/js/components/inputs/selects/ItemSelectInput.js index f76bb6d47..322b395e5 100644 --- a/dispatch/static/manager/src/js/components/inputs/selects/ItemSelectInput.js +++ b/dispatch/static/manager/src/js/components/inputs/selects/ItemSelectInput.js @@ -36,8 +36,6 @@ class ItemSelectInput extends React.Component { constructor(props) { super(props) - this.dropdown = React.createRef() - this.state = { query: '' } @@ -89,7 +87,7 @@ class ItemSelectInput extends React.Component { } closeDropdown() { - this.dropdown.current.close() + this.refs.dropdown.close() this.setState({ query: '' }) } @@ -205,7 +203,7 @@ class ItemSelectInput extends React.Component { render() { const anchorButton = ( - this.dropdown.current.open()}> + this.refs.dropdown.open()}> {this.props.editMessage} ) @@ -218,7 +216,7 @@ class ItemSelectInput extends React.Component { @@ -231,7 +229,7 @@ class ItemSelectInput extends React.Component { className='c-input c-input--item-select'> {this.props.showSortableList ? this.renderSortableList() : null } {this.props.filterButton ? filterButton : anchorButton} diff --git a/dispatch/static/manager/src/js/containers/MainContainer.js b/dispatch/static/manager/src/js/containers/MainContainer.js index aeb4aed46..0614c8774 100644 --- a/dispatch/static/manager/src/js/containers/MainContainer.js +++ b/dispatch/static/manager/src/js/containers/MainContainer.js @@ -13,18 +13,12 @@ require('../../styles/components/toaster.scss') class Main extends React.Component { - // UNSAFE_componentWillMount() { + // componentWillMount() { // this.props.countPending(this.props.token, { pending: 1, limit: 0 }) // } - constructor(props) { - super(props) - - this.toaster = React.createRef() - } - componentDidMount() { - this.props.setupToaster(this.toaster.current) + this.props.setupToaster(this.refs.toaster) } renderModal() { @@ -38,7 +32,10 @@ class Main extends React.Component { render() { return (
    - +
    {this.props.children} {this.props.modal.component ? this.renderModal() : null} diff --git a/dispatch/static/manager/src/js/pages/Articles/ArticleIndexPage.js b/dispatch/static/manager/src/js/pages/Articles/ArticleIndexPage.js index 07ca9b413..6f15428cd 100644 --- a/dispatch/static/manager/src/js/pages/Articles/ArticleIndexPage.js +++ b/dispatch/static/manager/src/js/pages/Articles/ArticleIndexPage.js @@ -53,11 +53,11 @@ function ArticlePageComponent(props) { props.searchArticles(props.location.query.author, section, props.location.query.q)}/>, + update={(section) => props.searchArticles(props.location.query.author, section, props.location.query.q)} />, props.searchArticles(author, props.location.query.section, props.location.query.q)}/> + update={(author) => props.searchArticles(author, props.location.query.section, props.location.query.q)} /> ] return ( diff --git a/dispatch/static/manager/src/js/pages/DashboardPage.js b/dispatch/static/manager/src/js/pages/DashboardPage.js index 7ba928fb1..b500ce0b6 100644 --- a/dispatch/static/manager/src/js/pages/DashboardPage.js +++ b/dispatch/static/manager/src/js/pages/DashboardPage.js @@ -14,7 +14,7 @@ require('../../styles/components/dashboard.scss') class DashboardPageComponent extends React.Component { - UNSAFE_componentWillMount() { + componentWillMount() { this.props.fetchActions(this.props.token) this.props.fetchRecent(this.props.token) } @@ -29,7 +29,7 @@ class DashboardPageComponent extends React.Component {
    {elem.meta.author} {elem.meta.count == 1 ? ` ${elem.meta.action} ` : ` made ${elem.meta.count} ${elem.meta.action} to ` } - + {` ${moment(elem.timestamp).from(moment())}`}
    diff --git a/dispatch/static/manager/src/js/pages/Events/EventAuditPage.js b/dispatch/static/manager/src/js/pages/Events/EventAuditPage.js index 4f46ef52c..b6e367273 100644 --- a/dispatch/static/manager/src/js/pages/Events/EventAuditPage.js +++ b/dispatch/static/manager/src/js/pages/Events/EventAuditPage.js @@ -43,7 +43,7 @@ class EventAuditPage extends React.Component { } } - UNSAFE_componentWillMount() { + componentWillMount() { this.props.listEvents(this.props.token, this.getQuery()) } diff --git a/dispatch/static/manager/src/js/pages/FilesPage.js b/dispatch/static/manager/src/js/pages/FilesPage.js index 47077e8aa..b3247e92c 100644 --- a/dispatch/static/manager/src/js/pages/FilesPage.js +++ b/dispatch/static/manager/src/js/pages/FilesPage.js @@ -14,7 +14,7 @@ const DEFAULT_LIMIT = 15 class FilesPageComponent extends React.Component { - UNSAFE_componentWillMount() { + componentWillMount() { this.props.clearAllFiles() this.props.clearSelectedFiles() this.props.listFiles(this.props.token, this.getQuery()) @@ -125,7 +125,7 @@ class FilesPageComponent extends React.Component { toggleAllItems: this.props.toggleAllFiles, deleteItems: (fileIds) => this.handleDeleteFiles(fileIds), searchItems: (query) => this.handleSearchFiles(query) - }}/> + }} />
    this.onDropzoneClick()}>

    Drag files into window or click here to upload

    diff --git a/dispatch/static/manager/src/js/pages/Images/ImagesIndexPage.js b/dispatch/static/manager/src/js/pages/Images/ImagesIndexPage.js index 9d9469c56..eb6b8468a 100644 --- a/dispatch/static/manager/src/js/pages/Images/ImagesIndexPage.js +++ b/dispatch/static/manager/src/js/pages/Images/ImagesIndexPage.js @@ -20,7 +20,7 @@ const DEFAULT_LIMIT = 15 class ImagesPageComponent extends React.Component { - UNSAFE_componentWillMount() { + componentWillMount() { this.props.clearAllImages() this.props.clearSelectedImages() this.props.listImages(this.props.token, this.getQuery()) @@ -134,11 +134,11 @@ class ImagesPageComponent extends React.Component { this.props.searchImages(author, this.props.location.query.tags, this.props.location.query.q)}/>, + update={(author) => this.props.searchImages(author, this.props.location.query.tags, this.props.location.query.q)} />, this.props.searchImages(this.props.location.query.author, tags, this.props.location.query.q)}/> + update={(tags) => this.props.searchImages(this.props.location.query.author, tags, this.props.location.query.q)} /> ] return ( @@ -176,7 +176,7 @@ class ImagesPageComponent extends React.Component { toggleAllItems: this.props.toggleAllImages, deleteItems: (imageIds) => this.handleDeleteImages(imageIds), searchItems: (query) => this.handleSearchImages(query) - }}/> + }} />
    this.onDropzoneClick()}>

    Drag images into window or click here to upload

    diff --git a/dispatch/static/manager/src/js/pages/ItemIndexPage.js b/dispatch/static/manager/src/js/pages/ItemIndexPage.js index 2de4c0910..f671f193d 100644 --- a/dispatch/static/manager/src/js/pages/ItemIndexPage.js +++ b/dispatch/static/manager/src/js/pages/ItemIndexPage.js @@ -45,7 +45,7 @@ export default class ListItemsPageComponent extends React.Component { ) } - UNSAFE_componentWillMount() { + componentWillMount() { // Fetch listItems this.props.clearListItems() this.props.clearSelectedListItems() diff --git a/dispatch/static/manager/src/js/pages/LoginPage.js b/dispatch/static/manager/src/js/pages/LoginPage.js index 7ba2eadff..c2af928ef 100644 --- a/dispatch/static/manager/src/js/pages/LoginPage.js +++ b/dispatch/static/manager/src/js/pages/LoginPage.js @@ -47,14 +47,14 @@ class LoginPageComponent extends React.Component { type="email" placeholder='Email' value={this.state.email} - onChange={this.onChangeEmail} />
    + onChange={this.onChangeEmail} />

    + onChange={this.onChangePassword} />
    +
    ) diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index b0e5ecc69..29822e9e7 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -11,12 +11,10 @@ require('../../../styles/components/poll_form.scss') const DEFAULT_ANSWERS = [ { 'name': '', - 'votes': [], 'vote_count': 0 }, { 'name': '', - 'votes': [], 'vote_count': 0 } ] @@ -24,7 +22,7 @@ const DEFAULT_ANSWERS = [ export default class PollForm extends React.Component { addAnswer() { - this.props.update('answers', this.getAnswers() + DEFAULT_ANSWERS[0]) + this.props.update('answers', this.getAnswers().concat(DEFAULT_ANSWERS[0])) } removeAnswer(index) { @@ -44,17 +42,14 @@ export default class PollForm extends React.Component { renderAnswers() { const answers = this.getAnswers().map( (answer, index) => { - const name = answer.name - const votes = answer.vote_count - const key = index + 1 return ( this.handleUpdateAnswer(e, index)} /> ) } -} \ No newline at end of file +} diff --git a/dispatch/tests/test_api_polls.py b/dispatch/tests/test_api_polls.py index 5cbc5e3ff..876eaa5e9 100644 --- a/dispatch/tests/test_api_polls.py +++ b/dispatch/tests/test_api_polls.py @@ -238,7 +238,7 @@ def test_poll_change_vote(self): # Check that the vote change was successful self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertNotEqual(response.data['id'], vote_id) + self.assertEqual(response.data['id'], vote_id) # Get the poll url = reverse('api-polls-detail', args=[poll_id]) @@ -250,6 +250,19 @@ def test_poll_change_vote(self): self.assertEqual(response.data['answers'][0]['vote_count'], 0) self.assertEqual(response.data['answers'][1]['vote_count'], 1) + def test_poll_vote_invalid_answer(self): + """A user should not be able to vote for an answer that is not + valid for the poll""" + + # Create polls to vote in + response = DispatchTestHelpers.create_poll(self.client, is_open=True) + + poll_1_id = response.data['id'] + answer_poll_1 = PollAnswer.objects.filter(poll_id=poll_id).first() + + response = DispatchTestHelpers.create_poll(self.client, is_open=True) + poll_2_id = response.data['id'] + def test_poll_vote_closed(self): """A user should not be able to vote in a closed poll""" From 2bdcc523061fb3b636520eaa597b2254f04849b1 Mon Sep 17 00:00:00 2001 From: JamieRL Date: Wed, 6 Jun 2018 14:23:14 -0700 Subject: [PATCH 52/55] add test case for giving an invalid answer for a poll --- dispatch/tests/test_api_polls.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/dispatch/tests/test_api_polls.py b/dispatch/tests/test_api_polls.py index 876eaa5e9..60a304fc9 100644 --- a/dispatch/tests/test_api_polls.py +++ b/dispatch/tests/test_api_polls.py @@ -258,11 +258,25 @@ def test_poll_vote_invalid_answer(self): response = DispatchTestHelpers.create_poll(self.client, is_open=True) poll_1_id = response.data['id'] - answer_poll_1 = PollAnswer.objects.filter(poll_id=poll_id).first() + poll_1 = Poll.objects.get(id=poll_1_id) + answer_poll_1 = PollAnswer.objects.filter(poll_id=poll_1_id).first() response = DispatchTestHelpers.create_poll(self.client, is_open=True) + poll_2_id = response.data['id'] - + poll_2 = Poll.objects.get(id=poll_2_id) + answer_poll_2 = PollAnswer.objects.filter(poll_id=poll_2_id).first() + + url = reverse('api-polls-vote', args=[poll_1_id]) + + data = { + 'answer_id': answer_poll_2.id + } + + response = self.client.post(url, data, format='json') + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_poll_vote_closed(self): """A user should not be able to vote in a closed poll""" From a946da3f3f7ae4f5aa776468b4687007faecdbad Mon Sep 17 00:00:00 2001 From: rowansdabomb Date: Wed, 6 Jun 2018 19:24:29 -0700 Subject: [PATCH 53/55] remove poll embeds fix addAnswer in pollForm.js --- dispatch/modules/content/embeds.py | 4 ---- .../static/manager/src/js/components/PollEditor/PollForm.js | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/dispatch/modules/content/embeds.py b/dispatch/modules/content/embeds.py index 523a9390b..72ff401fc 100644 --- a/dispatch/modules/content/embeds.py +++ b/dispatch/modules/content/embeds.py @@ -83,9 +83,6 @@ class AdvertisementEmbed(AbstractTemplateEmbed): class PullQuoteEmbed(AbstractTemplateEmbed): TEMPLATE = 'embeds/quote.html' -class PollEmbed(AbstractTemplateEmbed): - TEMPLATE = 'embeds/poll.html' - class WidgetEmbed(AbstractEmbed): @classmethod def render(self, data): @@ -163,7 +160,6 @@ def prepare_data(self, data): 'size': len(images) } -embeds.register('poll', PollEmbed) embeds.register('widget', WidgetEmbed) embeds.register('quote', PullQuoteEmbed) embeds.register('code', CodeEmbed) diff --git a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js index b0e5ecc69..307ac3187 100644 --- a/dispatch/static/manager/src/js/components/PollEditor/PollForm.js +++ b/dispatch/static/manager/src/js/components/PollEditor/PollForm.js @@ -24,7 +24,7 @@ const DEFAULT_ANSWERS = [ export default class PollForm extends React.Component { addAnswer() { - this.props.update('answers', this.getAnswers() + DEFAULT_ANSWERS[0]) + this.props.update('answers', this.getAnswers().concat( DEFAULT_ANSWERS[0])) } removeAnswer(index) { From 96105ae54225edc30cbc74cae45f09d44bb06aaa Mon Sep 17 00:00:00 2001 From: rowansdabomb Date: Thu, 7 Jun 2018 11:08:21 -0700 Subject: [PATCH 54/55] hide featured videos --- .../src/js/components/ArticleEditor/ArticleSidebar.js | 9 +++++---- .../static/manager/src/js/components/PollEditor/Poll.js | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dispatch/static/manager/src/js/components/ArticleEditor/ArticleSidebar.js b/dispatch/static/manager/src/js/components/ArticleEditor/ArticleSidebar.js index 719ddf018..201c2d235 100644 --- a/dispatch/static/manager/src/js/components/ArticleEditor/ArticleSidebar.js +++ b/dispatch/static/manager/src/js/components/ArticleEditor/ArticleSidebar.js @@ -4,7 +4,7 @@ import { Tabs, TabList, Tab, TabPanel } from '@blueprintjs/core' import BasicFieldsTab from './tabs/BasicFieldsTab' import FeaturedImageTab from '../Editor/tabs/FeaturedImageTab' -import FeaturedVideoTab from '../Editor/tabs/FeaturedVideoTab' +// import FeaturedVideoTab from '../Editor/tabs/FeaturedVideoTab' import DeliveryTab from '../Editor/tabs/DeliveryTab' import TemplateTab from '../Editor/tabs/TemplateTab' import SEOTab from '../Editor/tabs/SEOTab' @@ -18,7 +18,7 @@ export default function ArticleSidebar(props) { Basic fields Featured image - Featured video + {/* Featured video */} Delivery Template SEO @@ -43,12 +43,13 @@ export default function ArticleSidebar(props) { entities={props.entities} /> - + {/* uncomment when featured videos are ready */} + {/* - + */} Date: Thu, 7 Jun 2018 15:29:02 -0700 Subject: [PATCH 55/55] Update VersionsDropdown.js --- .../src/js/components/Editor/toolbar/VersionsDropdown.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dispatch/static/manager/src/js/components/Editor/toolbar/VersionsDropdown.js b/dispatch/static/manager/src/js/components/Editor/toolbar/VersionsDropdown.js index 70984b1e8..9ec542af7 100644 --- a/dispatch/static/manager/src/js/components/Editor/toolbar/VersionsDropdown.js +++ b/dispatch/static/manager/src/js/components/Editor/toolbar/VersionsDropdown.js @@ -8,10 +8,6 @@ require('../../../../styles/components/versions_dropdown.scss') export default class VersionsDropdown extends React.Component { - constructor(props) { - super(props) - } - getVersions() { let versions = []