From 1d72eee606c55b054f239002aa04f6977d9375af Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Wed, 4 Sep 2013 16:09:44 +0200 Subject: [PATCH 01/27] * Removing privacy page --- survey/urls.py | 1 - survey/views.py | 2 -- templates/index.html | 1 - templates/privacy.html | 23 ----------------------- 4 files changed, 27 deletions(-) delete mode 100644 templates/privacy.html diff --git a/survey/urls.py b/survey/urls.py index dcaef12a..08e88903 100644 --- a/survey/urls.py +++ b/survey/urls.py @@ -12,7 +12,6 @@ url(r'^$', 'survey.views.Index', name='home'), url(r'^survey/(?P\d+)/$', 'survey.views.SurveyDetail', name='survey_detail'), url(r'^confirm/(?P\w+)/$', 'survey.views.Confirm', name='confirmation'), - url(r'^privacy/$', 'survey.views.privacy', name='privacy_statement'), # Uncomment the admin/doc line below to enable admin documentation: diff --git a/survey/views.py b/survey/views.py index 7ebdf7c5..e4bce57b 100644 --- a/survey/views.py +++ b/survey/views.py @@ -35,7 +35,5 @@ def Confirm(request, uuid): email = settings.support_email return render(request, 'confirm.html', {'uuid':uuid, "email": email}) -def privacy(request): - return render(request, 'privacy.html') diff --git a/templates/index.html b/templates/index.html index 707a1b8c..8d3eac2b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -15,7 +15,6 @@

Welcome to the Coliving Census

Take the Survey || - Read the Privacy Statement
diff --git a/templates/privacy.html b/templates/privacy.html deleted file mode 100644 index d709935b..00000000 --- a/templates/privacy.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends 'base.html' %} - -{% block body %} - -

We are passionate about the coliving movement and embrace a collaborative approach to projects. Our hope is to be able to share the information we find with other houses, as well as use it ourselves to identify shared needs within the greater coliving community - we think this is part of the value proposition this process brings to everyone. We also have total respect for your privacy, and coliving houses are your home. All information you share can be as anonymous or open as you'd like. In particular:

- -
    -
  • Your participation in this study is completely voluntary. You do not have to answer any questions that you do not feel comfortable answering, and you may stop the interview or observation at any point during a session without penalty.
  • -
  • There are no foreseeable risks to participation in this research.
  • -We would like permission to record audio of the interview session so that what you say may be transcribed and/or reviewed to ensure accuracy. -
  • The original audio and text from the interview and any associated observations will be kept confidential to the census team and the interviewer.
  • -
  • Personally identifying information such as your personal and house names will be removed and replaced with a pseudonym.
  • -
  • A data set comprised of aggregate data from all participating houses will be compiled and made publicly available.
  • -
  • You of course MAY elect to make certain answers or information about your individual house public, but that is not required. If there is an opportunity for doing so, we will lay out the structure and implications with you explicitly.
  • -
  • We will be compiling a written report to accompany the aggregate data set. We may wish to use certain quotes or data points specific to your community in this report. If this is the case, you will be consulted for permission before publication. -Information and experiences related to legal challenges and ambiguities associated with running a coliving house will be kept in the strictest confidence.
  • -
  • Data will be stored in a web application running on a server. Access to the server is protected with key-based secure logins and the web application is password-protected.
  • -
  • The software used for this survey is open source and can be retrieved and examined at https://github.com/jessykate/django-survey
  • -
- -

You do not have to answer any questions that you do not feel comfortable answering, and you may stop the interview or observation at any point during a session without penalty. Feel free to ask any questions before, during, or after the session, either in person or via email at survey@coliving.org

- -{% endblock %} \ No newline at end of file From cecce3f4b83e48589161a988cba6f61c035ec94d Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Wed, 4 Sep 2013 17:17:18 +0200 Subject: [PATCH 02/27] * Improved code (using 4 spaces indentation, etc) * Removed some default response fields * Converted views to class based views * Improved admin --- survey/actions.py | 14 ++ survey/admin.py | 17 ++- survey/forms.py | 202 ++++++++++++++------------- survey/models.py | 38 +++-- survey/settings.py | 2 +- survey/templatetags/survey_extras.py | 12 +- survey/urls.py | 12 +- survey/utils.py | 9 ++ survey/views.py | 81 ++++++----- templates/base.html | 2 +- templates/confirm.html | 8 +- templates/index.html | 20 +-- templates/survey.html | 81 ++++------- 13 files changed, 254 insertions(+), 244 deletions(-) create mode 100644 survey/actions.py create mode 100644 survey/utils.py diff --git a/survey/actions.py b/survey/actions.py new file mode 100644 index 00000000..b2db57b3 --- /dev/null +++ b/survey/actions.py @@ -0,0 +1,14 @@ +from django.utils.translation import ungettext, ugettext_lazy + +# Actions +def make_published(modeladmin, request, queryset): + """ + Mark the given survey as published + """ + count = queryset.update(is_published=True) + message = ungettext( + u'%(count)d survey was successfully marked as published.', + u'%(count)d surveys were successfully marked as published', + count) % {'count': count,} + modeladmin.message_user(request, message) +make_published.short_description = ugettext_lazy(u"Mark selected surveys as published") \ No newline at end of file diff --git a/survey/admin.py b/survey/admin.py index 79bb8e7f..65dcbcc5 100644 --- a/survey/admin.py +++ b/survey/admin.py @@ -1,19 +1,22 @@ -from survey.models import Question, Category, Survey, Response, AnswerText, AnswerRadio, AnswerSelect, AnswerInteger, AnswerSelectMultiple from django.contrib import admin -from django.contrib.auth.admin import UserAdmin -from django.contrib.auth.models import User + +from .models import Question, Category, Survey, Response, AnswerText, AnswerRadio, AnswerSelect, AnswerInteger, AnswerSelectMultiple +from .actions import make_published class QuestionInline(admin.TabularInline): model = Question - ordering = ('category',) - extra = 0 + ordering = ('category', 'order',) + extra = 1 class CategoryInline(admin.TabularInline): model = Category extra = 0 class SurveyAdmin(admin.ModelAdmin): + list_display = ('name', 'is_published') + list_filter = ('is_published',) inlines = [CategoryInline, QuestionInline] + actions = [make_published,] class AnswerBaseInline(admin.StackedInline): fields = ('question', 'body') @@ -36,7 +39,9 @@ class AnswerIntegerInline(AnswerBaseInline): model= AnswerInteger class ResponseAdmin(admin.ModelAdmin): - list_display = ('interview_uuid', 'interviewer', 'created') + list_display = ('interview_uuid', 'survey', 'created') + list_filter = ('survey', 'created') + date_hierarchy = 'created' inlines = [AnswerTextInline, AnswerRadioInline, AnswerSelectInline, AnswerSelectMultipleInline, AnswerIntegerInline] # specifies the order as well as which fields to act on readonly_fields = ('survey', 'created', 'updated', 'interview_uuid') diff --git a/survey/forms.py b/survey/forms.py index c00deba9..015f300d 100644 --- a/survey/forms.py +++ b/survey/forms.py @@ -1,116 +1,124 @@ +import uuid +import logging + from django import forms from django.forms import models -from survey.models import Question, Category, Survey, Response, AnswerText, AnswerRadio, AnswerSelect, AnswerInteger, AnswerSelectMultiple +from survey.models import Question, Response +from survey.models import AnswerText, AnswerRadio, AnswerSelect +from survey.models import AnswerInteger, AnswerSelectMultiple from django.utils.safestring import mark_safe -import uuid # blatantly stolen from # http://stackoverflow.com/questions/5935546/align-radio-buttons-horizontally-in-django-forms?rq=1 class HorizontalRadioRenderer(forms.RadioSelect.renderer): - def render(self): - return mark_safe(u'\n'.join([u'%s\n' % w for w in self])) + def render(self): + return mark_safe(u'\n'.join([u'%s\n' % w for w in self])) class ResponseForm(models.ModelForm): - class Meta: - model = Response - fields = ('interviewer', 'interviewee', 'conditions', 'comments') + class Meta: + model = Response + fields = () - def __init__(self, *args, **kwargs): - # expects a survey object to be passed in initially - survey = kwargs.pop('survey') - self.survey = survey - super(ResponseForm, self).__init__(*args, **kwargs) - self.uuid = random_uuid = uuid.uuid4().hex + def __init__(self, *args, **kwargs): + # expects a survey object to be passed in initially + survey = kwargs.pop('survey') + self.survey = survey + super(ResponseForm, self).__init__(*args, **kwargs) + random_uuid = uuid.uuid4().hex + self.uuid = random_uuid - # add a field for each survey question, corresponding to the question - # type as appropriate. - data = kwargs.get('data') - for q in survey.questions(): - if q.question_type == Question.TEXT: - self.fields["question_%d" % q.pk] = forms.CharField(label=q.text, - widget=forms.Textarea) - elif q.question_type == Question.RADIO: - question_choices = q.get_choices() - self.fields["question_%d" % q.pk] = forms.ChoiceField(label=q.text, - widget=forms.RadioSelect(renderer=HorizontalRadioRenderer), - choices = question_choices) - elif q.question_type == Question.SELECT: - question_choices = q.get_choices() - # add an empty option at the top so that the user has to - # explicitly select one of the options - question_choices = tuple([('', '-------------')]) + question_choices - self.fields["question_%d" % q.pk] = forms.ChoiceField(label=q.text, - widget=forms.Select, choices = question_choices) - elif q.question_type == Question.SELECT_MULTIPLE: - question_choices = q.get_choices() - self.fields["question_%d" % q.pk] = forms.MultipleChoiceField(label=q.text, - widget=forms.CheckboxSelectMultiple, choices = question_choices) - elif q.question_type == Question.INTEGER: - self.fields["question_%d" % q.pk] = forms.IntegerField(label=q.text) - - # if the field is required, give it a corresponding css class. - if q.required: - self.fields["question_%d" % q.pk].required = True - self.fields["question_%d" % q.pk].widget.attrs["class"] = "required" - else: - self.fields["question_%d" % q.pk].required = False - - # add the category as a css class, and add it as a data attribute - # as well (this is used in the template to allow sorting the - # questions by category) - if q.category: - classes = self.fields["question_%d" % q.pk].widget.attrs.get("class") - if classes: - self.fields["question_%d" % q.pk].widget.attrs["class"] = classes + (" cat_%s" % q.category.name) - else: - self.fields["question_%d" % q.pk].widget.attrs["class"] = (" cat_%s" % q.category.name) - self.fields["question_%d" % q.pk].widget.attrs["category"] = q.category.name + # add a field for each survey question, corresponding to the question + # type as appropriate. + data = kwargs.get('data') + for q in survey.questions(): + if q.question_type == Question.TEXT: + self.fields["question_%d" % q.pk] = forms.CharField(label=q.text, + widget=forms.Textarea) + elif q.question_type == Question.SHORT_TEXT: + self.fields["question_%d" % q.pk] = forms.CharField(label=q.text, + widget=forms.TextInput) + elif q.question_type == Question.RADIO: + question_choices = q.get_choices() + self.fields["question_%d" % q.pk] = forms.ChoiceField(label=q.text, + widget=forms.RadioSelect(renderer=HorizontalRadioRenderer), + choices = question_choices) + elif q.question_type == Question.SELECT: + question_choices = q.get_choices() + # add an empty option at the top so that the user has to + # explicitly select one of the options + question_choices = tuple([('', '-------------')]) + question_choices + self.fields["question_%d" % q.pk] = forms.ChoiceField(label=q.text, + widget=forms.Select, choices = question_choices) + elif q.question_type == Question.SELECT_MULTIPLE: + question_choices = q.get_choices() + self.fields["question_%d" % q.pk] = forms.MultipleChoiceField(label=q.text, + widget=forms.CheckboxSelectMultiple, choices = question_choices) + elif q.question_type == Question.INTEGER: + self.fields["question_%d" % q.pk] = forms.IntegerField(label=q.text) + + # if the field is required, give it a corresponding css class. + if q.required: + self.fields["question_%d" % q.pk].required = True + self.fields["question_%d" % q.pk].widget.attrs["class"] = "required" + else: + self.fields["question_%d" % q.pk].required = False + + # add the category as a css class, and add it as a data attribute + # as well (this is used in the template to allow sorting the + # questions by category) + if q.category: + classes = self.fields["question_%d" % q.pk].widget.attrs.get("class") + if classes: + self.fields["question_%d" % q.pk].widget.attrs["class"] = classes + (" cat_%s" % q.category.name) + else: + self.fields["question_%d" % q.pk].widget.attrs["class"] = (" cat_%s" % q.category.name) + self.fields["question_%d" % q.pk].widget.attrs["category"] = q.category.name - # initialize the form field with values from a POST request, if any. - if data: - self.fields["question_%d" % q.pk].initial = data.get('question_%d' % q.pk) + # initialize the form field with values from a POST request, if any. + if data: + self.fields["question_%d" % q.pk].initial = data.get('question_%d' % q.pk) - def save(self, commit=True): - # save the response object - response = super(ResponseForm, self).save(commit=False) - response.survey = self.survey - response.interview_uuid = self.uuid - response.save() + def save(self, commit=True): + # save the response object + response = super(ResponseForm, self).save(commit=False) + response.survey = self.survey + response.interview_uuid = self.uuid + response.save() - # create an answer object for each question and associate it with this - # response. - for field_name, field_value in self.cleaned_data.iteritems(): - if field_name.startswith("question_"): - # warning: this way of extracting the id is very fragile and - # entirely dependent on the way the question_id is encoded in the - # field name in the __init__ method of this form class. - q_id = int(field_name.split("_")[1]) - q = Question.objects.get(pk=q_id) - - if q.question_type == Question.TEXT: - a = AnswerText(question = q) - a.body = field_value - elif q.question_type == Question.RADIO: - a = AnswerRadio(question = q) - a.body = field_value - elif q.question_type == Question.SELECT: - a = AnswerSelect(question = q) - a.body = field_value - elif q.question_type == Question.SELECT_MULTIPLE: - a = AnswerSelectMultiple(question = q) - a.body = field_value - elif q.question_type == Question.INTEGER: - a = AnswerInteger(question = q) - a.body = field_value - print "creating answer to question %d of type %s" % (q_id, a.question.question_type) - print a.question.text - print 'answer value:' - print field_value - a.response = response - a.save() - return response + # create an answer object for each question and associate it with this + # response. + for field_name, field_value in self.cleaned_data.iteritems(): + if field_name.startswith("question_"): + # warning: this way of extracting the id is very fragile and + # entirely dependent on the way the question_id is encoded in the + # field name in the __init__ method of this form class. + q_id = int(field_name.split("_")[1]) + q = Question.objects.get(pk=q_id) + + if q.question_type == Question.TEXT or q.question_type == Question.SHORT_TEXT: + a = AnswerText(question = q) + a.body = field_value + elif q.question_type == Question.RADIO: + a = AnswerRadio(question = q) + a.body = field_value + elif q.question_type == Question.SELECT: + a = AnswerSelect(question = q) + a.body = field_value + elif q.question_type == Question.SELECT_MULTIPLE: + a = AnswerSelectMultiple(question = q) + a.body = field_value + elif q.question_type == Question.INTEGER: + a = AnswerInteger(question = q) + a.body = field_value + logging.debug("creating answer to question %d of type %s" % (q_id, a.question.question_type)) + logging.debug(a.question.text) + logging.debug('answer value:') + logging.debug(field_value) + a.response = response + a.save() + return response diff --git a/survey/models.py b/survey/models.py index 0a07f995..d7e96322 100644 --- a/survey/models.py +++ b/survey/models.py @@ -1,18 +1,25 @@ from django.db import models -from django.core.exceptions import ValidationError + +from .utils import validate_list class Survey(models.Model): name = models.CharField(max_length=400) description = models.TextField() + is_published = models.BooleanField() def __unicode__(self): return (self.name) + @models.permalink + def get_absolute_url(self): + return ('survey_detail', [self.id]) + def questions(self): if self.pk: return Question.objects.filter(survey=self.pk) else: - return None + return Question.objects.none() + class Category(models.Model): name = models.CharField(max_length=400) @@ -21,21 +28,18 @@ class Category(models.Model): def __unicode__(self): return (self.name) -def validate_list(value): - '''takes a text value and verifies that there is at least one comma ''' - values = value.split(',') - if len(values) < 2: - raise ValidationError("The selected field requires an associated list of choices. Choices must contain more than one item.") class Question(models.Model): TEXT = 'text' + SHORT_TEXT = 'short-text' RADIO = 'radio' SELECT = 'select' SELECT_MULTIPLE = 'select-multiple' INTEGER = 'integer' QUESTION_TYPES = ( - (TEXT, 'text'), + (TEXT, 'text (multiple line)'), + (SHORT_TEXT, 'short text (one line)'), (RADIO, 'radio'), (SELECT, 'select'), (SELECT_MULTIPLE, 'Select Multiple'), @@ -43,6 +47,7 @@ class Question(models.Model): ) text = models.TextField() + order = models.IntegerField() required = models.BooleanField() category = models.ForeignKey(Category, blank=True, null=True,) survey = models.ForeignKey(Survey) @@ -71,21 +76,22 @@ def get_choices(self): def __unicode__(self): return (self.text) + class Meta: + ordering = ('survey', 'order') + + class Response(models.Model): # a response object is just a collection of questions and answers with a # unique interview uuid created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) survey = models.ForeignKey(Survey) - interviewer = models.CharField('Name of Interviewer', max_length=400) - interviewee = models.CharField('Name of Interviewee', max_length=400) - conditions = models.TextField('Conditions during interview', blank=True, null=True) - comments = models.TextField('Any additional Comments', blank=True, null=True) interview_uuid = models.CharField("Interview unique identifier", max_length=36) def __unicode__(self): return ("response %s" % self.interview_uuid) + class AnswerBase(models.Model): question = models.ForeignKey(Question) response = models.ForeignKey(Response) @@ -109,11 +115,3 @@ class AnswerSelectMultiple(AnswerBase): class AnswerInteger(AnswerBase): body = models.IntegerField(blank=True, null=True) - - - - - - - - diff --git a/survey/settings.py b/survey/settings.py index a854ff1c..54aec9b5 100644 --- a/survey/settings.py +++ b/survey/settings.py @@ -59,7 +59,7 @@ # URL that handles the media served from MEDIA_ROOT. Make sure to use a # trailing slash. # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" -MEDIA_URL = '/media' +MEDIA_URL = '/media/' # Absolute path to the directory static files should be collected to. # Don't put anything in this directory yourself; store your static files diff --git a/survey/templatetags/survey_extras.py b/survey/templatetags/survey_extras.py index 7e616e2d..f4d8378f 100644 --- a/survey/templatetags/survey_extras.py +++ b/survey/templatetags/survey_extras.py @@ -4,13 +4,13 @@ class CounterNode(template.Node): - def __init__(self): - self.count = 0 + def __init__(self): + self.count = 0 - def render(self, context): - self.count += 1 - return self.count + def render(self, context): + self.count += 1 + return self.count @register.tag def counter(parser, token): - return CounterNode() \ No newline at end of file + return CounterNode() diff --git a/survey/urls.py b/survey/urls.py index 08e88903..804d1c7a 100644 --- a/survey/urls.py +++ b/survey/urls.py @@ -4,18 +4,16 @@ from django.contrib import admin import settings +from .views import IndexView, SurveyDetail, ConfirmView + admin.autodiscover() media_url = settings.MEDIA_URL.lstrip('/').rstrip('/') urlpatterns = patterns('', # Examples: - url(r'^$', 'survey.views.Index', name='home'), - url(r'^survey/(?P\d+)/$', 'survey.views.SurveyDetail', name='survey_detail'), - url(r'^confirm/(?P\w+)/$', 'survey.views.Confirm', name='confirmation'), - - - # Uncomment the admin/doc line below to enable admin documentation: - url(r'^admin/doc/', include('django.contrib.admindocs.urls')), + url(r'^$', IndexView.as_view(), name='home'), + url(r'^survey/(?P\d+)/$', SurveyDetail.as_view(), name='survey_detail'), + url(r'^confirm/(?P\w+)/$', ConfirmView.as_view(), name='confirmation'), # Uncomment the next line to enable the admin: url(r'^admin/', include(admin.site.urls)), diff --git a/survey/utils.py b/survey/utils.py new file mode 100644 index 00000000..529a7d08 --- /dev/null +++ b/survey/utils.py @@ -0,0 +1,9 @@ +from django.core.exceptions import ValidationError + +def validate_list(value): + """ + takes a text value and verifies that there is at least one comma + """ + values = value.split(',') + if len(values) < 2: + raise ValidationError("The selected field requires an associated list of choices. Choices must contain more than one item.") diff --git a/survey/views.py b/survey/views.py index e4bce57b..ce312ed0 100644 --- a/survey/views.py +++ b/survey/views.py @@ -1,39 +1,48 @@ -from django.shortcuts import render -from django.http import HttpResponse, HttpResponseRedirect -from django.shortcuts import render_to_response -from django.template import RequestContext -from django.core import urlresolvers -from django.contrib import messages -import datetime -import settings - -from models import Question, Survey, Category -from forms import ResponseForm - - -def Index(request): - return render(request, 'index.html') - -def SurveyDetail(request, id): - survey = Survey.objects.get(id=id) - category_items = Category.objects.filter(survey=survey) - categories = [c.name for c in category_items] - print 'categories for this survey:' - print categories - if request.method == 'POST': - form = ResponseForm(request.POST, survey=survey) - if form.is_valid(): - response = form.save() - return HttpResponseRedirect("/confirm/%s" % response.interview_uuid) - else: - form = ResponseForm(survey=survey) - print form - # TODO sort by category - return render(request, 'survey.html', {'response_form': form, 'survey': survey, 'categories': categories}) - -def Confirm(request, uuid): - email = settings.support_email - return render(request, 'confirm.html', {'uuid':uuid, "email": email}) +from django.shortcuts import render, get_object_or_404 +from django.http import HttpResponseRedirect +from .models import Survey, Category +from .forms import ResponseForm +from django.views.generic import TemplateView, View + +class IndexView(TemplateView): + template_name = "index.html" + + def get_context_data(self, **kwargs): + context = super(IndexView, self).get_context_data(**kwargs) + context['surveys'] = Survey.objects.filter(is_published=True) + return context + + +class SurveyDetail(View): + template_name = 'survey.html' + + def get(self, request, *args, **kwargs): + survey = get_object_or_404(Survey, is_published=True, id=kwargs['id']) + category_items = Category.objects.filter(survey=survey) + categories = [c.name for c in category_items] + form = ResponseForm(survey=survey) + context = {'response_form': form, 'survey': survey, 'categories': categories} + return render(request, self.template_name, context) + + def post(self, request, *args, **kwargs): + survey = get_object_or_404(Survey, is_published=True, id=kwargs['id']) + category_items = Category.objects.filter(survey=survey) + categories = [c.name for c in category_items] + form = ResponseForm(request.POST, survey=survey) + if form.is_valid(): + response = form.save() + return HttpResponseRedirect("/confirm/%s" % response.interview_uuid) + + context = {'response_form': form, 'survey': survey, 'categories': categories} + return render(request, self.template_name, context) + +class ConfirmView(TemplateView): + template_name = 'confirm.html' + + def get_context_data(self, **kwargs): + context = super(ConfirmView, self).get_context_data(**kwargs) + context['uuid'] = kwargs['uuid'] + return context diff --git a/templates/base.html b/templates/base.html index 91ec6bc7..68f72d9b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -2,7 +2,7 @@ - Embassy Network + {% block title %}{% endblock title %} diff --git a/templates/confirm.html b/templates/confirm.html index 2528f9a0..7d38ed0e 100644 --- a/templates/confirm.html +++ b/templates/confirm.html @@ -1,6 +1,10 @@ {% extends 'base.html' %} -{% block body %} +{% block title %}Survey submitted{% endblock title %} -Thanks! Your confirmation code is {{uuid}}. Use this to refer to your response. In case you have any questions, contact {{ email }} +{% block body %} +

Survey submitted

+

+ Thanks! Your confirmation code is {{uuid}}. Use this to refer to your response. +

{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 8d3eac2b..75355877 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,21 +1,13 @@ {% extends 'base.html' %} {% block body %} -
-

Welcome to the Coliving Census

- -
-

We’re interested in understanding the ecosystem and archetypes of the coliving movement, in order to make it easier to start and operate coliving houses, and to create a picture of the amazing spectrum of these spaces.

+

Welcome to the Django Survey app

-

Similar to the way the US government does a census of the US population every few years, we are undertaking a “Coliving Census,” to capture the diversity, challenges, and opportunities of these spaces. The result will be aggregated into an anonymized, open data set about coliving spaces, as well as a report going into more detail.

- -

This is an entirely not-for-profit, community initiative, motivated by a belief that there are more and better ways to live than the nuclear family. We want to get a better picture of the communities making this happen.

+
+ {% for survey in surveys %} + Take the Survey {{survey.name}}{% if not forloop.last %} || {% endif %} + {% endfor %} +
- - -
- {% endblock %} diff --git a/templates/survey.html b/templates/survey.html index d7be1c2d..aa0e57bb 100644 --- a/templates/survey.html +++ b/templates/survey.html @@ -1,6 +1,8 @@ {% extends 'base.html' %} {% load survey_extras %} +{% block title %}{{survey.name|title}}{% endblock title %} + {% block body %}

Welcome to {{survey.name|title}}

@@ -9,47 +11,7 @@

Welcome to {{survey.name|title}}

-
{% csrf_token %} - -

Response Metadata

-
- -
- {{ response_form.interviewer.errors }} - * {{ response_form.interviewer.label_tag }} - {{ response_form.interviewer.help_text}} -
- {{ response_form.interviewer }} -
-
- -
- {{ response_form.interviewee.errors }} - * {{ response_form.interviewee.label_tag }} - {{ response_form.interviewee.help_text}} -
- {{ response_form.interviewee }} -
-
- -
- {{ response_form.conditions.errors }} - {{ response_form.conditions.label_tag }} - {{ response_form.conditions.help_text}} -
- {{ response_form.conditions }} -
-
- -
- {{ response_form.comments.errors }} - {{ response_form.comments.label_tag }} - {{ response_form.comments.help_text}} -
- {{ response_form.comments }} -
-
-
+ {% csrf_token %}
    {% for category in categories %} @@ -57,7 +19,7 @@

    {{category|title}} Questions

    {% for field in response_form %} {% if field.field.widget.attrs.category == category %} -
  1. +
  2. {% if field.field.required %}
    {{ field.errors }} @@ -77,21 +39,32 @@

    {{category|title}} Questions

    {% endfor %}
    {% endfor %} + +
    + {% for field in response_form %} + {% if not field.field.widget.attrs.category %} +
  3. + {% if field.field.required %} +
    + {{ field.errors }} + * + {% else %} +
    + {{ field.errors }} + + {% endif %} + {{ field.help_text}} +
    + {{ field }} +
    +
    +
  4. + {% endif %} + {% endfor %} +
-
- -{% endblock %} - -{% block extrajs %} - - {% endblock %} \ No newline at end of file From ed3bcc5dac19d031cc97c7087c52a01eabd7cf9a Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Wed, 4 Sep 2013 17:28:40 +0200 Subject: [PATCH 03/27] * Improved urls handling --- survey/models.py | 2 +- survey/urls.py | 6 +++--- survey/views.py | 6 +++--- templates/confirm.html | 3 +++ templates/index.html | 4 ++-- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/survey/models.py b/survey/models.py index d7e96322..e3f3c7ac 100644 --- a/survey/models.py +++ b/survey/models.py @@ -12,7 +12,7 @@ def __unicode__(self): @models.permalink def get_absolute_url(self): - return ('survey_detail', [self.id]) + return ('survey-detail', [self.id]) def questions(self): if self.pk: diff --git a/survey/urls.py b/survey/urls.py index 804d1c7a..eb166c28 100644 --- a/survey/urls.py +++ b/survey/urls.py @@ -11,9 +11,9 @@ urlpatterns = patterns('', # Examples: - url(r'^$', IndexView.as_view(), name='home'), - url(r'^survey/(?P\d+)/$', SurveyDetail.as_view(), name='survey_detail'), - url(r'^confirm/(?P\w+)/$', ConfirmView.as_view(), name='confirmation'), + url(r'^$', IndexView.as_view(), name='survey-list'), + url(r'^survey/(?P\d+)/$', SurveyDetail.as_view(), name='survey-detail'), + url(r'^confirm/(?P\w+)/$', ConfirmView.as_view(), name='survey-confirmation'), # Uncomment the next line to enable the admin: url(r'^admin/', include(admin.site.urls)), diff --git a/survey/views.py b/survey/views.py index ce312ed0..3594ed93 100644 --- a/survey/views.py +++ b/survey/views.py @@ -1,5 +1,4 @@ -from django.shortcuts import render, get_object_or_404 -from django.http import HttpResponseRedirect +from django.shortcuts import render, get_object_or_404, redirect from .models import Survey, Category from .forms import ResponseForm @@ -34,11 +33,12 @@ def post(self, request, *args, **kwargs): form = ResponseForm(request.POST, survey=survey) if form.is_valid(): response = form.save() - return HttpResponseRedirect("/confirm/%s" % response.interview_uuid) + return redirect('survey-confirmation', uuid=response.interview_uuid) context = {'response_form': form, 'survey': survey, 'categories': categories} return render(request, self.template_name, context) + class ConfirmView(TemplateView): template_name = 'confirm.html' diff --git a/templates/confirm.html b/templates/confirm.html index 7d38ed0e..cffcb028 100644 --- a/templates/confirm.html +++ b/templates/confirm.html @@ -7,4 +7,7 @@

Survey submitted

Thanks! Your confirmation code is {{uuid}}. Use this to refer to your response.

+

+ Back to survey list +

{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 75355877..ebacf50c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,9 +4,9 @@

Welcome to the Django Survey app

-
+
From 8500ed009969c0e858366179a03337c0865b341d Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Wed, 4 Sep 2013 18:00:12 +0200 Subject: [PATCH 04/27] * Added some translations (fr-FR) --- survey/locale/fr/LC_MESSAGES/django.mo | Bin 0 -> 1616 bytes survey/locale/fr/LC_MESSAGES/django.po | 98 ++++++++++++++ survey/models.py | 179 ++++++++++++++----------- 3 files changed, 195 insertions(+), 82 deletions(-) create mode 100644 survey/locale/fr/LC_MESSAGES/django.mo create mode 100644 survey/locale/fr/LC_MESSAGES/django.po diff --git a/survey/locale/fr/LC_MESSAGES/django.mo b/survey/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..d7b213c4b3be8e3abdfa55d55f800b1a778f47c7 GIT binary patch literal 1616 zcmZXTO>Y}T7{`ZFD7d_ostCl3^uPf-DjS^e(y~dax`|UP>^O3qN^qOlkJnSyJIl~8+;bLQ>_06y`Q__1@K?+CSsm>Scq4!Hm8Mn4g4H@4*Ur`3;quJ zy#4}z1@D61&yNW1^ZW($`P>Gd0Dmj&e-!qAz-M5;2YSD!kld5t3*b{=4SXKF1R|Z> zr}yIrj^xAV;bZ#YWBK!bK+(#Iczw{)Dnb zeVj3=Atz3a6dTcz<|DEk^T6?tY-Xl>Y)M*5v!htqF%QK-{*^je>{J@F7)ZyFHj3?H zdMwn4+?WZ@ty4N7cbp=OrK*ubrK_(~bshsVZ59if6fUcj8a+i;Bx#^06B$@er9qq_ zAqhu%L^}13Esb#Ajp4eCwJH<}j8M$NR8FkO;|s$!#bGWdR?PDecC3vf$G05SbixRi z@QRrCp@0?h&x>q}J^M!AMAw4WFu1|SBDs+E8{6+Ut~cpMvv<(y?p1o6!hO)Oks1cq zvd9K~9a3e#+Yh!4OJ7m2gEB)}udlrs)ZY#2Z_(PN@a=al*4OKGYz%rlRo;2A?}KKe z7j(M8PJ`a-hMm^M54(HKN?TeN^o>kxES)wX?H;z~!~Sk?dAZN0HDVJqlYt(pBns*Bk#d#&I5RR1cD0$H=~?baqYfJMA^~B#1>7vuQJicb@8puaCK_)@R7eZixl(L|9@qRLpA(d Ot2i?#LE(^H5&r@7-oiWp literal 0 HcmV?d00001 diff --git a/survey/locale/fr/LC_MESSAGES/django.po b/survey/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 00000000..2272c1a3 --- /dev/null +++ b/survey/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,98 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-09-04 17:56+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: actions.py:10 +#, python-format +msgid "%(count)d survey was successfully marked as published." +msgid_plural "%(count)d surveys were successfully marked as published" +msgstr[0] "" +msgstr[1] "" + +#: actions.py:14 +msgid "Mark selected surveys as published" +msgstr "Marquer le questionnaire comme publié" + +#: models.py:12 +msgid "survey" +msgstr "questionnaire" + +#: models.py:13 +msgid "surveys" +msgstr "questionnaires" + +#: models.py:34 +msgid "category" +msgstr "catégorie" + +#: models.py:35 +msgid "categories" +msgstr "catégories" + +#: models.py:50 +msgid "text (multiple line)" +msgstr "Texte (plusieurs lignes)" + +#: models.py:51 +msgid "short text (one line)" +msgstr "Texte court (une ligne)" + +#: models.py:52 +msgid "radio" +msgstr "Bouton radio" + +#: models.py:53 +msgid "select" +msgstr "Liste déroulante" + +#: models.py:54 +msgid "Select Multiple" +msgstr "Liste à choix multiple" + +#: models.py:55 +msgid "integer" +msgstr "Chiffre" + +#: models.py:66 +msgid "" +"if the question type is 'radio', 'select', or 'select multiple' provide a " +"comma-separated list of options for this question ." +msgstr "" +"si la question est de type 'bouton radio', 'liste déroulante', ou 'liste à choix multiple' il faut fournir " +"la liste des options pour cette question sous la forme d'une liste séparé par des virgules." + +#: models.py:69 +msgid "question" +msgstr "question" + +#: models.py:70 +msgid "questions" +msgstr "questions" + +#: models.py:100 +msgid "Interview unique identifier" +msgstr "Identifiant unique de la réponse" + +#: models.py:103 +msgid "response" +msgstr "réponse" + +#: models.py:104 +msgid "responses" +msgstr "réponses" diff --git a/survey/models.py b/survey/models.py index e3f3c7ac..5552d9cb 100644 --- a/survey/models.py +++ b/survey/models.py @@ -1,117 +1,132 @@ from django.db import models +from django.utils.translation import ugettext_lazy as _ from .utils import validate_list class Survey(models.Model): - name = models.CharField(max_length=400) - description = models.TextField() - is_published = models.BooleanField() + name = models.CharField(max_length=400) + description = models.TextField() + is_published = models.BooleanField() - def __unicode__(self): - return (self.name) + class Meta: + verbose_name = _('survey') + verbose_name_plural = _('surveys') - @models.permalink - def get_absolute_url(self): - return ('survey-detail', [self.id]) + def __unicode__(self): + return (self.name) - def questions(self): - if self.pk: - return Question.objects.filter(survey=self.pk) - else: - return Question.objects.none() + @models.permalink + def get_absolute_url(self): + return ('survey-detail', [self.id]) + + def questions(self): + if self.pk: + return Question.objects.filter(survey=self.pk) + else: + return Question.objects.none() class Category(models.Model): - name = models.CharField(max_length=400) - survey = models.ForeignKey(Survey) + name = models.CharField(max_length=400) + survey = models.ForeignKey(Survey) + + class Meta: + verbose_name = _('category') + verbose_name_plural = _('categories') - def __unicode__(self): - return (self.name) + def __unicode__(self): + return (self.name) class Question(models.Model): - TEXT = 'text' - SHORT_TEXT = 'short-text' - RADIO = 'radio' - SELECT = 'select' - SELECT_MULTIPLE = 'select-multiple' - INTEGER = 'integer' - - QUESTION_TYPES = ( - (TEXT, 'text (multiple line)'), - (SHORT_TEXT, 'short text (one line)'), - (RADIO, 'radio'), - (SELECT, 'select'), - (SELECT_MULTIPLE, 'Select Multiple'), - (INTEGER, 'integer'), - ) - - text = models.TextField() - order = models.IntegerField() - required = models.BooleanField() - category = models.ForeignKey(Category, blank=True, null=True,) - survey = models.ForeignKey(Survey) - question_type = models.CharField(max_length=200, choices=QUESTION_TYPES, default=TEXT) - # the choices field is only used if the question type - choices = models.TextField(blank=True, null=True, - help_text='if the question type is "radio," "select," or "select multiple" provide a comma-separated list of options for this question .') - - def save(self, *args, **kwargs): - if (self.question_type == Question.RADIO or self.question_type == Question.SELECT - or self.question_type == Question.SELECT_MULTIPLE): - validate_list(self.choices) - super(Question, self).save(*args, **kwargs) - - def get_choices(self): - ''' parse the choices field and return a tuple formatted appropriately - for the 'choices' argument of a form widget.''' - choices = self.choices.split(',') - choices_list = [] - for c in choices: - c = c.strip() - choices_list.append((c,c)) - choices_tuple = tuple(choices_list) - return choices_tuple - - def __unicode__(self): - return (self.text) - - class Meta: - ordering = ('survey', 'order') + TEXT = 'text' + SHORT_TEXT = 'short-text' + RADIO = 'radio' + SELECT = 'select' + SELECT_MULTIPLE = 'select-multiple' + INTEGER = 'integer' + + QUESTION_TYPES = ( + (TEXT, _(u'text (multiple line)')), + (SHORT_TEXT, _(u'short text (one line)')), + (RADIO, _(u'radio')), + (SELECT, _(u'select')), + (SELECT_MULTIPLE, _(u'Select Multiple')), + (INTEGER, _(u'integer')), + ) + + text = models.TextField() + order = models.IntegerField() + required = models.BooleanField() + category = models.ForeignKey(Category, blank=True, null=True,) + survey = models.ForeignKey(Survey) + question_type = models.CharField(max_length=200, choices=QUESTION_TYPES, default=TEXT) + # the choices field is only used if the question type + choices = models.TextField(blank=True, null=True, + help_text=_(u"if the question type is 'radio', 'select', or 'select multiple' provide a comma-separated list of options for this question .")) + + class Meta: + verbose_name = _('question') + verbose_name_plural = _('questions') + ordering = ('survey', 'order') + + def save(self, *args, **kwargs): + if (self.question_type == Question.RADIO or self.question_type == Question.SELECT + or self.question_type == Question.SELECT_MULTIPLE): + validate_list(self.choices) + super(Question, self).save(*args, **kwargs) + + def get_choices(self): + ''' parse the choices field and return a tuple formatted appropriately + for the 'choices' argument of a form widget.''' + choices = self.choices.split(',') + choices_list = [] + for c in choices: + c = c.strip() + choices_list.append((c,c)) + choices_tuple = tuple(choices_list) + return choices_tuple + + def __unicode__(self): + return (self.text) class Response(models.Model): - # a response object is just a collection of questions and answers with a - # unique interview uuid - created = models.DateTimeField(auto_now_add=True) - updated = models.DateTimeField(auto_now=True) - survey = models.ForeignKey(Survey) - interview_uuid = models.CharField("Interview unique identifier", max_length=36) + # a response object is just a collection of questions and answers with a + # unique interview uuid + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + survey = models.ForeignKey(Survey) + interview_uuid = models.CharField(_(u"Interview unique identifier"), max_length=36) + + class Meta: + verbose_name = _('response') + verbose_name_plural = _('responses') - def __unicode__(self): - return ("response %s" % self.interview_uuid) + def __unicode__(self): + return ("response %s" % self.interview_uuid) class AnswerBase(models.Model): - question = models.ForeignKey(Question) - response = models.ForeignKey(Response) - created = models.DateTimeField(auto_now_add=True) - updated = models.DateTimeField(auto_now=True) + question = models.ForeignKey(Question) + response = models.ForeignKey(Response) + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) # these type-specific answer models use a text field to allow for flexible # field sizes depending on the actual question this answer corresponds to. any # "required" attribute will be enforced by the form. class AnswerText(AnswerBase): - body = models.TextField(blank=True, null=True) + body = models.TextField(blank=True, null=True) class AnswerRadio(AnswerBase): - body = models.TextField(blank=True, null=True) + body = models.TextField(blank=True, null=True) class AnswerSelect(AnswerBase): - body = models.TextField(blank=True, null=True) + body = models.TextField(blank=True, null=True) class AnswerSelectMultiple(AnswerBase): - body = models.TextField(blank=True, null=True) + body = models.TextField(blank=True, null=True) class AnswerInteger(AnswerBase): - body = models.IntegerField(blank=True, null=True) + body = models.IntegerField(blank=True, null=True) From cc009751e11f7afca494a544adc0d5913e139e20 Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Wed, 4 Sep 2013 18:16:39 +0200 Subject: [PATCH 05/27] * Separated the app (survey) from the project (survey_test) (It was the first step to allow the addition of a setup.py file) --- survey/urls.py | 17 -------- manage.py => survey_test/manage.py | 2 +- survey_test/survey_test/__init__.py | 0 .../survey_test}/settings.py | 25 ++++++------ .../survey_test/templates}/base.html | 0 .../survey_test/templates}/confirm.html | 0 .../survey_test/templates}/index.html | 0 .../survey_test/templates}/survey.html | 0 survey_test/survey_test/urls.py | 40 +++++++++++++++++++ {survey => survey_test/survey_test}/wsgi.py | 8 +++- 10 files changed, 59 insertions(+), 33 deletions(-) rename manage.py => survey_test/manage.py (70%) mode change 100755 => 100644 create mode 100644 survey_test/survey_test/__init__.py rename {survey => survey_test/survey_test}/settings.py (90%) rename {templates => survey_test/survey_test/templates}/base.html (100%) rename {templates => survey_test/survey_test/templates}/confirm.html (100%) rename {templates => survey_test/survey_test/templates}/index.html (100%) rename {templates => survey_test/survey_test/templates}/survey.html (100%) create mode 100644 survey_test/survey_test/urls.py rename {survey => survey_test/survey_test}/wsgi.py (72%) diff --git a/survey/urls.py b/survey/urls.py index eb166c28..6ce6437d 100644 --- a/survey/urls.py +++ b/survey/urls.py @@ -1,27 +1,10 @@ from django.conf.urls import patterns, include, url -# Uncomment the next two lines to enable the admin: -from django.contrib import admin -import settings - from .views import IndexView, SurveyDetail, ConfirmView -admin.autodiscover() -media_url = settings.MEDIA_URL.lstrip('/').rstrip('/') - urlpatterns = patterns('', # Examples: url(r'^$', IndexView.as_view(), name='survey-list'), url(r'^survey/(?P\d+)/$', SurveyDetail.as_view(), name='survey-detail'), url(r'^confirm/(?P\w+)/$', ConfirmView.as_view(), name='survey-confirmation'), - - # Uncomment the next line to enable the admin: - url(r'^admin/', include(admin.site.urls)), ) - -# media url hackery. le sigh. -urlpatterns += patterns('', - (r'^%s/(?P.*)$' % media_url, 'django.views.static.serve', - { 'document_root': settings.MEDIA_ROOT, 'show_indexes':True }), -) - diff --git a/manage.py b/survey_test/manage.py old mode 100755 new mode 100644 similarity index 70% rename from manage.py rename to survey_test/manage.py index 3cd91d73..ed774c0b --- a/manage.py +++ b/survey_test/manage.py @@ -3,7 +3,7 @@ import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "survey.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "survey_test.settings") from django.core.management import execute_from_command_line diff --git a/survey_test/survey_test/__init__.py b/survey_test/survey_test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/survey/settings.py b/survey_test/survey_test/settings.py similarity index 90% rename from survey/settings.py rename to survey_test/survey_test/settings.py index 54aec9b5..526eccc4 100644 --- a/survey/settings.py +++ b/survey_test/survey_test/settings.py @@ -26,14 +26,15 @@ } } +# Hosts/domain names that are valid for this site; required if DEBUG is False +# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts +ALLOWED_HOSTS = [] + # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. -# On Unix systems, a value of None will cause Django to use the same -# timezone as the operating system. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = 'America/Los_Angeles' +# In a Windows environment this must be set to your system time zone. +TIME_ZONE = 'America/Chicago' # Language code for this installation. All choices can be found here: # http://www.i18nguy.com/unicode/language-identifiers.html @@ -54,7 +55,7 @@ # Absolute filesystem path to the directory that will hold user-uploaded files. # Example: "/home/media/media.lawrence.com/media/" -MEDIA_ROOT = path("../media/") +MEDIA_ROOT = path("../../media/") # URL that handles the media served from MEDIA_ROOT. Make sure to use a # trailing slash. @@ -87,7 +88,7 @@ ) # Make this unique, and don't share it with anybody. -SECRET_KEY = 'rzk0!e3eav79hs51!m4g3w!&52&o5e8yj)g70!f8$v^_vz)p7-' +SECRET_KEY = 'js*79rk(+s+x9)8co+10$zghe2f)+33jd1l2m#f)vl+pvtj24e' # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( @@ -106,17 +107,18 @@ # 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) -ROOT_URLCONF = 'survey.urls' +ROOT_URLCONF = 'survey_test.urls' # Python dotted path to the WSGI application used by Django's runserver. -WSGI_APPLICATION = 'survey.wsgi.application' +WSGI_APPLICATION = 'survey_test.wsgi.application' TEMPLATE_DIRS = ( # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". # Always use forward slashes, even on Windows. # Don't forget to use absolute paths, not relative paths. - path("../templates/") + path("templates/"), ) +print TEMPLATE_DIRS INSTALLED_APPS = ( 'django.contrib.auth', @@ -130,9 +132,6 @@ 'survey', ) -support_email = "survey@coliving.org" - - # A sample logging configuration. The only tangible logging # performed by this configuration is to send an email to # the site admins on every HTTP 500 error when DEBUG=False. diff --git a/templates/base.html b/survey_test/survey_test/templates/base.html similarity index 100% rename from templates/base.html rename to survey_test/survey_test/templates/base.html diff --git a/templates/confirm.html b/survey_test/survey_test/templates/confirm.html similarity index 100% rename from templates/confirm.html rename to survey_test/survey_test/templates/confirm.html diff --git a/templates/index.html b/survey_test/survey_test/templates/index.html similarity index 100% rename from templates/index.html rename to survey_test/survey_test/templates/index.html diff --git a/templates/survey.html b/survey_test/survey_test/templates/survey.html similarity index 100% rename from templates/survey.html rename to survey_test/survey_test/templates/survey.html diff --git a/survey_test/survey_test/urls.py b/survey_test/survey_test/urls.py new file mode 100644 index 00000000..92d96bb6 --- /dev/null +++ b/survey_test/survey_test/urls.py @@ -0,0 +1,40 @@ +from django.conf.urls import patterns, include, url + +# Uncomment the next two lines to enable the admin: +from django.contrib import admin +admin.autodiscover() + +urlpatterns = patterns('', + # Examples: + # url(r'^$', 'survey_test.views.home', name='home'), + # url(r'^survey_test/', include('survey_test.foo.urls')), + + # Uncomment the admin/doc line below to enable admin documentation: + # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), + + # Uncomment the next line to enable the admin: + # url(r'^admin/', include(admin.site.urls)), +) + + +# Uncomment the next two lines to enable the admin: + +import settings + + + + +media_url = settings.MEDIA_URL.lstrip('/').rstrip('/') + +urlpatterns = patterns('', + # Examples: + url(r'^admin/', include(admin.site.urls)), + url(r'^', include('survey.urls'), name='survey-list'), + +) + +# media url hackery. le sigh. +urlpatterns += patterns('', + (r'^%s/(?P.*)$' % media_url, 'django.views.static.serve', + { 'document_root': settings.MEDIA_ROOT, 'show_indexes':True }), +) diff --git a/survey/wsgi.py b/survey_test/survey_test/wsgi.py similarity index 72% rename from survey/wsgi.py rename to survey_test/survey_test/wsgi.py index 2cfef593..7dab1847 100644 --- a/survey/wsgi.py +++ b/survey_test/survey_test/wsgi.py @@ -1,5 +1,5 @@ """ -WSGI config for survey project. +WSGI config for survey_test project. This module contains the WSGI application used by Django's development server and any production WSGI deployments. It should expose a module-level variable @@ -15,7 +15,11 @@ """ import os -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "survey.settings") +# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks +# if running multiple sites in the same mod_wsgi process. To fix this, use +# mod_wsgi daemon mode with each site in its own daemon process, or use +# os.environ["DJANGO_SETTINGS_MODULE"] = "survey_test.settings" +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "survey_test.settings") # This application object is used by any WSGI server configured to use this # file. This includes Django's development server, if the WSGI_APPLICATION From c5959720019fbcf486a7f468b3388f1b7c1cfee0 Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Thu, 5 Sep 2013 09:09:12 +0200 Subject: [PATCH 06/27] * Updated run server command in readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 9ec71873..8d0cab1c 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ install requirements using `pip install -r requirements.txt` -then run using `./manage.py runserver` +then run using `./survey_test/manage.py runserver` ## about From cc19e843e09d1ac92cff45f4497a7970edf44025 Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Fri, 20 Sep 2013 12:37:33 +0200 Subject: [PATCH 07/27] * Added ImageSelect widget and field type (allow to user to select a response with a picture instead of text" * Created a "raw" response that will later be sent with a signal after the creation of a new Response (allow other app to use the data submitted by the user) --- survey/forms.py | 23 ++++++++++++++ survey/models.py | 2 ++ survey/static/js/survey.js | 4 +++ .../templates/survey/forms/image_select.html | 6 ++++ survey/widgets.py | 31 +++++++++++++++++++ survey_test/survey_test/settings.py | 1 - survey_test/survey_test/templates/survey.html | 9 ++++-- 7 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 survey/static/js/survey.js create mode 100644 survey/templates/survey/forms/image_select.html create mode 100644 survey/widgets.py diff --git a/survey/forms.py b/survey/forms.py index 015f300d..73b39c1f 100644 --- a/survey/forms.py +++ b/survey/forms.py @@ -6,6 +6,8 @@ from survey.models import Question, Response from survey.models import AnswerText, AnswerRadio, AnswerSelect from survey.models import AnswerInteger, AnswerSelectMultiple +from survey.widgets import ImageSelectWidget + from django.utils.safestring import mark_safe # blatantly stolen from @@ -50,6 +52,13 @@ def __init__(self, *args, **kwargs): question_choices = tuple([('', '-------------')]) + question_choices self.fields["question_%d" % q.pk] = forms.ChoiceField(label=q.text, widget=forms.Select, choices = question_choices) + elif q.question_type == Question.SELECT_IMAGE: + question_choices = q.get_choices() + # add an empty option at the top so that the user has to + # explicitly select one of the options + question_choices = tuple([('', '-------------')]) + question_choices + self.fields["question_%d" % q.pk] = forms.ChoiceField(label=q.text, + widget=ImageSelectWidget, choices = question_choices) elif q.question_type == Question.SELECT_MULTIPLE: question_choices = q.get_choices() self.fields["question_%d" % q.pk] = forms.MultipleChoiceField(label=q.text, @@ -87,6 +96,12 @@ def save(self, commit=True): response.interview_uuid = self.uuid response.save() + # response "raw" data as dict (for signal) + data = { + 'survey_id': response.survey.id, + 'interview_uuid': response.interview_uuid, + 'responses': [] + } # create an answer object for each question and associate it with this # response. for field_name, field_value in self.cleaned_data.iteritems(): @@ -106,18 +121,26 @@ def save(self, commit=True): elif q.question_type == Question.SELECT: a = AnswerSelect(question = q) a.body = field_value + elif q.question_type == Question.SELECT_IMAGE: + a = AnswerSelect(question = q) + value, img_src = field_value.split(":", 1) + a.body = value elif q.question_type == Question.SELECT_MULTIPLE: a = AnswerSelectMultiple(question = q) a.body = field_value elif q.question_type == Question.INTEGER: a = AnswerInteger(question = q) a.body = field_value + data['responses'].append((a.question.id, a.body)) logging.debug("creating answer to question %d of type %s" % (q_id, a.question.question_type)) logging.debug(a.question.text) logging.debug('answer value:') logging.debug(field_value) + a.response = response a.save() + logging.debug("raw data") + logging.debug(data) return response diff --git a/survey/models.py b/survey/models.py index 5552d9cb..c07406be 100644 --- a/survey/models.py +++ b/survey/models.py @@ -43,6 +43,7 @@ class Question(models.Model): SHORT_TEXT = 'short-text' RADIO = 'radio' SELECT = 'select' + SELECT_IMAGE = 'select_image' SELECT_MULTIPLE = 'select-multiple' INTEGER = 'integer' @@ -52,6 +53,7 @@ class Question(models.Model): (RADIO, _(u'radio')), (SELECT, _(u'select')), (SELECT_MULTIPLE, _(u'Select Multiple')), + (SELECT_IMAGE, _(u'Select Image')), (INTEGER, _(u'integer')), ) diff --git a/survey/static/js/survey.js b/survey/static/js/survey.js new file mode 100644 index 00000000..98f3b41c --- /dev/null +++ b/survey/static/js/survey.js @@ -0,0 +1,4 @@ +function setResponse(field_id, value) { + $('#' + field_id).val(value); + return false; +} \ No newline at end of file diff --git a/survey/templates/survey/forms/image_select.html b/survey/templates/survey/forms/image_select.html new file mode 100644 index 00000000..29808204 --- /dev/null +++ b/survey/templates/survey/forms/image_select.html @@ -0,0 +1,6 @@ + +{% for choice in choices %} + + + +{% endfor %} diff --git a/survey/widgets.py b/survey/widgets.py new file mode 100644 index 00000000..344d650a --- /dev/null +++ b/survey/widgets.py @@ -0,0 +1,31 @@ +#! -*- encoding: utf-8 -*- +from django import forms +from django.template.loader import render_to_string +from django.utils.safestring import mark_safe + +from django.conf import settings + +class ImageSelectWidget(forms.widgets.Widget): + + class Media: + js = ( + 'http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js', + 'http://maps.googleapis.com/maps/api/js?sensor=false', + 'js/survey.js', + ) + + def render(self, name, value, *args, **kwargs): + template_name = "survey/forms/image_select.html" + choices = [] + for index, choice in enumerate(self.choices): + if choice[0] != '': + value, img_src = choice[0].split(":", 1) + choices.append({ + 'img_src':img_src, + 'value': value, + 'full_value': choice[0], + 'index':index + }) + context = {'name': name, 'choices': choices} + html = render_to_string(template_name, context) + return html \ No newline at end of file diff --git a/survey_test/survey_test/settings.py b/survey_test/survey_test/settings.py index 526eccc4..c0b90471 100644 --- a/survey_test/survey_test/settings.py +++ b/survey_test/survey_test/settings.py @@ -118,7 +118,6 @@ # Don't forget to use absolute paths, not relative paths. path("templates/"), ) -print TEMPLATE_DIRS INSTALLED_APPS = ( 'django.contrib.auth', diff --git a/survey_test/survey_test/templates/survey.html b/survey_test/survey_test/templates/survey.html index aa0e57bb..b5353844 100644 --- a/survey_test/survey_test/templates/survey.html +++ b/survey_test/survey_test/templates/survey.html @@ -3,6 +3,11 @@ {% block title %}{{survey.name|title}}{% endblock title %} +{% block extrajs %} + +{{response_form.media}} +{% endblock %} + {% block body %}

Welcome to {{survey.name|title}}

@@ -19,7 +24,7 @@

{{category|title}} Questions

{% for field in response_form %} {% if field.field.widget.attrs.category == category %} -
  • +
  • {% if field.field.required %}
    {{ field.errors }} @@ -43,7 +48,7 @@

    {{category|title}} Questions

    {% for field in response_form %} {% if not field.field.widget.attrs.category %} -
  • +
  • {% if field.field.required %}
    {{ field.errors }} From 8b27c56b0c78b7c67a88b52ea8565b113a40c05d Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Sat, 21 Sep 2013 23:32:20 +0200 Subject: [PATCH 08/27] * added a field to specify if a survey MUST be filled by a logged user --- survey/admin.py | 8 ++++---- survey/forms.py | 3 +++ survey/models.py | 16 +++++++++++----- survey/views.py | 17 +++++++++++++---- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/survey/admin.py b/survey/admin.py index 65dcbcc5..22f0e25e 100644 --- a/survey/admin.py +++ b/survey/admin.py @@ -13,8 +13,8 @@ class CategoryInline(admin.TabularInline): extra = 0 class SurveyAdmin(admin.ModelAdmin): - list_display = ('name', 'is_published') - list_filter = ('is_published',) + list_display = ('name', 'is_published', 'need_logged_user') + list_filter = ('is_published', 'need_logged_user') inlines = [CategoryInline, QuestionInline] actions = [make_published,] @@ -39,12 +39,12 @@ class AnswerIntegerInline(AnswerBaseInline): model= AnswerInteger class ResponseAdmin(admin.ModelAdmin): - list_display = ('interview_uuid', 'survey', 'created') + list_display = ('interview_uuid', 'survey', 'created', 'user') list_filter = ('survey', 'created') date_hierarchy = 'created' inlines = [AnswerTextInline, AnswerRadioInline, AnswerSelectInline, AnswerSelectMultipleInline, AnswerIntegerInline] # specifies the order as well as which fields to act on - readonly_fields = ('survey', 'created', 'updated', 'interview_uuid') + readonly_fields = ('survey', 'created', 'updated', 'interview_uuid', 'user') #admin.site.register(Question, QuestionInline) #admin.site.register(Category, CategoryInline) diff --git a/survey/forms.py b/survey/forms.py index 73b39c1f..516da04e 100644 --- a/survey/forms.py +++ b/survey/forms.py @@ -25,6 +25,7 @@ class Meta: def __init__(self, *args, **kwargs): # expects a survey object to be passed in initially survey = kwargs.pop('survey') + self.user = kwargs.pop('user') self.survey = survey super(ResponseForm, self).__init__(*args, **kwargs) random_uuid = uuid.uuid4().hex @@ -94,6 +95,8 @@ def save(self, commit=True): response = super(ResponseForm, self).save(commit=False) response.survey = self.survey response.interview_uuid = self.uuid + if self.user.is_authenticated(): + response.user = self.user response.save() # response "raw" data as dict (for signal) diff --git a/survey/models.py b/survey/models.py index c07406be..62ed246b 100644 --- a/survey/models.py +++ b/survey/models.py @@ -1,12 +1,13 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ - +from django.contrib.auth.models import User from .utils import validate_list class Survey(models.Model): name = models.CharField(max_length=400) description = models.TextField() is_published = models.BooleanField() + need_logged_user = models.BooleanField() class Meta: verbose_name = _('survey') @@ -79,8 +80,10 @@ def save(self, *args, **kwargs): super(Question, self).save(*args, **kwargs) def get_choices(self): - ''' parse the choices field and return a tuple formatted appropriately - for the 'choices' argument of a form widget.''' + """ + Parse the choices field and return a tuple formatted appropriately + for the 'choices' argument of a form widget. + """ choices = self.choices.split(',') choices_list = [] for c in choices: @@ -94,11 +97,14 @@ def __unicode__(self): class Response(models.Model): - # a response object is just a collection of questions and answers with a - # unique interview uuid + """ + A Response object is just a collection of questions and answers with a + unique interview uuid + """ created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) survey = models.ForeignKey(Survey) + user = models.ForeignKey(User, null=True, blank=True) interview_uuid = models.CharField(_(u"Interview unique identifier"), max_length=36) class Meta: diff --git a/survey/views.py b/survey/views.py index 3594ed93..d72ff609 100644 --- a/survey/views.py +++ b/survey/views.py @@ -1,4 +1,6 @@ -from django.shortcuts import render, get_object_or_404, redirect +from django.shortcuts import render +from django.shortcuts import get_object_or_404 +from django.shortcuts import redirect from .models import Survey, Category from .forms import ResponseForm @@ -11,7 +13,10 @@ class IndexView(TemplateView): def get_context_data(self, **kwargs): context = super(IndexView, self).get_context_data(**kwargs) - context['surveys'] = Survey.objects.filter(is_published=True) + qs = Survey.objects.filter(is_published=True) + if not self.request.user.is_authenticated(): + qs = qs.filter(need_logged_user=False) + context['surveys'] = qs return context @@ -20,17 +25,21 @@ class SurveyDetail(View): def get(self, request, *args, **kwargs): survey = get_object_or_404(Survey, is_published=True, id=kwargs['id']) + if survey.need_logged_user and not request.user.is_authenticated(): + return redirect('/login/?next=%s' % request.path) category_items = Category.objects.filter(survey=survey) categories = [c.name for c in category_items] - form = ResponseForm(survey=survey) + form = ResponseForm(survey=survey, user=request.user) context = {'response_form': form, 'survey': survey, 'categories': categories} return render(request, self.template_name, context) def post(self, request, *args, **kwargs): survey = get_object_or_404(Survey, is_published=True, id=kwargs['id']) + if survey.need_logged_user and not request.user.is_authenticated(): + return redirect('/login/?next=%s' % request.path) category_items = Category.objects.filter(survey=survey) categories = [c.name for c in category_items] - form = ResponseForm(request.POST, survey=survey) + form = ResponseForm(request.POST, survey=survey, user=request.user) if form.is_valid(): response = form.save() return redirect('survey-confirmation', uuid=response.interview_uuid) From 586d08f0c71490f816b73099f2f661142e808ffb Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Thu, 3 Oct 2013 14:18:04 +0200 Subject: [PATCH 09/27] * Saving the form at the last step for the step by step form. --- survey/forms.py | 137 +++++++++++------- survey/models.py | 2 + survey/urls.py | 1 + survey/views.py | 34 ++++- survey_test/survey_test/templates/survey.html | 3 +- 5 files changed, 115 insertions(+), 62 deletions(-) diff --git a/survey/forms.py b/survey/forms.py index 516da04e..fbfb1782 100644 --- a/survey/forms.py +++ b/survey/forms.py @@ -3,6 +3,8 @@ from django import forms from django.forms import models +from django.core.urlresolvers import reverse + from survey.models import Question, Response from survey.models import AnswerText, AnswerRadio, AnswerSelect from survey.models import AnswerInteger, AnswerSelectMultiple @@ -25,70 +27,96 @@ class Meta: def __init__(self, *args, **kwargs): # expects a survey object to be passed in initially survey = kwargs.pop('survey') - self.user = kwargs.pop('user') self.survey = survey + self.user = kwargs.pop('user') + try: + self.step = int(kwargs.pop('step')) + except KeyError: + self.step = None + + super(ResponseForm, self).__init__(*args, **kwargs) random_uuid = uuid.uuid4().hex self.uuid = random_uuid - + + self.steps_count = survey.questions().count() + # add a field for each survey question, corresponding to the question # type as appropriate. data = kwargs.get('data') - for q in survey.questions(): - if q.question_type == Question.TEXT: - self.fields["question_%d" % q.pk] = forms.CharField(label=q.text, - widget=forms.Textarea) - elif q.question_type == Question.SHORT_TEXT: - self.fields["question_%d" % q.pk] = forms.CharField(label=q.text, - widget=forms.TextInput) - elif q.question_type == Question.RADIO: - question_choices = q.get_choices() - self.fields["question_%d" % q.pk] = forms.ChoiceField(label=q.text, - widget=forms.RadioSelect(renderer=HorizontalRadioRenderer), - choices = question_choices) - elif q.question_type == Question.SELECT: - question_choices = q.get_choices() - # add an empty option at the top so that the user has to - # explicitly select one of the options - question_choices = tuple([('', '-------------')]) + question_choices - self.fields["question_%d" % q.pk] = forms.ChoiceField(label=q.text, - widget=forms.Select, choices = question_choices) - elif q.question_type == Question.SELECT_IMAGE: - question_choices = q.get_choices() - # add an empty option at the top so that the user has to - # explicitly select one of the options - question_choices = tuple([('', '-------------')]) + question_choices - self.fields["question_%d" % q.pk] = forms.ChoiceField(label=q.text, - widget=ImageSelectWidget, choices = question_choices) - elif q.question_type == Question.SELECT_MULTIPLE: - question_choices = q.get_choices() - self.fields["question_%d" % q.pk] = forms.MultipleChoiceField(label=q.text, - widget=forms.CheckboxSelectMultiple, choices = question_choices) - elif q.question_type == Question.INTEGER: - self.fields["question_%d" % q.pk] = forms.IntegerField(label=q.text) - - # if the field is required, give it a corresponding css class. - if q.required: - self.fields["question_%d" % q.pk].required = True - self.fields["question_%d" % q.pk].widget.attrs["class"] = "required" + for index, q in enumerate(survey.questions()): + if self.survey.display_by_question and index != self.step and self.step is not None: + continue else: - self.fields["question_%d" % q.pk].required = False + if q.question_type == Question.TEXT: + self.fields["question_%d" % q.pk] = forms.CharField(label=q.text, + widget=forms.Textarea) + elif q.question_type == Question.SHORT_TEXT: + self.fields["question_%d" % q.pk] = forms.CharField(label=q.text, + widget=forms.TextInput) + elif q.question_type == Question.RADIO: + question_choices = q.get_choices() + self.fields["question_%d" % q.pk] = forms.ChoiceField(label=q.text, + widget=forms.RadioSelect(renderer=HorizontalRadioRenderer), + choices = question_choices) + elif q.question_type == Question.SELECT: + question_choices = q.get_choices() + # add an empty option at the top so that the user has to + # explicitly select one of the options + question_choices = tuple([('', '-------------')]) + question_choices + self.fields["question_%d" % q.pk] = forms.ChoiceField(label=q.text, + widget=forms.Select, choices = question_choices) + elif q.question_type == Question.SELECT_IMAGE: + question_choices = q.get_choices() + # add an empty option at the top so that the user has to + # explicitly select one of the options + question_choices = tuple([('', '-------------')]) + question_choices + self.fields["question_%d" % q.pk] = forms.ChoiceField(label=q.text, + widget=ImageSelectWidget, choices = question_choices) + elif q.question_type == Question.SELECT_MULTIPLE: + question_choices = q.get_choices() + self.fields["question_%d" % q.pk] = forms.MultipleChoiceField(label=q.text, + widget=forms.CheckboxSelectMultiple, choices = question_choices) + elif q.question_type == Question.INTEGER: + self.fields["question_%d" % q.pk] = forms.IntegerField(label=q.text) - # add the category as a css class, and add it as a data attribute - # as well (this is used in the template to allow sorting the - # questions by category) - if q.category: - classes = self.fields["question_%d" % q.pk].widget.attrs.get("class") - if classes: - self.fields["question_%d" % q.pk].widget.attrs["class"] = classes + (" cat_%s" % q.category.name) + # if the field is required, give it a corresponding css class. + if q.required: + self.fields["question_%d" % q.pk].required = True + self.fields["question_%d" % q.pk].widget.attrs["class"] = "required" else: - self.fields["question_%d" % q.pk].widget.attrs["class"] = (" cat_%s" % q.category.name) - self.fields["question_%d" % q.pk].widget.attrs["category"] = q.category.name - - - # initialize the form field with values from a POST request, if any. - if data: - self.fields["question_%d" % q.pk].initial = data.get('question_%d' % q.pk) + self.fields["question_%d" % q.pk].required = False + + # add the category as a css class, and add it as a data attribute + # as well (this is used in the template to allow sorting the + # questions by category) + if q.category: + classes = self.fields["question_%d" % q.pk].widget.attrs.get("class") + if classes: + self.fields["question_%d" % q.pk].widget.attrs["class"] = classes + (" cat_%s" % q.category.name) + else: + self.fields["question_%d" % q.pk].widget.attrs["class"] = (" cat_%s" % q.category.name) + self.fields["question_%d" % q.pk].widget.attrs["category"] = q.category.name + + + # initialize the form field with values from a POST request, if any. + if data: + self.fields["question_%d" % q.pk].initial = data.get('question_%d' % q.pk) + + def has_next_step(self): + if self.survey.display_by_question: + if self.step < self.steps_count-1: + return True + return False + + def next_step_url(self): + if self.has_next_step(): + return reverse('survey-detail-step', kwargs={'id':self.survey.id, 'step': self.step+1}) + else: + return None + + def current_step_url(self): + return reverse('survey-detail-step', kwargs={'id':self.survey.id, 'step': self.step}) def save(self, commit=True): # save the response object @@ -150,3 +178,4 @@ def save(self, commit=True): + diff --git a/survey/models.py b/survey/models.py index 62ed246b..9643a200 100644 --- a/survey/models.py +++ b/survey/models.py @@ -3,11 +3,13 @@ from django.contrib.auth.models import User from .utils import validate_list + class Survey(models.Model): name = models.CharField(max_length=400) description = models.TextField() is_published = models.BooleanField() need_logged_user = models.BooleanField() + display_by_question = models.BooleanField() class Meta: verbose_name = _('survey') diff --git a/survey/urls.py b/survey/urls.py index 6ce6437d..d046d450 100644 --- a/survey/urls.py +++ b/survey/urls.py @@ -6,5 +6,6 @@ # Examples: url(r'^$', IndexView.as_view(), name='survey-list'), url(r'^survey/(?P\d+)/$', SurveyDetail.as_view(), name='survey-detail'), + url(r'^survey/(?P\d+)-(?P\d+)/$', SurveyDetail.as_view(), name='survey-detail-step'), url(r'^confirm/(?P\w+)/$', ConfirmView.as_view(), name='survey-confirmation'), ) diff --git a/survey/views.py b/survey/views.py index d72ff609..4ae4c6ee 100644 --- a/survey/views.py +++ b/survey/views.py @@ -29,8 +29,9 @@ def get(self, request, *args, **kwargs): return redirect('/login/?next=%s' % request.path) category_items = Category.objects.filter(survey=survey) categories = [c.name for c in category_items] - form = ResponseForm(survey=survey, user=request.user) + form = ResponseForm(survey=survey, user=request.user, step=kwargs.get('step', 0)) context = {'response_form': form, 'survey': survey, 'categories': categories} + return render(request, self.template_name, context) def post(self, request, *args, **kwargs): @@ -39,12 +40,33 @@ def post(self, request, *args, **kwargs): return redirect('/login/?next=%s' % request.path) category_items = Category.objects.filter(survey=survey) categories = [c.name for c in category_items] - form = ResponseForm(request.POST, survey=survey, user=request.user) - if form.is_valid(): - response = form.save() - return redirect('survey-confirmation', uuid=response.interview_uuid) - + form = ResponseForm(request.POST, survey=survey, user=request.user, step=kwargs.get('step', 0)) context = {'response_form': form, 'survey': survey, 'categories': categories} + if form.is_valid(): + session_key = 'survey_%s' % (kwargs['id'],) + if not session_key in request.session: + request.session[session_key] = {} + for key, value in form.cleaned_data.items(): + request.session[session_key][key] = value + request.session.modified = True + + next_url = form.next_step_url() + response = None + if survey.display_by_question: + if not form.has_next_step(): + save_form = ResponseForm(request.session[session_key], survey=survey, user=request.user) + response = save_form.save() + else: + response = form.save() + + if next_url is not None: + return redirect(next_url) + else: + del request.session[session_key] + if response is None: + return redirect('/') + else: + return redirect('survey-confirmation', uuid=response.interview_uuid) return render(request, self.template_name, context) diff --git a/survey_test/survey_test/templates/survey.html b/survey_test/survey_test/templates/survey.html index b5353844..96ffde64 100644 --- a/survey_test/survey_test/templates/survey.html +++ b/survey_test/survey_test/templates/survey.html @@ -16,8 +16,7 @@

    Welcome to {{survey.name|title}}

    -
    {% csrf_token %} - + {% csrf_token %}
      {% for category in categories %}

      {{category|title}} Questions

      From 266e769c38ae237ceb5a4f3e4341b7bfdd6b2554 Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Thu, 3 Oct 2013 23:22:13 +0200 Subject: [PATCH 10/27] * Added setup.py --- setup.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 setup.py diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..2476d48b --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +from distutils.core import setup + +setup( + name="survey", + version="0.1", + author="Jessy Kate Schingler", + author_email="jessy@jessykate.com", + license="AGPL", + url="https://github.com/jessykate/django-survey", + packages=[ + "survey", + ], + classifiers=[ + "Development Status :: 3 - Alpha", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Framework :: Django", + ] +) \ No newline at end of file From c7f14df107f9e78ef25a2a11bb2ec7c58e4a0628 Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Thu, 3 Oct 2013 23:38:02 +0200 Subject: [PATCH 11/27] * Updated template path --- survey/views.py | 6 +++--- survey_test/survey_test/templates/{ => survey}/confirm.html | 0 .../survey_test/templates/{index.html => survey/list.html} | 0 survey_test/survey_test/templates/{ => survey}/survey.html | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename survey_test/survey_test/templates/{ => survey}/confirm.html (100%) rename survey_test/survey_test/templates/{index.html => survey/list.html} (100%) rename survey_test/survey_test/templates/{ => survey}/survey.html (100%) diff --git a/survey/views.py b/survey/views.py index 4ae4c6ee..e5783422 100644 --- a/survey/views.py +++ b/survey/views.py @@ -9,7 +9,7 @@ from django.views.generic import TemplateView, View class IndexView(TemplateView): - template_name = "index.html" + template_name = "survey/list.html" def get_context_data(self, **kwargs): context = super(IndexView, self).get_context_data(**kwargs) @@ -21,7 +21,7 @@ def get_context_data(self, **kwargs): class SurveyDetail(View): - template_name = 'survey.html' + template_name = 'survey/survey.html' def get(self, request, *args, **kwargs): survey = get_object_or_404(Survey, is_published=True, id=kwargs['id']) @@ -71,7 +71,7 @@ def post(self, request, *args, **kwargs): class ConfirmView(TemplateView): - template_name = 'confirm.html' + template_name = 'survey/confirm.html' def get_context_data(self, **kwargs): context = super(ConfirmView, self).get_context_data(**kwargs) diff --git a/survey_test/survey_test/templates/confirm.html b/survey_test/survey_test/templates/survey/confirm.html similarity index 100% rename from survey_test/survey_test/templates/confirm.html rename to survey_test/survey_test/templates/survey/confirm.html diff --git a/survey_test/survey_test/templates/index.html b/survey_test/survey_test/templates/survey/list.html similarity index 100% rename from survey_test/survey_test/templates/index.html rename to survey_test/survey_test/templates/survey/list.html diff --git a/survey_test/survey_test/templates/survey.html b/survey_test/survey_test/templates/survey/survey.html similarity index 100% rename from survey_test/survey_test/templates/survey.html rename to survey_test/survey_test/templates/survey/survey.html From 3b9bc6de2651eef91bc8626b0665d7f85a9d0945 Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Thu, 10 Oct 2013 14:22:34 +0200 Subject: [PATCH 12/27] handling the case where someone has already taken as survey who need a logged user --- survey/urls.py | 3 ++- survey/views.py | 25 +++++++++++++++++-- .../templates/survey/completed.html | 13 ++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 survey_test/survey_test/templates/survey/completed.html diff --git a/survey/urls.py b/survey/urls.py index d046d450..159352f4 100644 --- a/survey/urls.py +++ b/survey/urls.py @@ -1,11 +1,12 @@ from django.conf.urls import patterns, include, url -from .views import IndexView, SurveyDetail, ConfirmView +from .views import IndexView, SurveyDetail, ConfirmView, SurveyCompleted urlpatterns = patterns('', # Examples: url(r'^$', IndexView.as_view(), name='survey-list'), url(r'^survey/(?P\d+)/$', SurveyDetail.as_view(), name='survey-detail'), + url(r'^survey/(?P\d+)/completed/$', SurveyCompleted.as_view(), name='survey-completed'), url(r'^survey/(?P\d+)-(?P\d+)/$', SurveyDetail.as_view(), name='survey-detail-step'), url(r'^confirm/(?P\w+)/$', ConfirmView.as_view(), name='survey-confirmation'), ) diff --git a/survey/views.py b/survey/views.py index e5783422..ffac7bd0 100644 --- a/survey/views.py +++ b/survey/views.py @@ -2,7 +2,7 @@ from django.shortcuts import get_object_or_404 from django.shortcuts import redirect -from .models import Survey, Category +from .models import Survey, Category, Response from .forms import ResponseForm @@ -27,10 +27,18 @@ def get(self, request, *args, **kwargs): survey = get_object_or_404(Survey, is_published=True, id=kwargs['id']) if survey.need_logged_user and not request.user.is_authenticated(): return redirect('/login/?next=%s' % request.path) + if survey.need_logged_user and request.user.is_authenticated(): + if Response.objects.filter(survey=survey, user=request.user).exists(): + return redirect('survey-completed', id=survey.id) + category_items = Category.objects.filter(survey=survey) categories = [c.name for c in category_items] form = ResponseForm(survey=survey, user=request.user, step=kwargs.get('step', 0)) - context = {'response_form': form, 'survey': survey, 'categories': categories} + context = { + 'response_form': form, + 'survey': survey, + 'categories': categories, + } return render(request, self.template_name, context) @@ -38,6 +46,9 @@ def post(self, request, *args, **kwargs): survey = get_object_or_404(Survey, is_published=True, id=kwargs['id']) if survey.need_logged_user and not request.user.is_authenticated(): return redirect('/login/?next=%s' % request.path) + if survey.need_logged_user and request.user.is_authenticated(): + if Response.objects.filter(survey=survey, user=request.user).exists(): + return redirect('survey-completed', id=survey.id) category_items = Category.objects.filter(survey=survey) categories = [c.name for c in category_items] form = ResponseForm(request.POST, survey=survey, user=request.user, step=kwargs.get('step', 0)) @@ -77,3 +88,13 @@ def get_context_data(self, **kwargs): context = super(ConfirmView, self).get_context_data(**kwargs) context['uuid'] = kwargs['uuid'] return context + + +class SurveyCompleted(TemplateView): + template_name = 'survey/completed.html' + + def get_context_data(self, **kwargs): + context = super(SurveyCompleted, self).get_context_data(**kwargs) + survey = get_object_or_404(Survey, is_published=True, id=kwargs['id']) + context['survey'] = survey + return context diff --git a/survey_test/survey_test/templates/survey/completed.html b/survey_test/survey_test/templates/survey/completed.html new file mode 100644 index 00000000..b903e36a --- /dev/null +++ b/survey_test/survey_test/templates/survey/completed.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block title %}Survey completed{% endblock title %} + +{% block body %} +

      Survey completed

      +

      + Sorry! Your already replied to our survey "{{ survey.name}}". +

      +

      + Back to survey list +

      +{% endblock %} \ No newline at end of file From 5e5adbf69b3be8ac1a59c38395b9520d51e5e8b3 Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Sun, 13 Oct 2013 22:40:56 +0200 Subject: [PATCH 13/27] * Added signal to django-survey --- .gitignore | 1 + survey/forms.py | 9 ++------- survey/signals.py | 3 +++ 3 files changed, 6 insertions(+), 7 deletions(-) create mode 100644 survey/signals.py diff --git a/.gitignore b/.gitignore index f916bab8..21c00d81 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ media/data local_settings.py survey.db +survey.egg-info diff --git a/survey/forms.py b/survey/forms.py index fbfb1782..476925fe 100644 --- a/survey/forms.py +++ b/survey/forms.py @@ -9,6 +9,7 @@ from survey.models import AnswerText, AnswerRadio, AnswerSelect from survey.models import AnswerInteger, AnswerSelectMultiple from survey.widgets import ImageSelectWidget +from survey.signals import survey_completed from django.utils.safestring import mark_safe @@ -170,12 +171,6 @@ def save(self, commit=True): a.response = response a.save() - logging.debug("raw data") - logging.debug(data) + survey_completed.send(sender=Response, instance=response, data=data) return response - - - - - diff --git a/survey/signals.py b/survey/signals.py new file mode 100644 index 00000000..f56c834d --- /dev/null +++ b/survey/signals.py @@ -0,0 +1,3 @@ +import django.dispatch + +survey_completed = django.dispatch.Signal(providing_args=["instance", "data"]) From b69090daba0879abd74a2e9e8d9db49170e4b2b5 Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Mon, 4 Aug 2014 00:51:42 +0200 Subject: [PATCH 14/27] * allow to override redirect with session --- survey/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/survey/views.py b/survey/views.py index ffac7bd0..1b0c7021 100644 --- a/survey/views.py +++ b/survey/views.py @@ -77,7 +77,11 @@ def post(self, request, *args, **kwargs): if response is None: return redirect('/') else: - return redirect('survey-confirmation', uuid=response.interview_uuid) + next = request.session.get('next', None) + if next is not None: + return redirect(next) + else: + return redirect('survey-confirmation', uuid=response.interview_uuid) return render(request, self.template_name, context) From f22f5f59c9f986dd7e8949aa9984a8fab500e2c1 Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Mon, 4 Aug 2014 09:18:22 +0200 Subject: [PATCH 15/27] * Deleting session key before redirect --- survey/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/survey/views.py b/survey/views.py index 1b0c7021..84cb6224 100644 --- a/survey/views.py +++ b/survey/views.py @@ -78,6 +78,7 @@ def post(self, request, *args, **kwargs): return redirect('/') else: next = request.session.get('next', None) + del request.session['next'] if next is not None: return redirect(next) else: From 387f725ec1d26abdf72665d58e060e41ff322cb1 Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Mon, 4 Aug 2014 20:42:40 +0200 Subject: [PATCH 16/27] * Specific template for one page survey --- survey/views.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/survey/views.py b/survey/views.py index 84cb6224..63ac1194 100644 --- a/survey/views.py +++ b/survey/views.py @@ -21,10 +21,13 @@ def get_context_data(self, **kwargs): class SurveyDetail(View): - template_name = 'survey/survey.html' def get(self, request, *args, **kwargs): survey = get_object_or_404(Survey, is_published=True, id=kwargs['id']) + if survey.display_by_question: + template_name = 'survey/survey.html' + else: + template_name = 'survey/one_page_survey.html' if survey.need_logged_user and not request.user.is_authenticated(): return redirect('/login/?next=%s' % request.path) if survey.need_logged_user and request.user.is_authenticated(): @@ -40,7 +43,7 @@ def get(self, request, *args, **kwargs): 'categories': categories, } - return render(request, self.template_name, context) + return render(request, template_name, context) def post(self, request, *args, **kwargs): survey = get_object_or_404(Survey, is_published=True, id=kwargs['id']) @@ -83,7 +86,11 @@ def post(self, request, *args, **kwargs): return redirect(next) else: return redirect('survey-confirmation', uuid=response.interview_uuid) - return render(request, self.template_name, context) + if survey.display_by_question: + template_name = 'survey/survey.html' + else: + template_name = 'survey/one_page_survey.html' + return render(request, template_name, context) class ConfirmView(TemplateView): From 6dd049d6b01b7d1e97eb36b673f0e5dfce21be84 Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Tue, 5 Aug 2014 14:05:08 +0200 Subject: [PATCH 17/27] * Making sure that dict key exists before deleting it --- survey/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/survey/views.py b/survey/views.py index 63ac1194..bbf3ddbc 100644 --- a/survey/views.py +++ b/survey/views.py @@ -81,8 +81,9 @@ def post(self, request, *args, **kwargs): return redirect('/') else: next = request.session.get('next', None) - del request.session['next'] if next is not None: + if 'next' in request.session: + del request.session['next'] return redirect(next) else: return redirect('survey-confirmation', uuid=response.interview_uuid) From 3ea89529b49d83ecfaeaff9ef969c95255f2542f Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Sun, 21 Sep 2014 15:14:37 +0200 Subject: [PATCH 18/27] * Updated survey --- survey/migrations/0001_initial.py | 239 ++++++++++++++++++ .../0002_auto__add_field_category_order.py | 130 ++++++++++ survey/migrations/__init__.py | 0 survey/models.py | 3 +- survey/views.py | 4 +- 5 files changed, 373 insertions(+), 3 deletions(-) create mode 100644 survey/migrations/0001_initial.py create mode 100644 survey/migrations/0002_auto__add_field_category_order.py create mode 100644 survey/migrations/__init__.py diff --git a/survey/migrations/0001_initial.py b/survey/migrations/0001_initial.py new file mode 100644 index 00000000..876a4dfc --- /dev/null +++ b/survey/migrations/0001_initial.py @@ -0,0 +1,239 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Survey' + db.create_table(u'survey_survey', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=400)), + ('description', self.gf('django.db.models.fields.TextField')()), + ('is_published', self.gf('django.db.models.fields.BooleanField')()), + ('need_logged_user', self.gf('django.db.models.fields.BooleanField')()), + ('display_by_question', self.gf('django.db.models.fields.BooleanField')()), + )) + db.send_create_signal(u'survey', ['Survey']) + + # Adding model 'Category' + db.create_table(u'survey_category', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=400)), + ('survey', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['survey.Survey'])), + )) + db.send_create_signal(u'survey', ['Category']) + + # Adding model 'Question' + db.create_table(u'survey_question', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('text', self.gf('django.db.models.fields.TextField')()), + ('order', self.gf('django.db.models.fields.IntegerField')()), + ('required', self.gf('django.db.models.fields.BooleanField')()), + ('category', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['survey.Category'], null=True, blank=True)), + ('survey', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['survey.Survey'])), + ('question_type', self.gf('django.db.models.fields.CharField')(default='text', max_length=200)), + ('choices', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal(u'survey', ['Question']) + + # Adding model 'Response' + db.create_table(u'survey_response', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + ('survey', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['survey.Survey'])), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)), + ('interview_uuid', self.gf('django.db.models.fields.CharField')(max_length=36)), + )) + db.send_create_signal(u'survey', ['Response']) + + # Adding model 'AnswerBase' + db.create_table(u'survey_answerbase', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('question', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['survey.Question'])), + ('response', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['survey.Response'])), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + )) + db.send_create_signal(u'survey', ['AnswerBase']) + + # Adding model 'AnswerText' + db.create_table(u'survey_answertext', ( + (u'answerbase_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['survey.AnswerBase'], unique=True, primary_key=True)), + ('body', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal(u'survey', ['AnswerText']) + + # Adding model 'AnswerRadio' + db.create_table(u'survey_answerradio', ( + (u'answerbase_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['survey.AnswerBase'], unique=True, primary_key=True)), + ('body', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal(u'survey', ['AnswerRadio']) + + # Adding model 'AnswerSelect' + db.create_table(u'survey_answerselect', ( + (u'answerbase_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['survey.AnswerBase'], unique=True, primary_key=True)), + ('body', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal(u'survey', ['AnswerSelect']) + + # Adding model 'AnswerSelectMultiple' + db.create_table(u'survey_answerselectmultiple', ( + (u'answerbase_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['survey.AnswerBase'], unique=True, primary_key=True)), + ('body', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal(u'survey', ['AnswerSelectMultiple']) + + # Adding model 'AnswerInteger' + db.create_table(u'survey_answerinteger', ( + (u'answerbase_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['survey.AnswerBase'], unique=True, primary_key=True)), + ('body', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)), + )) + db.send_create_signal(u'survey', ['AnswerInteger']) + + + def backwards(self, orm): + # Deleting model 'Survey' + db.delete_table(u'survey_survey') + + # Deleting model 'Category' + db.delete_table(u'survey_category') + + # Deleting model 'Question' + db.delete_table(u'survey_question') + + # Deleting model 'Response' + db.delete_table(u'survey_response') + + # Deleting model 'AnswerBase' + db.delete_table(u'survey_answerbase') + + # Deleting model 'AnswerText' + db.delete_table(u'survey_answertext') + + # Deleting model 'AnswerRadio' + db.delete_table(u'survey_answerradio') + + # Deleting model 'AnswerSelect' + db.delete_table(u'survey_answerselect') + + # Deleting model 'AnswerSelectMultiple' + db.delete_table(u'survey_answerselectmultiple') + + # Deleting model 'AnswerInteger' + db.delete_table(u'survey_answerinteger') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'survey.answerbase': { + 'Meta': {'object_name': 'AnswerBase'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['survey.Question']"}), + 'response': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['survey.Response']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + u'survey.answerinteger': { + 'Meta': {'object_name': 'AnswerInteger', '_ormbases': [u'survey.AnswerBase']}, + u'answerbase_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['survey.AnswerBase']", 'unique': 'True', 'primary_key': 'True'}), + 'body': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + u'survey.answerradio': { + 'Meta': {'object_name': 'AnswerRadio', '_ormbases': [u'survey.AnswerBase']}, + u'answerbase_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['survey.AnswerBase']", 'unique': 'True', 'primary_key': 'True'}), + 'body': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + u'survey.answerselect': { + 'Meta': {'object_name': 'AnswerSelect', '_ormbases': [u'survey.AnswerBase']}, + u'answerbase_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['survey.AnswerBase']", 'unique': 'True', 'primary_key': 'True'}), + 'body': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + u'survey.answerselectmultiple': { + 'Meta': {'object_name': 'AnswerSelectMultiple', '_ormbases': [u'survey.AnswerBase']}, + u'answerbase_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['survey.AnswerBase']", 'unique': 'True', 'primary_key': 'True'}), + 'body': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + u'survey.answertext': { + 'Meta': {'object_name': 'AnswerText', '_ormbases': [u'survey.AnswerBase']}, + u'answerbase_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['survey.AnswerBase']", 'unique': 'True', 'primary_key': 'True'}), + 'body': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + u'survey.category': { + 'Meta': {'object_name': 'Category'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '400'}), + 'survey': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['survey.Survey']"}) + }, + u'survey.question': { + 'Meta': {'ordering': "('survey', 'order')", 'object_name': 'Question'}, + 'category': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['survey.Category']", 'null': 'True', 'blank': 'True'}), + 'choices': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {}), + 'question_type': ('django.db.models.fields.CharField', [], {'default': "'text'", 'max_length': '200'}), + 'required': ('django.db.models.fields.BooleanField', [], {}), + 'survey': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['survey.Survey']"}), + 'text': ('django.db.models.fields.TextField', [], {}) + }, + u'survey.response': { + 'Meta': {'object_name': 'Response'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'interview_uuid': ('django.db.models.fields.CharField', [], {'max_length': '36'}), + 'survey': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['survey.Survey']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}) + }, + u'survey.survey': { + 'Meta': {'object_name': 'Survey'}, + 'description': ('django.db.models.fields.TextField', [], {}), + 'display_by_question': ('django.db.models.fields.BooleanField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_published': ('django.db.models.fields.BooleanField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '400'}), + 'need_logged_user': ('django.db.models.fields.BooleanField', [], {}) + } + } + + complete_apps = ['survey'] \ No newline at end of file diff --git a/survey/migrations/0002_auto__add_field_category_order.py b/survey/migrations/0002_auto__add_field_category_order.py new file mode 100644 index 00000000..e0a9e4b2 --- /dev/null +++ b/survey/migrations/0002_auto__add_field_category_order.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Category.order' + db.add_column(u'survey_category', 'order', + self.gf('django.db.models.fields.IntegerField')(null=True, blank=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Category.order' + db.delete_column(u'survey_category', 'order') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'survey.answerbase': { + 'Meta': {'object_name': 'AnswerBase'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['survey.Question']"}), + 'response': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['survey.Response']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + u'survey.answerinteger': { + 'Meta': {'object_name': 'AnswerInteger', '_ormbases': [u'survey.AnswerBase']}, + u'answerbase_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['survey.AnswerBase']", 'unique': 'True', 'primary_key': 'True'}), + 'body': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + u'survey.answerradio': { + 'Meta': {'object_name': 'AnswerRadio', '_ormbases': [u'survey.AnswerBase']}, + u'answerbase_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['survey.AnswerBase']", 'unique': 'True', 'primary_key': 'True'}), + 'body': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + u'survey.answerselect': { + 'Meta': {'object_name': 'AnswerSelect', '_ormbases': [u'survey.AnswerBase']}, + u'answerbase_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['survey.AnswerBase']", 'unique': 'True', 'primary_key': 'True'}), + 'body': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + u'survey.answerselectmultiple': { + 'Meta': {'object_name': 'AnswerSelectMultiple', '_ormbases': [u'survey.AnswerBase']}, + u'answerbase_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['survey.AnswerBase']", 'unique': 'True', 'primary_key': 'True'}), + 'body': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + u'survey.answertext': { + 'Meta': {'object_name': 'AnswerText', '_ormbases': [u'survey.AnswerBase']}, + u'answerbase_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['survey.AnswerBase']", 'unique': 'True', 'primary_key': 'True'}), + 'body': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + u'survey.category': { + 'Meta': {'object_name': 'Category'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '400'}), + 'order': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'survey': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['survey.Survey']"}) + }, + u'survey.question': { + 'Meta': {'ordering': "('survey', 'order')", 'object_name': 'Question'}, + 'category': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['survey.Category']", 'null': 'True', 'blank': 'True'}), + 'choices': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {}), + 'question_type': ('django.db.models.fields.CharField', [], {'default': "'text'", 'max_length': '200'}), + 'required': ('django.db.models.fields.BooleanField', [], {}), + 'survey': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['survey.Survey']"}), + 'text': ('django.db.models.fields.TextField', [], {}) + }, + u'survey.response': { + 'Meta': {'object_name': 'Response'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'interview_uuid': ('django.db.models.fields.CharField', [], {'max_length': '36'}), + 'survey': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['survey.Survey']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}) + }, + u'survey.survey': { + 'Meta': {'object_name': 'Survey'}, + 'description': ('django.db.models.fields.TextField', [], {}), + 'display_by_question': ('django.db.models.fields.BooleanField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_published': ('django.db.models.fields.BooleanField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '400'}), + 'need_logged_user': ('django.db.models.fields.BooleanField', [], {}) + } + } + + complete_apps = ['survey'] \ No newline at end of file diff --git a/survey/migrations/__init__.py b/survey/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/survey/models.py b/survey/models.py index 9643a200..1a452ea9 100644 --- a/survey/models.py +++ b/survey/models.py @@ -24,7 +24,7 @@ def get_absolute_url(self): def questions(self): if self.pk: - return Question.objects.filter(survey=self.pk) + return Question.objects.filter(survey=self.pk).order_by('category__order', 'order') else: return Question.objects.none() @@ -32,6 +32,7 @@ def questions(self): class Category(models.Model): name = models.CharField(max_length=400) survey = models.ForeignKey(Survey) + order = models.IntegerField(blank=True, null=True) class Meta: verbose_name = _('category') diff --git a/survey/views.py b/survey/views.py index bbf3ddbc..45deb523 100644 --- a/survey/views.py +++ b/survey/views.py @@ -34,7 +34,7 @@ def get(self, request, *args, **kwargs): if Response.objects.filter(survey=survey, user=request.user).exists(): return redirect('survey-completed', id=survey.id) - category_items = Category.objects.filter(survey=survey) + category_items = Category.objects.filter(survey=survey).order_by('order') categories = [c.name for c in category_items] form = ResponseForm(survey=survey, user=request.user, step=kwargs.get('step', 0)) context = { @@ -52,7 +52,7 @@ def post(self, request, *args, **kwargs): if survey.need_logged_user and request.user.is_authenticated(): if Response.objects.filter(survey=survey, user=request.user).exists(): return redirect('survey-completed', id=survey.id) - category_items = Category.objects.filter(survey=survey) + category_items = Category.objects.filter(survey=survey).order_by('order') categories = [c.name for c in category_items] form = ResponseForm(request.POST, survey=survey, user=request.user, step=kwargs.get('step', 0)) context = {'response_form': form, 'survey': survey, 'categories': categories} From 2e565f11f150f80b63dd0911e9bbeffb997b0984 Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Mon, 22 Sep 2014 14:04:58 +0200 Subject: [PATCH 19/27] * Using dynamic login url --- survey/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/survey/views.py b/survey/views.py index 45deb523..e017c567 100644 --- a/survey/views.py +++ b/survey/views.py @@ -1,6 +1,7 @@ from django.shortcuts import render from django.shortcuts import get_object_or_404 from django.shortcuts import redirect +from django.conf import settings from .models import Survey, Category, Response from .forms import ResponseForm @@ -29,7 +30,7 @@ def get(self, request, *args, **kwargs): else: template_name = 'survey/one_page_survey.html' if survey.need_logged_user and not request.user.is_authenticated(): - return redirect('/login/?next=%s' % request.path) + return redirect('%?next=%s' % (settings.LOGIN_URL, request.path)) if survey.need_logged_user and request.user.is_authenticated(): if Response.objects.filter(survey=survey, user=request.user).exists(): return redirect('survey-completed', id=survey.id) @@ -48,7 +49,7 @@ def get(self, request, *args, **kwargs): def post(self, request, *args, **kwargs): survey = get_object_or_404(Survey, is_published=True, id=kwargs['id']) if survey.need_logged_user and not request.user.is_authenticated(): - return redirect('/login/?next=%s' % request.path) + return redirect('%?next=%s' % (settings.LOGIN_URL, request.path)) if survey.need_logged_user and request.user.is_authenticated(): if Response.objects.filter(survey=survey, user=request.user).exists(): return redirect('survey-completed', id=survey.id) From 2a3fd9dbec61e5acf9396315cc83ababe9e8cd77 Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Mon, 22 Sep 2014 17:51:52 +0200 Subject: [PATCH 20/27] * Using dynamic login url (typo) --- survey/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/survey/views.py b/survey/views.py index e017c567..5c143d3a 100644 --- a/survey/views.py +++ b/survey/views.py @@ -30,7 +30,7 @@ def get(self, request, *args, **kwargs): else: template_name = 'survey/one_page_survey.html' if survey.need_logged_user and not request.user.is_authenticated(): - return redirect('%?next=%s' % (settings.LOGIN_URL, request.path)) + return redirect('%s?next=%s' % (settings.LOGIN_URL, request.path)) if survey.need_logged_user and request.user.is_authenticated(): if Response.objects.filter(survey=survey, user=request.user).exists(): return redirect('survey-completed', id=survey.id) @@ -49,7 +49,7 @@ def get(self, request, *args, **kwargs): def post(self, request, *args, **kwargs): survey = get_object_or_404(Survey, is_published=True, id=kwargs['id']) if survey.need_logged_user and not request.user.is_authenticated(): - return redirect('%?next=%s' % (settings.LOGIN_URL, request.path)) + return redirect('%s?next=%s' % (settings.LOGIN_URL, request.path)) if survey.need_logged_user and request.user.is_authenticated(): if Response.objects.filter(survey=survey, user=request.user).exists(): return redirect('survey-completed', id=survey.id) From 7be8c8cb7ac298585095efd192d157905634ce9a Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Tue, 18 Aug 2015 08:12:38 +0200 Subject: [PATCH 21/27] * Updated readme --- readme.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index e2ae0b69..3d968d36 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,10 @@ +# Django survey + install requirements using `pip install -r requirements.txt` then run using `./survey_test/manage.py runserver` -## about +## About Using the admin interface you can create surveys, add questions, give questions categories, and mark them as required or not. the front-end survey view then @@ -11,7 +13,8 @@ admin interface. Submitted responses can also be viewed via the admin backend. -## credits +## Credits + some inspiration came from an older [django-survey](https://github.com/flynnguy/django-survey) app, but this app uses a different model architecture and different mechanism for dynamic form @@ -21,6 +24,4 @@ generation. this code is licensed under the [affero general public license](http://www.gnu.org/licenses/agpl-3.0.html). - The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public... The GNU Affero General Public License is designed specifically to - ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a - network server to provide the source code of the modified version running there to the users of that server. +The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public... The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. From 77c3d2216e38354fa1787f0b9d962ce5502a2663 Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Thu, 10 Sep 2015 18:02:45 +0200 Subject: [PATCH 22/27] * Commit from guillaume --- survey/forms.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/survey/forms.py b/survey/forms.py index 476925fe..b6a2c7a5 100644 --- a/survey/forms.py +++ b/survey/forms.py @@ -85,6 +85,7 @@ def __init__(self, *args, **kwargs): if q.required: self.fields["question_%d" % q.pk].required = True self.fields["question_%d" % q.pk].widget.attrs["class"] = "required" + self.fields["question_%d" % q.pk].widget.attrs["required"] = True else: self.fields["question_%d" % q.pk].required = False @@ -99,6 +100,17 @@ def __init__(self, *args, **kwargs): self.fields["question_%d" % q.pk].widget.attrs["class"] = (" cat_%s" % q.category.name) self.fields["question_%d" % q.pk].widget.attrs["category"] = q.category.name + if q.question_type == Question.SELECT: + classes = self.fields["question_%d" % q.pk].widget.attrs.get("class") + self.fields["question_%d" % q.pk].widget.attrs["class"] = classes + (" cs-select cs-skin-boxes") + + if q.question_type == Question.RADIO: + classes = self.fields["question_%d" % q.pk].widget.attrs.get("class") + self.fields["question_%d" % q.pk].widget.attrs["class"] = classes + (" fs-radio-group fs-radio-custom clearfix") + + #if q.question_type == Question.SELECT_MULTIPLE: + # classes = self.fields["question_%d" % q.pk].widget.attrs.get("class") + # self.fields["question_%d" % q.pk].widget.attrs["class"] = classes + (" ") # initialize the form field with values from a POST request, if any. if data: From 178e27f3481a3307961616cbecdda9ee35e3a72e Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Thu, 10 Sep 2015 18:03:46 +0200 Subject: [PATCH 23/27] * Moving migrations to south migrations --- survey/{migrations => south_migrations}/0001_initial.py | 0 .../0002_auto__add_field_category_order.py | 0 survey/{migrations => south_migrations}/__init__.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename survey/{migrations => south_migrations}/0001_initial.py (100%) rename survey/{migrations => south_migrations}/0002_auto__add_field_category_order.py (100%) rename survey/{migrations => south_migrations}/__init__.py (100%) diff --git a/survey/migrations/0001_initial.py b/survey/south_migrations/0001_initial.py similarity index 100% rename from survey/migrations/0001_initial.py rename to survey/south_migrations/0001_initial.py diff --git a/survey/migrations/0002_auto__add_field_category_order.py b/survey/south_migrations/0002_auto__add_field_category_order.py similarity index 100% rename from survey/migrations/0002_auto__add_field_category_order.py rename to survey/south_migrations/0002_auto__add_field_category_order.py diff --git a/survey/migrations/__init__.py b/survey/south_migrations/__init__.py similarity index 100% rename from survey/migrations/__init__.py rename to survey/south_migrations/__init__.py From 65a0981a059c440332b08f82121bfc6997ebd906 Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Thu, 10 Sep 2015 18:06:45 +0200 Subject: [PATCH 24/27] * Improved setup.py --- setup.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 2476d48b..95402027 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,14 @@ -from distutils.core import setup +from setuptools import setup, find_packages setup( name="survey", - version="0.1", + version="0.1.1", author="Jessy Kate Schingler", author_email="jessy@jessykate.com", license="AGPL", url="https://github.com/jessykate/django-survey", - packages=[ - "survey", - ], + packages=find_packages(exclude=[]), + include_package_data=True, classifiers=[ "Development Status :: 3 - Alpha", "Environment :: Web Environment", @@ -19,4 +18,4 @@ "Programming Language :: Python", "Framework :: Django", ] -) \ No newline at end of file +) From 89bb3b5346852266b1a678f7b4cac8d2c7831bee Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Thu, 10 Sep 2015 18:11:24 +0200 Subject: [PATCH 25/27] * Creating new Django migrations --- survey/migrations/0001_initial.py | 150 ++++++++++++++++++++++++++++++ survey/migrations/__init__.py | 0 2 files changed, 150 insertions(+) create mode 100644 survey/migrations/0001_initial.py create mode 100644 survey/migrations/__init__.py diff --git a/survey/migrations/0001_initial.py b/survey/migrations/0001_initial.py new file mode 100644 index 00000000..44a732b4 --- /dev/null +++ b/survey/migrations/0001_initial.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='AnswerBase', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=400)), + ('order', models.IntegerField(null=True, blank=True)), + ], + options={ + 'verbose_name': 'category', + 'verbose_name_plural': 'categories', + }, + ), + migrations.CreateModel( + name='Question', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('text', models.TextField()), + ('order', models.IntegerField()), + ('required', models.BooleanField()), + ('question_type', models.CharField(default=b'text', max_length=200, choices=[(b'text', 'text (multiple line)'), (b'short-text', 'short text (one line)'), (b'radio', 'radio'), (b'select', 'select'), (b'select-multiple', 'Select Multiple'), (b'select_image', 'Select Image'), (b'integer', 'integer')])), + ('choices', models.TextField(help_text="if the question type is 'radio', 'select', or 'select multiple' provide a comma-separated list of options for this question .", null=True, blank=True)), + ('category', models.ForeignKey(blank=True, to='survey.Category', null=True)), + ], + options={ + 'ordering': ('survey', 'order'), + 'verbose_name': 'question', + 'verbose_name_plural': 'questions', + }, + ), + migrations.CreateModel( + name='Response', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('interview_uuid', models.CharField(max_length=36, verbose_name='Interview unique identifier')), + ], + options={ + 'verbose_name': 'response', + 'verbose_name_plural': 'responses', + }, + ), + migrations.CreateModel( + name='Survey', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=400)), + ('description', models.TextField()), + ('is_published', models.BooleanField()), + ('need_logged_user', models.BooleanField()), + ('display_by_question', models.BooleanField()), + ], + options={ + 'verbose_name': 'survey', + 'verbose_name_plural': 'surveys', + }, + ), + migrations.CreateModel( + name='AnswerInteger', + fields=[ + ('answerbase_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='survey.AnswerBase')), + ('body', models.IntegerField(null=True, blank=True)), + ], + bases=('survey.answerbase',), + ), + migrations.CreateModel( + name='AnswerRadio', + fields=[ + ('answerbase_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='survey.AnswerBase')), + ('body', models.TextField(null=True, blank=True)), + ], + bases=('survey.answerbase',), + ), + migrations.CreateModel( + name='AnswerSelect', + fields=[ + ('answerbase_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='survey.AnswerBase')), + ('body', models.TextField(null=True, blank=True)), + ], + bases=('survey.answerbase',), + ), + migrations.CreateModel( + name='AnswerSelectMultiple', + fields=[ + ('answerbase_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='survey.AnswerBase')), + ('body', models.TextField(null=True, blank=True)), + ], + bases=('survey.answerbase',), + ), + migrations.CreateModel( + name='AnswerText', + fields=[ + ('answerbase_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='survey.AnswerBase')), + ('body', models.TextField(null=True, blank=True)), + ], + bases=('survey.answerbase',), + ), + migrations.AddField( + model_name='response', + name='survey', + field=models.ForeignKey(to='survey.Survey'), + ), + migrations.AddField( + model_name='response', + name='user', + field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True), + ), + migrations.AddField( + model_name='question', + name='survey', + field=models.ForeignKey(to='survey.Survey'), + ), + migrations.AddField( + model_name='category', + name='survey', + field=models.ForeignKey(to='survey.Survey'), + ), + migrations.AddField( + model_name='answerbase', + name='question', + field=models.ForeignKey(to='survey.Question'), + ), + migrations.AddField( + model_name='answerbase', + name='response', + field=models.ForeignKey(to='survey.Response'), + ), + ] diff --git a/survey/migrations/__init__.py b/survey/migrations/__init__.py new file mode 100644 index 00000000..e69de29b From e4606b79e30cf7cad7ac7f4b4fd11b2d0cf25cbf Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Thu, 10 Sep 2015 18:24:04 +0200 Subject: [PATCH 26/27] * Adding template field to survey --- survey/admin.py | 73 ++++++++++++++--------- survey/migrations/0002_survey_template.py | 19 ++++++ survey/models.py | 1 + 3 files changed, 65 insertions(+), 28 deletions(-) create mode 100644 survey/migrations/0002_survey_template.py diff --git a/survey/admin.py b/survey/admin.py index 22f0e25e..6bd515df 100644 --- a/survey/admin.py +++ b/survey/admin.py @@ -1,53 +1,70 @@ from django.contrib import admin -from .models import Question, Category, Survey, Response, AnswerText, AnswerRadio, AnswerSelect, AnswerInteger, AnswerSelectMultiple +from .models import Question, Category, Survey, Response, AnswerText +from .models import AnswerRadio, AnswerSelect +from .models import AnswerInteger, AnswerSelectMultiple from .actions import make_published + class QuestionInline(admin.TabularInline): - model = Question - ordering = ('category', 'order',) - extra = 1 + model = Question + ordering = ('category', 'order',) + extra = 1 + class CategoryInline(admin.TabularInline): - model = Category - extra = 0 + model = Category + extra = 0 + class SurveyAdmin(admin.ModelAdmin): - list_display = ('name', 'is_published', 'need_logged_user') - list_filter = ('is_published', 'need_logged_user') - inlines = [CategoryInline, QuestionInline] - actions = [make_published,] + list_display = ('name', 'is_published', 'need_logged_user', 'template') + list_filter = ('is_published', 'need_logged_user') + inlines = [CategoryInline, QuestionInline] + actions = [make_published] + class AnswerBaseInline(admin.StackedInline): - fields = ('question', 'body') - readonly_fields = ('question',) - extra = 0 + fields = ('question', 'body') + readonly_fields = ('question',) + extra = 0 + class AnswerTextInline(AnswerBaseInline): - model= AnswerText + model = AnswerText + class AnswerRadioInline(AnswerBaseInline): - model= AnswerRadio + model = AnswerRadio + class AnswerSelectInline(AnswerBaseInline): - model= AnswerSelect + model = AnswerSelect + class AnswerSelectMultipleInline(AnswerBaseInline): - model= AnswerSelectMultiple + model = AnswerSelectMultiple + class AnswerIntegerInline(AnswerBaseInline): - model= AnswerInteger + model = AnswerInteger + class ResponseAdmin(admin.ModelAdmin): - list_display = ('interview_uuid', 'survey', 'created', 'user') - list_filter = ('survey', 'created') - date_hierarchy = 'created' - inlines = [AnswerTextInline, AnswerRadioInline, AnswerSelectInline, AnswerSelectMultipleInline, AnswerIntegerInline] - # specifies the order as well as which fields to act on - readonly_fields = ('survey', 'created', 'updated', 'interview_uuid', 'user') - -#admin.site.register(Question, QuestionInline) -#admin.site.register(Category, CategoryInline) -admin.site.register(Survey, SurveyAdmin) + list_display = ('interview_uuid', 'survey', 'created', 'user') + list_filter = ('survey', 'created') + date_hierarchy = 'created' + inlines = [ + AnswerTextInline, AnswerRadioInline, AnswerSelectInline, + AnswerSelectMultipleInline, AnswerIntegerInline + ] + # specifies the order as well as which fields to act on + readonly_fields = ( + 'survey', 'created', 'updated', 'interview_uuid', 'user' + ) + +# admin.site.register(Question, QuestionInline) +# admin.site.register(Category, CategoryInline) +admin.site.register(Survey, SurveyAdmin) admin.site.register(Response, ResponseAdmin) diff --git a/survey/migrations/0002_survey_template.py b/survey/migrations/0002_survey_template.py new file mode 100644 index 00000000..b1203f15 --- /dev/null +++ b/survey/migrations/0002_survey_template.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('survey', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='survey', + name='template', + field=models.CharField(max_length=255, null=True, blank=True), + ), + ] diff --git a/survey/models.py b/survey/models.py index 1a452ea9..272fd9f6 100644 --- a/survey/models.py +++ b/survey/models.py @@ -10,6 +10,7 @@ class Survey(models.Model): is_published = models.BooleanField() need_logged_user = models.BooleanField() display_by_question = models.BooleanField() + template = models.CharField(max_length=255, null=True, blank=True) class Meta: verbose_name = _('survey') From 97bbec2359b8fc37ff70fa2f7520109fa143cb5b Mon Sep 17 00:00:00 2001 From: Fabien Schwob Date: Thu, 10 Sep 2015 18:36:51 +0200 Subject: [PATCH 27/27] * Using survey template if available --- survey/views.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/survey/views.py b/survey/views.py index 5c143d3a..09f919bb 100644 --- a/survey/views.py +++ b/survey/views.py @@ -9,6 +9,7 @@ from django.views.generic import TemplateView, View + class IndexView(TemplateView): template_name = "survey/list.html" @@ -25,10 +26,13 @@ class SurveyDetail(View): def get(self, request, *args, **kwargs): survey = get_object_or_404(Survey, is_published=True, id=kwargs['id']) - if survey.display_by_question: - template_name = 'survey/survey.html' + if survey.template is not None and len(survey.template) > 4: + template_name = survey.template else: - template_name = 'survey/one_page_survey.html' + if survey.display_by_question: + template_name = 'survey/survey.html' + else: + template_name = 'survey/one_page_survey.html' if survey.need_logged_user and not request.user.is_authenticated(): return redirect('%s?next=%s' % (settings.LOGIN_URL, request.path)) if survey.need_logged_user and request.user.is_authenticated(): @@ -64,7 +68,7 @@ def post(self, request, *args, **kwargs): for key, value in form.cleaned_data.items(): request.session[session_key][key] = value request.session.modified = True - + next_url = form.next_step_url() response = None if survey.display_by_question: @@ -73,7 +77,7 @@ def post(self, request, *args, **kwargs): response = save_form.save() else: response = form.save() - + if next_url is not None: return redirect(next_url) else: @@ -88,10 +92,13 @@ def post(self, request, *args, **kwargs): return redirect(next) else: return redirect('survey-confirmation', uuid=response.interview_uuid) - if survey.display_by_question: - template_name = 'survey/survey.html' + if survey.template is not None and len(survey.template) > 4: + template_name = survey.template else: - template_name = 'survey/one_page_survey.html' + if survey.display_by_question: + template_name = 'survey/survey.html' + else: + template_name = 'survey/one_page_survey.html' return render(request, template_name, context)