import datetime from django import forms from django.forms import ModelMultipleChoiceField from django.contrib.auth.models import User from generic.widgets import UserSearchWidget from .models import Survey, SurveyQuestion, SurveyResponse, SurveyAnswer class SurveyForm(forms.ModelForm): author = ModelMultipleChoiceField( queryset=User.objects.all(), widget=UserSearchWidget(), required=False, help_text="Users who can view and manage this survey when it is set to private." ) class Meta: model = Survey fields = [ "name", "description", "before_text", "after_text", "active", "anonymous", "authors_only", "author" ] widgets = { "description": forms.Textarea(attrs={"rows": 3}), "before_text": forms.Textarea(attrs={"rows": 3, "placeholder": "Instructions shown before the survey starts..."}), "after_text": forms.Textarea(attrs={"rows": 3, "placeholder": "Thank you message shown after submission..."}), } class SurveyQuestionForm(forms.ModelForm): # Form-only fields for validation rules min_value = forms.IntegerField(required=False, label="Minimum Value (Integer)") max_value = forms.IntegerField(required=False, label="Maximum Value (Integer)") min_date = forms.DateField(required=False, widget=forms.DateInput(attrs={"type": "date"}), label="Minimum Date") max_date = forms.DateField(required=False, widget=forms.DateInput(attrs={"type": "date"}), label="Maximum Date") class Meta: model = SurveyQuestion fields = ["text", "question_type", "choices", "required", "position"] widgets = { "choices": forms.Textarea(attrs={"rows": 4, "placeholder": "Enter each choice on a new line"}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Populate initial values for validation fields from validation_rules dict if self.instance and self.instance.pk and self.instance.validation_rules: rules = self.instance.validation_rules self.fields["min_value"].initial = rules.get("min_value") self.fields["max_value"].initial = rules.get("max_value") min_date_val = rules.get("min_date") if min_date_val: try: self.fields["min_date"].initial = datetime.date.fromisoformat(min_date_val) except ValueError: pass max_date_val = rules.get("max_date") if max_date_val: try: self.fields["max_date"].initial = datetime.date.fromisoformat(max_date_val) except ValueError: pass def save(self, commit=True): instance = super().save(commit=False) # Build validation_rules dict rules = {} min_val = self.cleaned_data.get("min_value") max_val = self.cleaned_data.get("max_value") min_dt = self.cleaned_data.get("min_date") max_dt = self.cleaned_data.get("max_date") if min_val is not None: rules["min_value"] = min_val if max_val is not None: rules["max_value"] = max_val if min_dt is not None: rules["min_date"] = min_dt.isoformat() if max_dt is not None: rules["max_date"] = max_dt.isoformat() instance.validation_rules = rules if commit: instance.save() return instance class SurveyTakeForm(forms.Form): def __init__(self, *args, **kwargs): self.survey = kwargs.pop("survey") self.questions = list(self.survey.questions.all()) super().__init__(*args, **kwargs) for q in self.questions: field_name = f"question_{q.pk}" if q.question_type == SurveyQuestion.QuestionType.TEXT: self.fields[field_name] = forms.CharField( label=q.text, required=q.required, widget=forms.Textarea(attrs={"rows": 3, "class": "form-control"}) ) elif q.question_type == SurveyQuestion.QuestionType.CHOICE: choices = [(c, c) for c in q.get_choices_list()] self.fields[field_name] = forms.ChoiceField( label=q.text, required=q.required, choices=[("", "Select an option")] + choices, widget=forms.Select(attrs={"class": "form-select"}) ) elif q.question_type == SurveyQuestion.QuestionType.RATING: self.fields[field_name] = forms.ChoiceField( label=q.text, required=q.required, choices=[(str(i), str(i)) for i in range(1, 6)], widget=forms.RadioSelect(attrs={"class": "form-check-input"}), ) elif q.question_type == SurveyQuestion.QuestionType.INTEGER: rules = q.validation_rules or {} self.fields[field_name] = forms.IntegerField( label=q.text, required=q.required, min_value=rules.get("min_value"), max_value=rules.get("max_value"), widget=forms.NumberInput(attrs={"class": "form-control"}) ) elif q.question_type == SurveyQuestion.QuestionType.DATE: rules = q.validation_rules or {} attrs = {"type": "date", "class": "form-control"} if rules.get("min_date"): attrs["min"] = rules["min_date"] if rules.get("max_date"): attrs["max"] = rules["max_date"] self.fields[field_name] = forms.DateField( label=q.text, required=q.required, widget=forms.DateInput(attrs=attrs) ) def clean(self): cleaned_data = super().clean() for q in self.questions: field_name = f"question_{q.pk}" val = cleaned_data.get(field_name) if val is not None and val != "": rules = q.validation_rules or {} if q.question_type == SurveyQuestion.QuestionType.INTEGER: min_val = rules.get("min_value") max_val = rules.get("max_value") if min_val is not None and val < min_val: self.add_error(field_name, f"Value must be at least {min_val}.") if max_val is not None and val > max_val: self.add_error(field_name, f"Value cannot exceed {max_val}.") elif q.question_type == SurveyQuestion.QuestionType.DATE: min_dt_str = rules.get("min_date") max_dt_str = rules.get("max_date") if min_dt_str: min_dt = datetime.date.fromisoformat(min_dt_str) if val < min_dt: self.add_error(field_name, f"Date cannot be before {min_dt_str}.") if max_dt_str: max_dt = datetime.date.fromisoformat(max_dt_str) if val > max_dt: self.add_error(field_name, f"Date cannot be after {max_dt_str}.") return cleaned_data def save(self, user=None, cid=None, content_type=None, object_id=None, pre_or_post='GENERAL'): response_user = None if self.survey.anonymous else user response_cid = None if self.survey.anonymous else cid response = SurveyResponse.objects.create( survey=self.survey, user=response_user, cid=response_cid, content_type=content_type, object_id=object_id, pre_or_post=pre_or_post ) for q in self.questions: field_name = f"question_{q.pk}" val = self.cleaned_data.get(field_name) if val is not None and val != "": ans = SurveyAnswer(response=response, question=q) if q.question_type == SurveyQuestion.QuestionType.TEXT: ans.text_answer = val elif q.question_type == SurveyQuestion.QuestionType.CHOICE: ans.choice_answer = val elif q.question_type == SurveyQuestion.QuestionType.RATING: ans.rating_answer = int(val) elif q.question_type == SurveyQuestion.QuestionType.INTEGER: ans.integer_answer = int(val) elif q.question_type == SurveyQuestion.QuestionType.DATE: ans.date_answer = val ans.save() return response