diff --git a/SORT/settings.py b/SORT/settings.py index fd7cf4c..3904b92 100644 --- a/SORT/settings.py +++ b/SORT/settings.py @@ -48,7 +48,6 @@ "home", "survey", - "invites", ] MIDDLEWARE = [ diff --git a/home/forms.py b/home/forms.py index 205a491..619b96a 100644 --- a/home/forms.py +++ b/home/forms.py @@ -43,6 +43,9 @@ class Meta: def clean_email(self): email = self.cleaned_data.get('email') + if email == self.instance.email: + return email + if User.objects.exclude(pk=self.instance.pk).filter(email=email).exists(): raise forms.ValidationError("This email is already in use.") return email diff --git a/home/urls.py b/home/urls.py index 1d1ad0c..c6c6076 100644 --- a/home/urls.py +++ b/home/urls.py @@ -9,7 +9,6 @@ path('logout/', views.LogoutInterfaceView.as_view(), name='logout'), path('signup/', views.SignupView.as_view(), name='signup'), path('', include('survey.urls'), name='survey'), - path('invite/', include('invites.urls'), name='invites'), path('profile/', views.ProfileView.as_view(), name='profile'), path('password_reset/', views.CustomPasswordResetView.as_view(), name='password_reset'), path('password_reset/done/', views.CustomPasswordResetDoneView.as_view(), name='password_reset_done'), diff --git a/home/views.py b/home/views.py index cb58a55..8186a5c 100644 --- a/home/views.py +++ b/home/views.py @@ -3,7 +3,6 @@ from django.contrib.auth.views import LoginView, LogoutView from django.views.generic.edit import CreateView, UpdateView from django.shortcuts import redirect -from survey.models import Questionnaire from django.shortcuts import render from django.views import View from .forms import ManagerSignupForm, ManagerLoginForm, UserProfileForm @@ -43,11 +42,10 @@ def dispatch(self, request, *args, **kwargs): class HomeView(LoginRequiredMixin, View): template_name = 'home/welcome.html' login_url = 'login' - def get(self, request): - consent_questionnaire = Questionnaire.objects.get( - title="Consent") - return render(request, 'home/welcome.html', {'questionnaire': consent_questionnaire}) + def get(self, request, *args, **kwargs): + + return render(request, self.template_name) class ProfileView(LoginRequiredMixin, UpdateView): model = User form_class = UserProfileForm diff --git a/invites/__init__.py b/invites/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/invites/admin.py b/invites/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/invites/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/invites/apps.py b/invites/apps.py deleted file mode 100644 index c7429fb..0000000 --- a/invites/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class InvitationsConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "invites" diff --git a/invites/forms.py b/invites/forms.py deleted file mode 100644 index 058f076..0000000 --- a/invites/forms.py +++ /dev/null @@ -1,9 +0,0 @@ -from django import forms -from django.core.validators import EmailValidator - - -class InvitationForm(forms.Form): - email = forms.EmailField(label='Participant Email', - max_length=100, - required=True, - validators=[EmailValidator()]) \ No newline at end of file diff --git a/invites/migrations/0001_initial.py b/invites/migrations/0001_initial.py deleted file mode 100644 index 48b030c..0000000 --- a/invites/migrations/0001_initial.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 5.1.2 on 2024-10-22 14:33 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - ("survey", "0007_comment"), - ] - - operations = [ - migrations.CreateModel( - name="Invitation", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("token", models.CharField(blank=True, max_length=64, unique=True)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("used", models.BooleanField(default=False)), - ( - "questionnaire", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="survey.questionnaire", - ), - ), - ], - ), - ] diff --git a/invites/migrations/0002_alter_invitation_token.py b/invites/migrations/0002_alter_invitation_token.py deleted file mode 100644 index 874b843..0000000 --- a/invites/migrations/0002_alter_invitation_token.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.1.2 on 2024-10-23 09:03 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("invites", "0001_initial"), - ] - - operations = [ - migrations.AlterField( - model_name="invitation", - name="token", - field=models.CharField( - blank=True, editable=False, max_length=64, unique=True - ), - ), - ] diff --git a/invites/migrations/__init__.py b/invites/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/invites/models.py b/invites/models.py deleted file mode 100644 index 2a09e8d..0000000 --- a/invites/models.py +++ /dev/null @@ -1,22 +0,0 @@ -import secrets -from django.db import models -from django.utils import timezone -from survey.models import Questionnaire - -class Invitation(models.Model): - - questionnaire = models.ForeignKey(Questionnaire, on_delete=models.CASCADE) - token = models.CharField(max_length=64, unique=True, blank=True, editable=False) - created_at = models.DateTimeField(auto_now_add=True) - used = models.BooleanField(default=False) - - def __str__(self): - return f"Invitation for {self.questionnaire.title}" - - def save(self, *args, **kwargs): - if not self.token: - self.token = secrets.token_urlsafe(32) - super().save(*args, **kwargs) - - def is_expired(self): - return timezone.now() > self.created_at + timezone.timedelta(days=7) diff --git a/invites/tests.py b/invites/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/invites/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/invites/urls.py b/invites/urls.py deleted file mode 100644 index fc16e83..0000000 --- a/invites/urls.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.urls import path -from .views import InvitationView, SuccessInvitationView - -urlpatterns = [ - path('', InvitationView.as_view(), name='invite'), - path('success/', SuccessInvitationView.as_view(), name='success_invitation') -] \ No newline at end of file diff --git a/invites/views.py b/invites/views.py deleted file mode 100644 index 7beb395..0000000 --- a/invites/views.py +++ /dev/null @@ -1,49 +0,0 @@ -from django.views.generic.edit import FormView -from django.core.mail import send_mail -from django.contrib import messages -from .forms import InvitationForm -from django.urls import reverse_lazy -from django.views.generic import TemplateView -from survey.models import Questionnaire -from invites.models import Invitation -from django.contrib.auth.mixins import LoginRequiredMixin -from django.shortcuts import get_object_or_404, redirect -from django.http import HttpResponseForbidden - - -class InvitationView(FormView): - - template_name = 'invitations/send_invitation.html' - form_class = InvitationForm - success_url = reverse_lazy('success_invitation') - - def form_valid(self, form): - email = form.cleaned_data['email'] - - questionnaire = Questionnaire.objects.first() - - invitation = Invitation.objects.create(questionnaire=questionnaire) - - token = invitation.token - - # Generate the survey link with the token - survey_link = f"http://localhost:8000/survey/{questionnaire.pk}/{token}/" - - # Send the email - send_mail( - 'Your Survey Invitation', - f'Click here to start the survey: {survey_link}', - 'from@example.com', - [email], - fail_silently=False, - ) - - # Show success message - messages.success(self.request, f'Invitation sent to {email}.') - return super().form_valid(form) - - - -class SuccessInvitationView(LoginRequiredMixin, TemplateView): - - template_name = 'invitations/complete_invitation.html' diff --git a/requirements.txt b/requirements.txt index 053c4ad..da5b1df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,3 +24,4 @@ traitlets==5.14.3 typing_extensions==4.12.2 tzdata==2024.1 wcwidth==0.2.13 +strenum==0.4.15 diff --git a/survey/admin.py b/survey/admin.py index 402c11c..d85e9b7 100644 --- a/survey/admin.py +++ b/survey/admin.py @@ -1,6 +1,5 @@ from django.contrib import admin -from .models import Questionnaire, Question, Answer +from .models import Survey, SurveyResponse -admin.site.register(Questionnaire) -admin.site.register(Question) -admin.site.register(Answer) +admin.site.register(Survey) +admin.site.register(SurveyResponse) diff --git a/survey/forms.py b/survey/forms.py index ef9f0c8..e345f8a 100644 --- a/survey/forms.py +++ b/survey/forms.py @@ -1,40 +1,62 @@ from django import forms -from .models import Answer, Question -class AnswerForm(forms.ModelForm): - class Meta: - model = Answer - fields = [] # no defaults needed - - def __init__(self, *args, **kwargs): - questionnaire = kwargs.pop('questionnaire', None) # get the questionnaire passed in - super().__init__(*args, **kwargs) - - if questionnaire: - - for index, question in enumerate(questionnaire.questions.all(), start=1): - if question.question_type == 'boolean': - self.fields[f'question_{question.id}'] = forms.ChoiceField( - label=f"{index}. {question.question_text}", - choices=[('agree', 'I agree'), ('disagree', 'I disagree')], - widget=forms.RadioSelect, - required=True - ) - - elif question.question_type == 'rating': - - self.fields[f'question_{question.id}'] = forms.ChoiceField( - label=question.question_text, - choices=[(i, str(i)) for i in range(1, 6)], # assuming a 1-5 rating - required=True - ) - - - else: - - pass - - # self.fields[f'question_{question.id}'] = forms.CharField( - # label=question.question_text, - # required=True, - # widget=forms.Textarea(attrs={'rows': 4, 'style': 'width: 80%;'}), - # ) +from django.forms import BaseFormSet, formset_factory +from strenum import StrEnum +from django.core.validators import EmailValidator + +class InvitationForm(forms.Form): + email = forms.EmailField(label='Participant Email', + max_length=100, + required=True, + validators=[EmailValidator()]) + +class FormFieldType(StrEnum): + CHAR = "char" + TEXT = "text" + RADIO = "radio" + CHECKBOX = "checkbox" + LIKERT = "likert" + + + +def create_field_from_config(field_config: dict): + """ + Convert a field configuration into the correct django field + """ + if field_config['type'] == FormFieldType.CHAR: + field = forms.CharField(label=field_config["label"]) + elif field_config['type'] == FormFieldType.TEXT: + field = forms.CharField(label=field_config["label"], + widget=forms.Textarea) + elif field_config['type'] == FormFieldType.RADIO: + field = forms.ChoiceField(label=field_config["label"], + choices=field_config["options"], + widget=forms.RadioSelect) + elif field_config['type'] == FormFieldType.CHECKBOX: + field = forms.MultipleChoiceField(label=field_config["label"], + widget=forms.CheckboxSelectMultiple, + choices=field_config["options"]) + else: + field = forms.CharField(label=field_config["label"], + widget=forms.Textarea) + + if "required" in field_config: + field.required = field_config["required"] + + return field + + + +def create_dynamic_formset(field_configs: list): + """ + Create a dynamic form set from a list of field configurations. + """ + class BlankDynamicForm(forms.Form): + pass + + class BaseTestFormSet(BaseFormSet): + def add_fields(self, form, index): + super().add_fields(form, index) + for field_config in field_configs: + form.fields[field_config["name"]] = create_field_from_config(field_config) + + return formset_factory(BlankDynamicForm, BaseTestFormSet, min_num=1, max_num=1) diff --git a/survey/migrations/0001_initial.py b/survey/migrations/0001_initial.py index 9849a7b..796716e 100644 --- a/survey/migrations/0001_initial.py +++ b/survey/migrations/0001_initial.py @@ -1,85 +1,42 @@ -# Generated by Django 5.1.1 on 2024-09-12 18:18 +# Generated by Django 5.1.2 on 2024-12-03 06:54 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): + initial = True - dependencies = [] + dependencies = [ + ] operations = [ migrations.CreateModel( - name="Question", + name='Survey', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("question_text", models.CharField(max_length=500)), - ( - "question_type", - models.CharField( - choices=[ - ("text", "Text"), - ("multiple_choice", "Multiple Choice"), - ("rating", "Rating"), - ], - max_length=50, - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('description', models.TextField()), + ('survey_config', models.JSONField()), ], ), migrations.CreateModel( - name="Questionnaire", + name='Invitation', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("title", models.CharField(max_length=200)), - ("description", models.TextField()), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('token', models.CharField(blank=True, editable=False, max_length=64, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('used', models.BooleanField(default=False)), + ('survey', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='survey.survey')), ], ), migrations.CreateModel( - name="Answer", + name='SurveyResponse', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("user", models.CharField(max_length=100)), - ("answer_text", models.TextField(blank=True, null=True)), - ( - "question", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="survey.question", - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('answers', models.JSONField()), + ('survey', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='survey', to='survey.survey')), ], ), - migrations.AddField( - model_name="question", - name="questionnaire", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="survey.questionnaire" - ), - ), ] diff --git a/survey/migrations/0002_answer_submitted_at_alter_answer_user.py b/survey/migrations/0002_answer_submitted_at_alter_answer_user.py deleted file mode 100644 index b61a588..0000000 --- a/survey/migrations/0002_answer_submitted_at_alter_answer_user.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.1.1 on 2024-10-01 13:59 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("survey", "0001_initial"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name="answer", - name="submitted_at", - field=models.DateTimeField(null=True), - ), - migrations.AlterField( - model_name="answer", - name="user", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL - ), - ), - ] diff --git a/survey/migrations/0003_alter_question_question_type.py b/survey/migrations/0003_alter_question_question_type.py deleted file mode 100644 index 1bd04d0..0000000 --- a/survey/migrations/0003_alter_question_question_type.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 5.1.1 on 2024-10-01 15:43 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("survey", "0002_answer_submitted_at_alter_answer_user"), - ] - - operations = [ - migrations.AlterField( - model_name="question", - name="question_type", - field=models.CharField( - choices=[ - ("text", "Text"), - ("multiple_choice", "Multiple Choice"), - ("rating", "Rating"), - ("boolean", "Agree/Disagree"), - ], - default="multiple_choice", - max_length=50, - ), - ), - ] diff --git a/survey/migrations/0004_alter_question_questionnaire.py b/survey/migrations/0004_alter_question_questionnaire.py deleted file mode 100644 index 1c35847..0000000 --- a/survey/migrations/0004_alter_question_questionnaire.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.1.1 on 2024-10-01 16:17 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("survey", "0003_alter_question_question_type"), - ] - - operations = [ - migrations.AlterField( - model_name="question", - name="questionnaire", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="questions", - to="survey.questionnaire", - ), - ), - ] diff --git a/survey/migrations/0005_question_answer_type.py b/survey/migrations/0005_question_answer_type.py deleted file mode 100644 index 8ddf9eb..0000000 --- a/survey/migrations/0005_question_answer_type.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 5.1.1 on 2024-10-01 16:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("survey", "0004_alter_question_questionnaire"), - ] - - operations = [ - migrations.AddField( - model_name="question", - name="answer_type", - field=models.CharField( - blank=True, - choices=[ - ("text", "Text"), - ("multiple_choice", "Multiple Choice"), - ("rating", "Rating"), - ("boolean", "Agree/Disagree"), - ], - max_length=20, - ), - ), - ] diff --git a/survey/migrations/0006_remove_question_answer_type.py b/survey/migrations/0006_remove_question_answer_type.py deleted file mode 100644 index d714e1a..0000000 --- a/survey/migrations/0006_remove_question_answer_type.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 5.1.1 on 2024-10-01 16:40 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("survey", "0005_question_answer_type"), - ] - - operations = [ - migrations.RemoveField( - model_name="question", - name="answer_type", - ), - ] diff --git a/survey/migrations/0007_comment.py b/survey/migrations/0007_comment.py deleted file mode 100644 index 08f5729..0000000 --- a/survey/migrations/0007_comment.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 5.1.1 on 2024-10-03 12:52 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("survey", "0006_remove_question_answer_type"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name="Comment", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("text", models.TextField()), - ("submitted_at", models.DateTimeField(null=True)), - ( - "questionnaire", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="survey.questionnaire", - ), - ), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), - ] diff --git a/survey/migrations/0008_remove_answer_user_remove_comment_user_answer_token_and_more.py b/survey/migrations/0008_remove_answer_user_remove_comment_user_answer_token_and_more.py deleted file mode 100644 index bcbab21..0000000 --- a/survey/migrations/0008_remove_answer_user_remove_comment_user_answer_token_and_more.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 5.1.2 on 2024-10-23 09:13 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("survey", "0007_comment"), - ] - - operations = [ - migrations.RemoveField( - model_name="answer", - name="user", - ), - migrations.RemoveField( - model_name="comment", - name="user", - ), - migrations.AddField( - model_name="answer", - name="token", - field=models.CharField( - blank=True, editable=False, max_length=64, unique=True - ), - ), - migrations.AddField( - model_name="comment", - name="token", - field=models.CharField( - blank=True, editable=False, max_length=64, unique=True - ), - ), - ] diff --git a/survey/migrations/0009_alter_answer_token_alter_comment_token.py b/survey/migrations/0009_alter_answer_token_alter_comment_token.py deleted file mode 100644 index 30857a2..0000000 --- a/survey/migrations/0009_alter_answer_token_alter_comment_token.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.1.2 on 2024-10-23 09:15 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("survey", "0008_remove_answer_user_remove_comment_user_answer_token_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="answer", - name="token", - field=models.CharField(blank=True, editable=False, max_length=64), - ), - migrations.AlterField( - model_name="comment", - name="token", - field=models.CharField(blank=True, editable=False, max_length=64), - ), - ] diff --git a/survey/misc.py b/survey/misc.py new file mode 100644 index 0000000..9639b20 --- /dev/null +++ b/survey/misc.py @@ -0,0 +1,73 @@ +test_survey_config = { + "sections": [ + { + "title": "Welcome", + "type": "consent", + "fields": [ + { + "type": "radio", + "name": "consent", + "label": "Your agreement to complete the survey", + "required": True, + "options": [["Yes", "I agree to complete the survey"], ["No", "I do not agree"]] + } + ] + }, + { + "title": "Section 1", + "description": "", + "fields": [ + { + "type": "char", + "name": "char_field", + "label": "Char field", + }, + { + "type": "text", + "name": "text_field", + "label": "Text field", + }, + { + "type": "checkbox", + "name": "checkbox_field", + "label": "Checkbox field", + "options": [["Value 1", "Label 1"], ["Value 2", "Label 2"]] + }, + { + "type": "radio", + "name": "radio_field", + "label": "Radio field", + "options": [["Value 1", "Label 1"], ["Value 2", "Label 2"]] + } + ], + }, + { + "title": "Section 2", + "description": "", + "fields": [ + { + "type": "char", + "name": "char_field", + "label": "Char field", + }, + { + "type": "text", + "name": "text_field", + "label": "Text field", + }, + { + "type": "checkbox", + "name": "checkbox_field", + "label": "Checkbox field", + "options": [["Value 1", "Label 1"], ["Value 2", "Label 2"]] + }, + { + "type": "radio", + "name": "radio_field", + "label": "Radio field", + "options": [["Value 1", "Label 1"], ["Value 2", "Label 2"]] + } + ], + } + ] + } \ No newline at end of file diff --git a/survey/mixins.py b/survey/mixins.py index ab2320e..cfc6e51 100644 --- a/survey/mixins.py +++ b/survey/mixins.py @@ -1,7 +1,7 @@ __author__ = "Farhad Allian" from django.shortcuts import redirect -from invites.models import Invitation +from survey.models import Invitation class TokenAuthenticationMixin: diff --git a/survey/models.py b/survey/models.py index d6656b4..74a76f5 100644 --- a/survey/models.py +++ b/survey/models.py @@ -1,11 +1,17 @@ +import secrets from django.db import models from django.contrib.auth.models import User from django.urls import reverse +from django.utils import timezone -class Questionnaire(models.Model): - # Questionnaire data model +class Survey(models.Model): + """ + Represents a survey that will be sent out to a participant + """ title = models.CharField(max_length=200) description = models.TextField() + survey_config = models.JSONField() + # TODO: Add the project it belongs to as foreign key def __str__(self): return self.title @@ -14,40 +20,33 @@ def get_absolute_url(self, token): return reverse('survey', kwargs={'pk': self.pk, 'token': token}) -class Question(models.Model): - # Question data model - QUESTION_TYPE_CHOICES = [ - ('text', 'Text'), - ('multiple_choice', 'Multiple Choice'), - ('rating', 'Rating'), - ('boolean', 'Agree/Disagree') - ] +class SurveyResponse(models.Model): + """ + Represents a single response to the survey from a participant + """ - questionnaire = models.ForeignKey(Questionnaire, related_name='questions', on_delete=models.CASCADE) # Many questions belong to one questionnaire - question_text = models.CharField(max_length=500) - question_type = models.CharField(max_length=50, choices=QUESTION_TYPE_CHOICES, default='multiple_choice') + survey = models.ForeignKey(Survey, related_name='survey', on_delete=models.CASCADE) # Many questions belong to one survey + answers = models.JSONField() - def __str__(self): - return self.question_text + def get_absolute_url(self, token): + return reverse('survey', kwargs={'pk': self.pk, 'token': token}) -class Answer(models.Model): - # User answer data model - question = models.ForeignKey(Question, on_delete=models.CASCADE) - answer_text = models.TextField(blank=True, null=True) - token = models.CharField(max_length=64, blank=True, editable=False) - submitted_at = models.DateTimeField(null=True) # - def __str__(self): - return f"Answer for {self.question.question_text}" +class Invitation(models.Model): - -class Comment(models.Model): - # Comments data model - questionnaire = models.ForeignKey(Questionnaire, on_delete=models.CASCADE) - token = models.CharField(max_length=64, blank=True, editable=False) - text = models.TextField() - submitted_at = models.DateTimeField(null=True) + survey = models.ForeignKey(Survey, on_delete=models.CASCADE) + token = models.CharField(max_length=64, unique=True, blank=True, editable=False) + created_at = models.DateTimeField(auto_now_add=True) + used = models.BooleanField(default=False) def __str__(self): - return f"Comment on {self.questionnaire.title}" \ No newline at end of file + return f"Invitation for {self.survey.title}" + + def save(self, *args, **kwargs): + if not self.token: + self.token = secrets.token_urlsafe(32) + super().save(*args, **kwargs) + + def is_expired(self): + return timezone.now() > self.created_at + timezone.timedelta(days=7) \ No newline at end of file diff --git a/invites/templates/invitations/complete_invitation.html b/survey/templates/invitations/complete_invitation.html similarity index 100% rename from invites/templates/invitations/complete_invitation.html rename to survey/templates/invitations/complete_invitation.html diff --git a/invites/templates/invitations/send_invitation.html b/survey/templates/invitations/send_invitation.html similarity index 100% rename from invites/templates/invitations/send_invitation.html rename to survey/templates/invitations/send_invitation.html diff --git a/survey/templates/survey/survey.html b/survey/templates/survey/survey.html new file mode 100644 index 0000000..0c9ee4f --- /dev/null +++ b/survey/templates/survey/survey.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} + +{% block content %} +
{{ survey.description }}
+ +Reason ... e.g. token expired, no longer accepting responses,etc.
+ +{% endblock %} \ No newline at end of file diff --git a/survey/templates/survey/survey_response.html b/survey/templates/survey/survey_response.html new file mode 100644 index 0000000..7395716 --- /dev/null +++ b/survey/templates/survey/survey_response.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block content %} + +