-
Notifications
You must be signed in to change notification settings - Fork 6
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
Changes from 30 commits
6eef631
709c033
71de7d0
260c9e5
215d0b8
12024a1
c5d443d
e3a2d55
b1e1722
c56cc75
a869c35
f36368a
906404c
b4b887d
aef2072
b85d3db
a51ea32
8c1a6ea
98b9918
5c55af3
b47339e
c28f416
7334de5
f2530f3
0df16d7
d3a4efd
1e2a440
6f14b0b
1d3770f
0dacc0f
6f15acd
de028b9
3d5c8b1
c672e89
0c3931c
0dbeaaa
695453b
caf77e5
baf53a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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 |
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 |
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 | ||
}, | ||
|
||
getNewQuestion: function (label) { | ||
var newQuestion = {} | ||
newQuestion['label'] = label | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Combine those: api.poll.change(...)
.done(...)
.fail(...) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider using fat arrow functions to get rid of all the |
||
.fail(function (xhr, status, err) { | ||
this.setState({ | ||
errors: xhr.responseJSON.questions || [] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why a fallback? If there are no errors to set (which |
||
}) | ||
}.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 |
There was a problem hiding this comment.
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 themaxQuestionKey
cause arender
?There was a problem hiding this comment.
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