From 90b1027e0a275df9fd29f171ead93be6ab7b156f Mon Sep 17 00:00:00 2001 From: Sebastien Mirolo Date: Fri, 23 Feb 2018 09:28:45 -0800 Subject: [PATCH] refactors vies to use EnumeratedQuestions (#8) --- LICENSE.txt | 2 +- Makefile | 4 +- README.md | 9 +- setup.py | 2 +- survey/admin.py | 15 ++- survey/api/matrix.py | 2 +- survey/compat.py | 2 +- survey/forms.py | 14 +-- survey/mixins.py | 58 ++++++------ survey/models.py | 22 ++--- survey/settings.py | 2 +- ...rveymodel_form.html => campaign_form.html} | 0 ...rveymodel_list.html => campaign_list.html} | 0 survey/templates/survey/question_list.html | 26 +++--- survey/templatetags/survey_tags.py | 2 +- survey/urls/__init__.py | 2 +- survey/urls/manager.py | 2 +- survey/urls/matrix.py | 2 +- survey/urls/sample.py | 2 +- survey/views/createquestion.py | 91 +++++++++---------- survey/views/edit.py | 2 +- survey/views/matrix.py | 2 +- testsite/fixtures/initial_data.json | 38 ++++---- testsite/requirements.txt | 17 ++-- 24 files changed, 157 insertions(+), 161 deletions(-) rename survey/templates/survey/{surveymodel_form.html => campaign_form.html} (100%) rename survey/templates/survey/{surveymodel_list.html => campaign_list.html} (100%) diff --git a/LICENSE.txt b/LICENSE.txt index 0c24957..44098c5 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2017, DjaoDjin inc. +Copyright (c) 2018, DjaoDjin inc. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/Makefile b/Makefile index 9d0dad7..77d7628 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Copyright (c) 2017, DjaoDjin inc. +# Copyright (c) 2018, DjaoDjin inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -46,7 +46,7 @@ $(srcDir)/credentials: $(srcDir)/testsite/etc/credentials # XXX Enter a superuser when asked otherwise the fixtures won't load # correctly. initdb: install-conf - -rm -f db.sqlite3 + -rm -f $(srcDir)/db.sqlite3 cd $(srcDir) && $(PYTHON) ./manage.py migrate $(RUNSYNCDB) --noinput cd $(srcDir) && $(PYTHON) ./manage.py loaddata \ testsite/fixtures/initial_data.json diff --git a/README.md b/README.md index c73fdb5..7778ee5 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,15 @@ Five minutes evaluation The source code is bundled with a sample django project. - $ virtualenv-2.7 *virtual_env_dir* + $ virtualenv *virtual_env_dir* $ cd *virtual_env_dir* $ source bin/activate - $ pip install -r requirements.txt - - $ python manage.py syncdb + $ pip install -r testsite/requirements.txt + $ make initdb $ python manage.py runserver # Visit url at http://localhost:8000/ - + # You can use username: donny, password: yoyo to test the manager options. Releases ======== diff --git a/setup.py b/setup.py index 2e517a5..398a69c 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017, DjaoDjin inc. +# Copyright (c) 2018, DjaoDjin inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/survey/admin.py b/survey/admin.py index 47aa41c..b146496 100644 --- a/survey/admin.py +++ b/survey/admin.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017, DjaoDjin inc. +# Copyright (c) 2018, DjaoDjin inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -24,9 +24,18 @@ from django.contrib import admin -from survey.models import Answer, Question, Sample, Campaign +from survey.models import (Answer, Campaign, Choice, EditableFilter, + EditablePredicate, EnumeratedQuestions, Question, Matrix, Metric, + Sample, Unit) +admin.site.register(Answer) admin.site.register(Campaign) +admin.site.register(Choice) +admin.site.register(EditableFilter) +admin.site.register(EditablePredicate) +admin.site.register(EnumeratedQuestions) admin.site.register(Question) +admin.site.register(Matrix) +admin.site.register(Metric) admin.site.register(Sample) -admin.site.register(Answer) +admin.site.register(Unit) diff --git a/survey/api/matrix.py b/survey/api/matrix.py index b78e45f..0e88724 100644 --- a/survey/api/matrix.py +++ b/survey/api/matrix.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017, DjaoDjin inc. +# Copyright (c) 2018, DjaoDjin inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/survey/compat.py b/survey/compat.py index 26659f1..70ea1a3 100644 --- a/survey/compat.py +++ b/survey/compat.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017, DjaoDjin inc. +# Copyright (c) 2018, DjaoDjin inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/survey/forms.py b/survey/forms.py index 84be827..d06681a 100644 --- a/survey/forms.py +++ b/survey/forms.py @@ -104,24 +104,12 @@ class QuestionForm(forms.ModelForm): class Meta: model = get_question_model() - exclude = ['survey', 'rank'] - - def save(self, commit=True): - if 'survey' in self.initial: - self.instance.survey = self.initial['survey'] - if 'rank' in self.initial and not self.instance.rank: - self.instance.rank = self.initial['rank'] - return super(QuestionForm, self).save(commit) + fields = ('path', 'title', 'text', 'default_metric', 'extra') def clean_choices(self): self.cleaned_data['choices'] = self.cleaned_data['choices'].strip() return self.cleaned_data['choices'] - def clean_correct_answer(self): - self.cleaned_data['correct_answer'] \ - = self.cleaned_data['correct_answer'].strip() - return self.cleaned_data['correct_answer'] - class SampleCreateForm(forms.ModelForm): diff --git a/survey/mixins.py b/survey/mixins.py index 9451e5b..fc62bc6 100644 --- a/survey/mixins.py +++ b/survey/mixins.py @@ -26,12 +26,12 @@ from django.core.exceptions import ImproperlyConfigured from django.http import Http404 -from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext as _ -from django.views.generic.detail import SingleObjectMixin +from rest_framework.generics import get_object_or_404 from . import settings -from .models import Matrix, EditableFilter, Sample, Campaign +from .models import (Campaign, EnumeratedQuestions, EditableFilter, Matrix, + Sample) from .utils import get_account_model, get_question_model @@ -114,7 +114,8 @@ def get_interviewee(self): try: kwargs = {'%s__exact' % settings.ACCOUNT_LOOKUP_FIELD: self.kwargs.get(self.interviewee_slug)} - interviewee = get_object_or_404(account_model, **kwargs) + interviewee = get_object_or_404( + account_model.objects.all(), **kwargs) except account_model.DoesNotExist: interviewee = self.request.user else: @@ -140,21 +141,6 @@ def get_url_context(self): return kwargs -class QuestionMixin(SingleObjectMixin): - - num_url_kwarg = 'num' - survey_url_kwarg = 'survey' - - def get_object(self, queryset=None): - """ - Returns a question object based on the URL. - """ - rank = self.kwargs.get(self.num_url_kwarg, 1) - slug = self.kwargs.get(self.survey_url_kwarg, None) - survey = get_object_or_404(Campaign, slug__exact=slug) - return get_object_or_404(get_question_model(), survey=survey, rank=rank) - - class CampaignMixin(object): """ Returns a ``Campaign`` object associated with the request URL. @@ -168,10 +154,29 @@ def campaign(self): return self._campaign def get_survey(self): - return get_object_or_404(Campaign, slug=self.kwargs.get( + return get_object_or_404(Campaign.objects.all(), slug=self.kwargs.get( self.survey_url_kwarg)) +class CampaignQuestionMixin(CampaignMixin): + + num_url_kwarg = 'num' + model = EnumeratedQuestions + + def get_queryset(self): + return self.model.objects.filter( + campaign=self.campaign).order_by('rank') + + def get_object(self, queryset=None): + """ + Returns a question object based on the URL. + """ + if not queryset: + queryset = self.get_queryset() + return get_object_or_404(queryset, + rank=self.kwargs.get(self.num_url_kwarg, 1)) + + class SampleMixin(IntervieweeMixin, CampaignMixin): """ Returns a ``Sample`` to a ``Campaign``. @@ -207,7 +212,7 @@ def get_sample(self, url_kwarg=None): # Well no id, let's see if we can find a sample from # a survey slug and a account interviewee = self.get_interviewee() - sample = get_object_or_404(Sample, + sample = get_object_or_404(Sample.objects.all(), survey=self.get_survey(), account=interviewee) return sample @@ -219,7 +224,7 @@ def get_reverse_kwargs(self): return [self.interviewee_slug, 'survey', self.sample_url_kwarg] -class MatrixQuerysetMixin(AccountMixin): +class MatrixQuerysetMixin(object): @staticmethod def get_queryset(): @@ -233,18 +238,19 @@ class MatrixMixin(MatrixQuerysetMixin): @property def matrix(self): if not hasattr(self, '_matrix'): - self._matrix = get_object_or_404(Matrix, + self._matrix = get_object_or_404(self.get_queryset(), slug=self.kwargs.get(self.matrix_url_kwarg)) return self._matrix -class EditableFilterMixin(AccountMixin): +class EditableFilterMixin(object): editable_filter_url_kwarg = 'editable_filter' @property def editable_filter(self): if not hasattr(self, '_editable_filter'): - self._editable_filter = get_object_or_404(EditableFilter, - slug=self.kwargs.get(self.editable_filter_url_kwarg)) + self._editable_filter = get_object_or_404( + EditableFilter.objects.all(), + slug=self.kwargs.get(self.editable_filter_url_kwarg)) return self._editable_filter diff --git a/survey/models.py b/survey/models.py index 75faf39..444bd2f 100644 --- a/survey/models.py +++ b/survey/models.py @@ -151,23 +151,21 @@ class Meta: text = models.TextField( help_text=_("Detailed description about the question")) question_type = models.CharField( - max_length=9, choices=QUESTION_TYPES, default=TEXT, + max_length=9, choices=QUESTION_TYPES, default=RADIO, help_text=_("Choose the type of answser.")) - correct_answer = models.ForeignKey(Choice, null=True) + correct_answer = models.ForeignKey(Choice, null=True, blank=True) default_metric = models.ForeignKey(Metric) - extra = settings.get_extra_field_class()(null=True) + extra = settings.get_extra_field_class()(null=True, blank=True) def __str__(self): return self.path - def get_choices(self): - choices_list = [] - if self.choices: - #pylint: disable=no-member - for choice in self.choices.split('\n'): - choice = choice.strip() - choices_list += [(choice, choice)] - return choices_list + @property + def choices(self): + if self.default_metric.unit.system == Unit.SYSTEM_ENUMERATED: + return [choice.text for choice + in Choice.objects.filter(unit=self.default_metric.unit)] + return None def get_correct_answer(self): correct_answer_list = [] @@ -181,7 +179,7 @@ def get_correct_answer(self): class Question(AbstractQuestion): # XXX Before migration: - pass +# pass # XXX After migration class Meta(AbstractQuestion.Meta): swappable = 'QUESTION_MODEL' diff --git a/survey/settings.py b/survey/settings.py index 7b24c4e..ae18fdb 100644 --- a/survey/settings.py +++ b/survey/settings.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017, DjaoDjin inc. +# Copyright (c) 2018, DjaoDjin inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/survey/templates/survey/surveymodel_form.html b/survey/templates/survey/campaign_form.html similarity index 100% rename from survey/templates/survey/surveymodel_form.html rename to survey/templates/survey/campaign_form.html diff --git a/survey/templates/survey/surveymodel_list.html b/survey/templates/survey/campaign_list.html similarity index 100% rename from survey/templates/survey/surveymodel_list.html rename to survey/templates/survey/campaign_list.html diff --git a/survey/templates/survey/question_list.html b/survey/templates/survey/question_list.html index 1a92cf7..fdb83a6 100644 --- a/survey/templates/survey/question_list.html +++ b/survey/templates/survey/question_list.html @@ -3,10 +3,10 @@ {% block content %}
-

Questions for {{survey.title}}

+

Questions for {{campaign.title}}

-

Add a question

+

Add a question

@@ -18,37 +18,37 @@

Questions for {{survey.title}}

- {% if question_list %} - {% for question in question_list %} + {% if enumeratedquestions_list %} + {% for campaign_question in enumeratedquestions_list %} - - - - + + + + - + {% endfor %}
Question Order down Order
{{question.text}}{{question.question_type}}{% if question.choices %}{{question.choices}}{% else %}N/A{% endif %}Edit{{campaign_question.question.text}}{{campaign_question.question.question_type}}{% if campaign_question.question.choices %}{{campaign_question.question.choices}}{% else %}N/A{% endif %}Edit -
+ {% csrf_token %}
-
+ {% csrf_token %}
-
+ {% csrf_token %}
{{question.rank}}{{campaign_question.question.rank}}
{% else %} - No question for {{survey.title}} survey + No question for campaign titled "{{campaign.title}}" {% endif %}
{% endblock %} diff --git a/survey/templatetags/survey_tags.py b/survey/templatetags/survey_tags.py index 616e31c..fd99b8e 100644 --- a/survey/templatetags/survey_tags.py +++ b/survey/templatetags/survey_tags.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017, DjaoDjin inc. +# Copyright (c) 2018, DjaoDjin inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/survey/urls/__init__.py b/survey/urls/__init__.py index fc50af7..af8da6f 100644 --- a/survey/urls/__init__.py +++ b/survey/urls/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017, DjaoDjin inc. +# Copyright (c) 2018, DjaoDjin inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/survey/urls/manager.py b/survey/urls/manager.py index 3588cd8..cb0bfe8 100644 --- a/survey/urls/manager.py +++ b/survey/urls/manager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017, DjaoDjin inc. +# Copyright (c) 2018, DjaoDjin inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/survey/urls/matrix.py b/survey/urls/matrix.py index 250e803..16f0306 100644 --- a/survey/urls/matrix.py +++ b/survey/urls/matrix.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017, DjaoDjin inc. +# Copyright (c) 2018, DjaoDjin inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/survey/urls/sample.py b/survey/urls/sample.py index a82cd6e..cdab3f9 100644 --- a/survey/urls/sample.py +++ b/survey/urls/sample.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017, DjaoDjin inc. +# Copyright (c) 2018, DjaoDjin inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/survey/views/createquestion.py b/survey/views/createquestion.py index 4811dad..f68453d 100644 --- a/survey/views/createquestion.py +++ b/survey/views/createquestion.py @@ -23,41 +23,46 @@ # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from django.core.urlresolvers import reverse +from django.db import transaction +from django.db.models import Max from django.shortcuts import get_object_or_404 from django.views.generic import (CreateView, DeleteView, ListView, RedirectView, UpdateView) from ..compat import csrf from ..forms import QuestionForm -from ..models import Campaign -from ..mixins import QuestionMixin, CampaignMixin +from ..models import EnumeratedQuestions +from ..mixins import CampaignMixin, CampaignQuestionMixin from ..utils import get_question_model -class QuestionFormMixin(QuestionMixin): +class QuestionFormMixin(CampaignQuestionMixin): model = get_question_model() form_class = QuestionForm success_url = 'survey_question_list' - def __init__(self, *args, **kwargs): - super(QuestionFormMixin, self).__init__(*args, **kwargs) - self.survey = None - - def get_initial(self): + def get_object(self, queryset=None): """ - Returns the initial data to use for forms on this view. + Returns a question object based on the URL. """ - kwargs = super(QuestionFormMixin, self).get_initial() - self.survey = get_object_or_404( - Campaign, slug__exact=self.kwargs.get('survey')) - last_rank = self.model.objects.filter(survey=self.survey).count() - kwargs.update({'survey': self.survey, - 'rank': last_rank + 1}) - return kwargs + return super(QuestionFormMixin, self).get_object( + queryset=queryset).question + + def form_valid(self, form): + with transaction.atomic(): + result = super(QuestionFormMixin, self).form_valid(form) + last_rank = EnumeratedQuestions.objects.filter( + campaign=self.campaign).aggregate(Max('rank')).get( + 'rank__max', 0) + _ = EnumeratedQuestions.objects.get_or_create( + question=self.object, + campaign=self.campaign, + defaults={'rank': last_rank}) + return result def get_success_url(self): - return reverse(self.success_url, args=(self.object.survey,)) + return reverse(self.success_url, args=(self.campaign,)) class QuestionCreateView(QuestionFormMixin, CreateView): @@ -67,40 +72,30 @@ class QuestionCreateView(QuestionFormMixin, CreateView): pass -class QuestionDeleteView(QuestionMixin, DeleteView): +class QuestionDeleteView(CampaignQuestionMixin, DeleteView): """ Delete a question. """ success_url = 'survey_question_list' def get_success_url(self): - return reverse(self.success_url, args=(self.object.survey,)) + return reverse(self.success_url, args=(self.campaign,)) -class QuestionListView(CampaignMixin, ListView): +class QuestionListView(CampaignQuestionMixin, ListView): """ List of questions for a survey """ - model = get_question_model() - - def __init__(self, *args, **kwargs): - super(QuestionListView, self).__init__(*args, **kwargs) - self.survey = None - - def get_queryset(self): - self.survey = self.get_survey() - queryset = self.model.objects.filter( - survey=self.survey).order_by('rank') - return queryset + template_name = 'survey/question_list.html' def get_context_data(self, **kwargs): context = super(QuestionListView, self).get_context_data(**kwargs) context.update(csrf(self.request)) - context.update({'survey': self.survey}) + context.update({'campaign': self.campaign}) return context -class QuestionRankView(QuestionMixin, RedirectView): +class QuestionRankView(CampaignQuestionMixin, RedirectView): """ Update the rank of a question in a survey """ @@ -109,24 +104,26 @@ class QuestionRankView(QuestionMixin, RedirectView): direction = 1 # defaults to "down" def post(self, request, *args, **kwargs): - question = self.get_object() - swapped_question = None - question_rank = question.rank + enum_question = self.get_object() + swapped_enum_question = None + question_rank = enum_question.rank if self.direction < 0: if question_rank > 1: - swapped_question = self.model.objects.get( - survey=question.survey, rank=question_rank - 1) + swapped_enum_question = self.model.objects.get( + campaign=enum_question.campaign, rank=question_rank - 1) else: if question_rank < self.model.objects.filter( - survey=question.survey).count(): - swapped_question = self.model.objects.get( - survey=question.survey, rank=question_rank + 1) - if swapped_question: - question.rank = swapped_question.rank - swapped_question.rank = question_rank - question.save() - swapped_question.save() - kwargs = {'slug': kwargs['survey']} + campaign=enum_question.campaign).count(): + swapped_enum_question = self.model.objects.get( + campaign=enum_question.campaign, rank=question_rank + 1) + print("XXX %d swap %s(%d) for %s(%d)" % (question_rank, enum_question, enum_question.rank, swapped_enum_question, swapped_enum_question.rank)) + if swapped_enum_question: + enum_question.rank = swapped_enum_question.rank + swapped_enum_question.rank = question_rank + print("XXX updated to %s(%d) for %s(%d)" % (enum_question, enum_question.rank, swapped_enum_question, swapped_enum_question.rank)) + enum_question.save() + swapped_enum_question.save() + del kwargs[self.num_url_kwarg] return super(QuestionRankView, self).post(request, *args, **kwargs) diff --git a/survey/views/edit.py b/survey/views/edit.py index febbc8a..8813388 100644 --- a/survey/views/edit.py +++ b/survey/views/edit.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017, DjaoDjin inc. +# Copyright (c) 2018, DjaoDjin inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/survey/views/matrix.py b/survey/views/matrix.py index caf46e8..9db4f61 100644 --- a/survey/views/matrix.py +++ b/survey/views/matrix.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017, DjaoDjin inc. +# Copyright (c) 2018, DjaoDjin inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/testsite/fixtures/initial_data.json b/testsite/fixtures/initial_data.json index d38700f..d9e6bdc 100644 --- a/testsite/fixtures/initial_data.json +++ b/testsite/fixtures/initial_data.json @@ -25,15 +25,15 @@ "model" : "survey.campaign", "pk" : 1 }, { "fields": { - "slug": "q1", - "title": "Question 1", + "slug": "sentiment", + "title": "Sentiment", "system": 3 }, "model" : "survey.unit", "pk" : 1 }, { "fields": { - "slug": "q1", - "title": "Question 1", + "slug": "sentiment", + "title": "Sentiment", "unit": 1 }, "model" : "survey.metric", "pk" : 1 @@ -60,16 +60,16 @@ "model" : "survey.choice", "pk" : 3 }, { "fields": { - "slug": "q2", - "title": "Question 2", + "slug": "features", + "title": "Features", "system": 3 }, "model" : "survey.unit", "pk" : 2 }, { "fields": { - "slug": "q2", - "title": "Question 2", - "unit": 1 + "slug": "features", + "title": "Features", + "unit": 2 }, "model" : "survey.metric", "pk" : 2 }, @@ -102,29 +102,29 @@ "model" : "survey.choice", "pk" : 7 }, { "fields": { - "slug": "q3", - "title": "Question 3", - "system": 3 + "slug": "rating", + "title": "Rating", + "system": 2 }, "model" : "survey.unit", "pk" : 3 }, { "fields": { - "slug": "q3", - "title": "Question 3", - "unit": 2 + "slug": "rating", + "title": "Rating", + "unit": 3 }, "model" : "survey.metric", "pk" : 3 }, { "fields": { - "slug": "q4", - "title": "Question 4", + "slug": "free-text", + "title": "Free Text", "system": 3 }, "model" : "survey.unit", "pk" : 4 }, { "fields": { - "slug": "q4", - "title": "Question 4", + "slug": "free-text", + "title": "Free Text", "unit": 4 }, "model" : "survey.metric", "pk" : 4 diff --git a/testsite/requirements.txt b/testsite/requirements.txt index 7423af0..ef1e645 100644 --- a/testsite/requirements.txt +++ b/testsite/requirements.txt @@ -1,13 +1,12 @@ -Django==1.9.6 -django-extra-views==0.6.5 -djangorestframework==3.3.3 +Django==1.9.9 +django-extra-views==0.9.0 +djangorestframework==3.7.0 # testsite-only -django-debug-toolbar==1.4 -django-extensions==1.6.1 -django-urldecorators==0.5 +django-debug-toolbar==1.5 +django-extensions==1.6.7 +django-urldecorators==0.6 # development -Sphinx==1.4.1 -sphinxcontrib-httpdomain==1.4.0 - +Sphinx==1.6.2 +sphinxcontrib-httpdomain==1.5.0