Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow votes from the user frontend #283

Merged
merged 39 commits into from
May 8, 2017
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
6eef631
Add django-nested-formset as dependency and with a small fix for fields
Apr 27, 2017
709c033
Use nested formsets for poll creation
Apr 27, 2017
71de7d0
Merge branch '2017-04-jd-polls-refactoring' into 2017-04-jd-polls-man…
Apr 27, 2017
260c9e5
Adapt forms to model refactoring
Apr 27, 2017
215d0b8
Revert "Add django-nested-formset as dependency and with a small fix …
Apr 27, 2017
12024a1
Add react based poll management
Apr 27, 2017
c5d443d
Merge remote-tracking branch 'origin/master' into 2017-04-jd-polls-ma…
Apr 27, 2017
e3a2d55
Use Manager to annotate vote count
May 2, 2017
b1e1722
Merge remote-tracking branch 'origin/master' into 2017-04-jd-polls-ma…
May 2, 2017
c56cc75
Fix: Use Manager to annotate vote count
May 2, 2017
a869c35
Add poll api endpoint
May 2, 2017
f36368a
Use Ajax for poll management submission.
May 2, 2017
906404c
Show validation errors in poll forms
May 2, 2017
b4b887d
Merge remote-tracking branch 'origin/master' into 2017-04-jd-polls-ma…
May 2, 2017
aef2072
Fix rules for the polls management api
May 2, 2017
b85d3db
Merge remote-tracking branch 'origin/master' into 2017-04-jd-polls-ma…
May 3, 2017
a51ea32
Add correct rules for votes
May 3, 2017
8c1a6ea
Add vote per user per question validator
May 3, 2017
98b9918
Use a4 api.js
May 3, 2017
5c55af3
Use react state for poll/choice inputs
May 3, 2017
b47339e
Merge branch 'master' into 2017-04-jd-polls-management
kleingeist May 3, 2017
c28f416
Merge branch 'master' into 2017-05-jd-polls-frontend
kleingeist May 3, 2017
7334de5
Remove falsy unique together constraint
May 3, 2017
f2530f3
Merge branch '2017-04-jd-polls-management' into 2017-05-jd-polls-fron…
May 3, 2017
0df16d7
Rename Polls.jsx to react_polls.jsx
May 4, 2017
d3a4efd
Enable vote rest api
May 4, 2017
1e2a440
Use /polls route for updating
May 4, 2017
6f14b0b
Use /pollvotes/$questionId route for the voting API
May 4, 2017
1d3770f
Gardening
May 4, 2017
0dacc0f
fix typo
vellip May 4, 2017
6f15acd
Remove weight from Question serializer
May 4, 2017
de028b9
Split Poll Serializer update functions
May 4, 2017
3d5c8b1
Fix vote submission on create.
May 4, 2017
c672e89
Merge remote-tracking branch 'origin/master' into 2017-05-jd-polls-fr…
May 8, 2017
0c3931c
Gardening in response to review
May 8, 2017
0dbeaaa
Move maxKeys counters from the state to the object
May 8, 2017
695453b
Use arrow functions
May 8, 2017
caf77e5
Use ErrorList and Alert Components
May 8, 2017
baf53a5
Merge remote-tracking branch 'origin/master' into 2017-05-jd-polls-fr…
May 8, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added apps/contrib/api/__init__.py
Empty file.
46 changes: 46 additions & 0 deletions apps/contrib/api/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from django.http import Http404

from rest_framework import status
from rest_framework.request import clone_request
from rest_framework.response import Response


class AllowPUTAsCreateMixin(object):
"""Allow Put-as-create behaviour for incoming requests."""

def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object_or_none()
serializer = self.get_serializer(instance,
data=request.data,
partial=partial)
serializer.is_valid(raise_exception=True)

if instance is None:
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
lookup_value = self.kwargs[lookup_url_kwarg]
extra_kwargs = {self.lookup_field: lookup_value}
serializer.save(**extra_kwargs)
return Response(serializer.data, status=status.HTTP_201_CREATED)

serializer.save()
return Response(serializer.data)

def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)

def get_object_or_none(self):
try:
return self.get_object()
except Http404:
if self.request.method == 'PUT':
# For PUT-as-create operation, we need to ensure that we have
# relevant permissions, as if this was a POST request. This
# will either raise a PermissionDenied exception, or simply
# return None.
self.check_permissions(clone_request(self.request, 'POST'))
else:
# PATCH requests where the object does not exist should still
# return a 404 response.
raise
58 changes: 58 additions & 0 deletions apps/polls/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from django.shortcuts import get_object_or_404
from rest_framework import mixins
from rest_framework import viewsets

from adhocracy4.api.permissions import ViewSetRulesPermission
from apps.contrib.api.mixins import AllowPUTAsCreateMixin
from .models import Poll
from .models import Question
from .models import Vote
from .serializers import PollSerializer
from .serializers import VoteSerializer


class PollViewSet(mixins.UpdateModelMixin,
viewsets.GenericViewSet):
queryset = Poll.objects.all()
serializer_class = PollSerializer
permission_classes = (ViewSetRulesPermission,)

def get_permission_object(self):
poll = self.get_object()
return poll.module


class VoteViewSet(AllowPUTAsCreateMixin,
mixins.UpdateModelMixin,
viewsets.GenericViewSet):
queryset = Vote.objects.all()
serializer_class = VoteSerializer
permission_classes = (ViewSetRulesPermission,)

def dispatch(self, request, *args, **kwargs):
self.question_pk = int(kwargs['pk'])
return super().dispatch(request, *args, **kwargs)

@property
def question(self):
return get_object_or_404(
Question,
pk=self.question_pk
)

def get_object(self):
return get_object_or_404(
Vote,
creator=self.request.user,
choice__question=self.question_pk
)

def get_permission_object(self):
return self.question.poll.module

def get_serializer_context(self):
context = super(VoteViewSet, self).get_serializer_context()
context.update({
'question_pk': self.question_pk,
})
return context
49 changes: 49 additions & 0 deletions apps/polls/assets/ChoiceForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
var React = require('react')
var django = require('django')

let ChoiceForm = React.createClass({
handleLabelChange: function (e) {
var index = this.props.index
var label = e.target.value
this.props.updateChoiceLabel(index, label)
},

handleDelete: function () {
this.props.deleteChoice(this.props.index)
},

render: function () {
return (
<div>
<label
htmlFor={'id_choices-' + this.props.key + '-name'}>
{django.gettext('Choice:')}
</label>
<input
className="form-control"
id={'id_choices-' + this.props.key + '-name'}
name={'choices-' + this.props.key + '-name'}
type="text"
defaultValue={this.props.choice.label}
onChange={this.handleLabelChange} />
<div className="button-group">
<button
className="button"
onClick={this.handleDelete}
type="button">
<i className="fa fa-trash" />
</button>
</div>
{this.props.errors && this.props.errors.label
? <ul className="errorlist">
{this.props.errors.label.map(function (msg, index) {
return <li key={msg}>{msg}</li>
})}
</ul>
: null}
</div>
)
}
})

module.exports = ChoiceForm
224 changes: 224 additions & 0 deletions apps/polls/assets/PollManagement.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
var api = require('adhocracy4').api
var React = require('react')
var django = require('django')
var update = require('react-addons-update')
var FlipMove = require('react-flip-move')
var QuestionForm = require('./QuestionForm')

let PollManagement = React.createClass({
getInitialState: function () {
return {
questions: this.props.poll.questions,
errors: [],
successMessage: '',
maxQuestionKey: 0,
maxChoiceKey: 0
}
},

/*
|--------------------------------------------------------------------------
| Question state related handlers
|--------------------------------------------------------------------------
*/

getNextQuestionKey: function () {
/** Get an artifical key for non-commited questions.
*
* Prefix to prevent collisions with real database keys;
*/
var questionKey = 'local_' + (this.state.maxQuestionKey + 1)
this.setState({maxQuestionKey: this.state.maxQuestionKey + 1})
return questionKey
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be in state? Should changes to the maxQuestionKey cause a render?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, i changed it to be a class variable. the changes should be reflected in documents, too

},

getNewQuestion: function (label) {
var newQuestion = {}
newQuestion['label'] = label
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this

return {
  label: label,
  key: this.getNextQuestionKey(),
  choices: []
}

newQuestion['key'] = this.getNextQuestionKey()
newQuestion['choices'] = []
return newQuestion
},

handleUpdateQuestionLabel: function (index, label) {
var diff = {}
diff[index] = {$merge: {label: label}}

this.setState({
questions: update(this.state.questions, diff)
})
},

handleMoveQuestionUp: function (index) {
var question = this.state.questions[index]
var diff = {$splice: [[index, 1], [index - 1, 0, question]]}

this.setState({
questions: update(this.state.questions, diff)
})
},

handleMoveQuestionDown: function (index) {
var question = this.state.questions[index]
var diff = {$splice: [[index, 1], [index + 1, 0, question]]}

this.setState({
questions: update(this.state.questions, diff)
})
},

handleAppendQuestion: function () {
var newQuestion = this.getNewQuestion('')
var diff = {$push: [newQuestion]}

this.setState({
questions: update(this.state.questions, diff)
})
},

handleDeleteQuestion: function (index) {
var diff = {$splice: [[index, 1]]}

this.setState({
questions: update(this.state.questions, diff)
})
},

/*
|--------------------------------------------------------------------------
| Choice state related handlers
|--------------------------------------------------------------------------
*/

getNextChoiceKey: function () {
/** Get an artifical key for non-commited choices.
*
* Prefix to prevent collisions with real database keys;
*/
var choiceKey = 'local_' + (this.state.maxChoiceKey + 1)
this.setState({maxChoiceKey: this.state.maxChoiceKey + 1})
return choiceKey
},

getNewChoice: function (label) {
var newChoice = {}
newChoice['label'] = label
newChoice['key'] = this.getNextChoiceKey()
return newChoice
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return {
  label: label,
  key: this.getNextChoiceKey()
}

},

handleUpdateChoiceLabel: function (questionIndex, choiceIndex, label) {
var diff = {}
diff[questionIndex] = {choices: {}}
diff[questionIndex]['choices'][choiceIndex] = {$merge: {label: label}}

this.setState({
questions: update(this.state.questions, diff)
})
},

handleAppendChoice: function (questionIndex) {
var newChoice = this.getNewChoice('')
var diff = {}
diff[questionIndex] = {choices: {$push: [newChoice]}}

this.setState({
questions: update(this.state.questions, diff)
})
},

handleDeleteChoice: function (questionIndex, choiceIndex) {
var diff = {}
diff[questionIndex] = {choices: {$splice: [[choiceIndex, 1]]}}

this.setState({
questions: update(this.state.questions, diff)
})
},

/*
|--------------------------------------------------------------------------
| Poll form and submit logic
|--------------------------------------------------------------------------
*/

handleSubmit: function (e) {
e.preventDefault()

let data = {
questions: this.state.questions
}

let promise = api.poll.change(data, this.props.poll.id)
promise
.done(function (data) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combine those:

api.poll.change(...)
  .done(...)
  .fail(...)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meaning, no need to store it in a variable.

this.setState({
successMessage: django.gettext('The poll has been updated.')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll want to reset previous errors here.

})

setTimeout(function () {
this.setState({
successMessage: ''
})
}.bind(this), 1500)
}.bind(this))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using fat arrow functions to get rid of all the .binds

.fail(function (xhr, status, err) {
this.setState({
errors: xhr.responseJSON.questions || []
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a fallback? If there are no errors to set (which errors: [] would mean) you don't want to call setState.

})
}.bind(this))
},

render: function () {
return (
<form onSubmit={this.handleSubmit}>
{ this.state.successMessage
? <p className="alert alert-success ">
{this.state.successMessage}
</p> : null
}

<FlipMove easing="cubic-bezier(0.25, 0.5, 0.75, 1)">
{
this.state.questions.map(function (question, index) {
var key = question.id || question.key
var errors = this.state.errors && this.state.errors[index] ? this.state.errors[index] : {}
return (
<QuestionForm
key={key}
index={index}
question={question}
updateQuestionLabel={this.handleUpdateQuestionLabel}
moveQuestionUp={index !== 0 ? this.handleMoveQuestionUp : null}
moveQuestionDown={index < this.state.questions.length - 1 ? this.handleMoveQuestionDown : null}
deleteQuestion={this.handleDeleteQuestion}
errors={errors}
updateChoiceLabel={this.handleUpdateChoiceLabel}
deleteChoice={this.handleDeleteChoice}
appendChoice={this.handleAppendChoice}
/>
)
}.bind(this))
}
</FlipMove>

<button
className="button button--full"
onClick={this.handleAppendQuestion}
type="button">
<i className="fa fa-plus" /> {django.gettext('add a new question')}
</button>

{ this.state.successMessage
? <p className="alert alert-success ">
{this.state.successMessage}
</p> : null
}

<button type="submit" className="button button--primary">{django.gettext('save')}</button>
</form>
)
}
})

module.exports = PollManagement
Loading