diff --git a/apps/activity/forms.py b/apps/activity/forms.py index dcbd3c9d..dced014a 100644 --- a/apps/activity/forms.py +++ b/apps/activity/forms.py @@ -4,7 +4,7 @@ from datetime import timedelta, datetime from django import forms from django.contrib.contenttypes.models import ContentType -from django.utils.translation import gettext as _ +from django.utils.translation import gettext_lazy as _ from member.models import Club from note.models import NoteUser, Note from note_kfet.inputs import DateTimePickerInput, Autocomplete diff --git a/apps/registration/views.py b/apps/registration/views.py index 6a9e91f4..0e5977d7 100644 --- a/apps/registration/views.py +++ b/apps/registration/views.py @@ -46,6 +46,7 @@ class UserCreateView(CreateView): del wei_form.fields["user"] del wei_form.fields["caution_check"] del wei_form.fields["first_year"] + del wei_form.fields["information_json"] context["wei_form"] = wei_form context["wei_registration_form"] = WEISignupForm() diff --git a/apps/wei/admin.py b/apps/wei/admin.py index 4ebcec3a..f93a44ed 100644 --- a/apps/wei/admin.py +++ b/apps/wei/admin.py @@ -3,11 +3,11 @@ from django.contrib import admin -from .models import WEIClub, WEIRegistration, WEIRole, Bus, BusTeam - +from .models import WEIClub, WEIRegistration, WEIMembership, WEIRole, Bus, BusTeam admin.site.register(WEIClub) admin.site.register(WEIRegistration) +admin.site.register(WEIMembership) admin.site.register(WEIRole) admin.site.register(Bus) admin.site.register(BusTeam) diff --git a/apps/wei/api/views.py b/apps/wei/api/views.py index 9ab3a139..6f2b2cd1 100644 --- a/apps/wei/api/views.py +++ b/apps/wei/api/views.py @@ -1,6 +1,6 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later - +from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import SearchFilter from api.viewsets import ReadProtectedModelViewSet @@ -16,8 +16,9 @@ class WEIClubViewSet(ReadProtectedModelViewSet): """ queryset = WEIClub.objects.all() serializer_class = WEIClubSerializer - filter_backends = [SearchFilter] + filter_backends = [SearchFilter, DjangoFilterBackend] search_fields = ['$name', ] + filterset_fields = ['name', 'year', ] class BusViewSet(ReadProtectedModelViewSet): @@ -28,8 +29,9 @@ class BusViewSet(ReadProtectedModelViewSet): """ queryset = Bus.objects.all() serializer_class = BusSerializer - filter_backends = [SearchFilter] + filter_backends = [SearchFilter, DjangoFilterBackend] search_fields = ['$name', ] + filterset_fields = ['name', 'wei', ] class BusTeamViewSet(ReadProtectedModelViewSet): @@ -40,5 +42,6 @@ class BusTeamViewSet(ReadProtectedModelViewSet): """ queryset = BusTeam.objects.all() serializer_class = BusTeamSerializer - filter_backends = [SearchFilter] + filter_backends = [SearchFilter, DjangoFilterBackend] search_fields = ['$name', ] + filterset_fields = ['name', 'bus', 'wei', ] diff --git a/apps/wei/forms/__init__.py b/apps/wei/forms/__init__.py index 24ba02e2..af948157 100644 --- a/apps/wei/forms/__init__.py +++ b/apps/wei/forms/__init__.py @@ -1,8 +1,8 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from .registration import * -from .surveys import * +from .registration import WEIForm, WEIRegistrationForm, WEIMembershipForm, BusForm, BusTeamForm +from .surveys import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, CurrentSurvey __all__ = [ 'WEIForm', 'WEIRegistrationForm', 'WEIMembershipForm', 'BusForm', 'BusTeamForm', diff --git a/apps/wei/forms/registration.py b/apps/wei/forms/registration.py index b76b9954..24f17ff9 100644 --- a/apps/wei/forms/registration.py +++ b/apps/wei/forms/registration.py @@ -3,9 +3,10 @@ from django import forms from django.contrib.auth.models import User +from django.utils.translation import gettext_lazy as _ from note_kfet.inputs import AmountInput, DatePickerInput, Autocomplete, ColorWidget -from wei.models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership, WEIRole +from ..models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership, WEIRole class WEIForm(forms.ModelForm): @@ -25,7 +26,7 @@ class WEIForm(forms.ModelForm): class WEIRegistrationForm(forms.ModelForm): class Meta: model = WEIRegistration - exclude = ('wei', 'information_json', ) + exclude = ('wei', ) widgets = { "user": Autocomplete( User, @@ -42,6 +43,12 @@ class WEIRegistrationForm(forms.ModelForm): class WEIMembershipForm(forms.ModelForm): roles = forms.ModelMultipleChoiceField(queryset=WEIRole.objects) + def clean(self): + cleaned_data = super().clean() + if cleaned_data["team"] is not None and cleaned_data["team"].bus != cleaned_data["bus"]: + self.add_error('bus', _("This team doesn't belong to the given team.")) + return cleaned_data + class Meta: model = WEIMembership fields = ('roles', 'bus', 'team',) diff --git a/apps/wei/forms/surveys/base.py b/apps/wei/forms/surveys/base.py index 0bb6d344..dc8256c3 100644 --- a/apps/wei/forms/surveys/base.py +++ b/apps/wei/forms/surveys/base.py @@ -1,46 +1,18 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from typing import Optional + +from django.db.models import QuerySet +from django.forms import Form + from ...models import WEIClub, WEIRegistration, Bus -class WEISurvey: - year = None - step = 0 - - def __init__(self, registration): - self.registration = registration - self.information = self.get_survey_information_class()(registration) - - def get_wei(self): - return WEIClub.objects.get(year=self.year) - - def get_survey_information_class(self): - raise NotImplementedError - - def get_form_class(self): - raise NotImplementedError - - def update_form(self, form): - pass - - @staticmethod - def get_algorithm_class(): - raise NotImplementedError - - def form_valid(self, form): - raise NotImplementedError - - def save(self): - self.information.save(self.registration) - - def select_bus(self, bus): - self.information.selected_bus_pk = bus.pk - self.information.selected_bus_name = bus.name - self.information.valid = True - - class WEISurveyInformation: + """ + Abstract data of the survey. + """ valid = False selected_bus_pk = None selected_bus_name = None @@ -48,22 +20,142 @@ class WEISurveyInformation: def __init__(self, registration): self.__dict__.update(registration.information) - def get_selected_bus(self): + def get_selected_bus(self) -> Optional[Bus]: + """ + If the algorithm ran, return the prefered bus according to the survey. + In the other case, return None. + """ if not self.valid: return None return Bus.objects.get(pk=self.selected_bus_pk) - def save(self, registration): + def save(self, registration) -> None: + """ + Store the data of the survey into the database, with the information of the registration. + """ registration.information = self.__dict__ registration.save() class WEISurveyAlgorithm: - def get_survey_class(self): + """ + Abstract algorithm that attributes a bus to each new member. + """ + + @classmethod + def get_survey_class(cls): + """ + The class of the survey associated with this algorithm. + """ raise NotImplementedError - def get_registrations(self): - return WEIRegistration.objects.filter(wei__year=self.get_survey_class().year, first_year=True).all() + @classmethod + def get_registrations(cls) -> QuerySet: + """ + Queryset of all first year registrations + """ + return WEIRegistration.objects.filter(wei__year=cls.get_survey_class().get_year(), first_year=True) - def run_algorithm(self): + @classmethod + def get_buses(cls) -> QuerySet: + """ + Queryset of all buses of the associated wei. + """ + return Bus.objects.filter(wei__year=cls.get_survey_class().get_year()) + + def run_algorithm(self) -> None: + """ + Once this method implemented, run the algorithm that attributes a bus to each first year member. + This method can be run in command line through ``python manage.py wei_algorithm`` + See ``wei.management.commmands.wei_algorithm`` + This method must call Survey.select_bus for each survey. + """ raise NotImplementedError + + +class WEISurvey: + """ + Survey associated to a first year WEI registration. + The data is stored into WEISurveyInformation, this class acts as a manager. + This is an abstract class: this has to be extended each year to implement custom methods. + """ + + def __init__(self, registration: WEIRegistration): + self.registration = registration + self.information = self.get_survey_information_class()(registration) + + @classmethod + def get_year(cls) -> int: + """ + Get year of the wei concerned by the type of the survey. + """ + raise NotImplementedError + + @classmethod + def get_wei(cls) -> WEIClub: + """ + The WEI associated to this kind of survey. + """ + return WEIClub.objects.get(year=cls.get_year()) + + @classmethod + def get_survey_information_class(cls): + """ + The class of the data (extending WEISurveyInformation). + """ + raise NotImplementedError + + def get_form_class(self) -> Form: + """ + The form class of the survey. + This is proper to the status of the survey: the form class can evolve according to the progress of the survey. + """ + raise NotImplementedError + + def update_form(self, form) -> None: + """ + Once the form is instanciated, the information can be updated with the information of the registration + and the information of the survey. + This method is called once the form is created. + """ + pass + + def form_valid(self, form) -> None: + """ + Called when the information of the form are validated. + This method should update the information of the survey. + """ + raise NotImplementedError + + def is_complete(self) -> bool: + """ + Return True if the survey is complete. + If the survey is complete, then the button "Next" will display some text for the end of the survey. + If not, the survey is reloaded and continues. + """ + raise NotImplementedError + + def save(self) -> None: + """ + Store the information of the survey into the database. + """ + self.information.save(self.registration) + self.registration.save() + + @classmethod + def get_algorithm_class(cls): + """ + Algorithm class associated to the survey. + The algorithm, extending WEISurveyAlgorithm, should associate a bus to each first year member. + The association is not permanent: that's only a suggestion. + """ + raise NotImplementedError + + def select_bus(self, bus) -> None: + """ + Set the suggestion into the data of the membership. + :param bus: The bus suggested. + """ + self.information.selected_bus_pk = bus.pk + self.information.selected_bus_name = bus.name + self.information.valid = True diff --git a/apps/wei/forms/surveys/wei2020.py b/apps/wei/forms/surveys/wei2020.py index 489afc5b..4f60f6d4 100644 --- a/apps/wei/forms/surveys/wei2020.py +++ b/apps/wei/forms/surveys/wei2020.py @@ -8,29 +8,50 @@ from ...models import Bus class WEISurveyForm2020(forms.Form): + """ + Survey form for the year 2020. + For now, that's only a Bus selector. + TODO: Do a better survey (later) + """ bus = forms.ModelChoiceField( Bus.objects, ) def set_registration(self, registration): + """ + Filter the bus selector with the buses of the current WEI. + """ self.fields["bus"].queryset = Bus.objects.filter(wei=registration.wei) class WEISurveyInformation2020(WEISurveyInformation): + """ + We store the id of the selected bus. We store only the name, but is not used in the selection: + that's only for humans that try to read data. + """ chosen_bus_pk = None chosen_bus_name = None class WEISurvey2020(WEISurvey): - year = 2020 + """ + Survey for the year 2020. + """ + @classmethod + def get_year(cls): + return 2020 - def get_survey_information_class(self): + @classmethod + def get_survey_information_class(cls): return WEISurveyInformation2020 def get_form_class(self): return WEISurveyForm2020 def update_form(self, form): + """ + Filter the bus selector with the buses of the WEI. + """ form.set_registration(self.registration) def form_valid(self, form): @@ -39,13 +60,26 @@ class WEISurvey2020(WEISurvey): self.information.chosen_bus_name = bus.name self.save() - @staticmethod - def get_algorithm_class(): + @classmethod + def get_algorithm_class(cls): return WEISurveyAlgorithm2020 + def is_complete(self) -> bool: + """ + The survey is complete once the bus is chosen. + """ + return self.information.chosen_bus_pk is not None + class WEISurveyAlgorithm2020(WEISurveyAlgorithm): - def get_survey_class(self): + """ + The algorithm class for the year 2020. + For now, the algorithm is quite simple: the selected bus is the chosen bus. + TODO: Improve this algorithm. + """ + + @classmethod + def get_survey_class(cls): return WEISurvey2020 def run_algorithm(self): diff --git a/apps/wei/management/__init__.py b/apps/wei/management/__init__.py index e69de29b..4e945ad5 100644 --- a/apps/wei/management/__init__.py +++ b/apps/wei/management/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/apps/wei/management/commands/wei_algorithm.py b/apps/wei/management/commands/wei_algorithm.py index 8eb7e75e..d639117e 100644 --- a/apps/wei/management/commands/wei_algorithm.py +++ b/apps/wei/management/commands/wei_algorithm.py @@ -4,7 +4,7 @@ from django.core.management import BaseCommand from django.utils.translation import gettext_lazy as _ -from wei.forms import CurrentSurvey +from ...forms import CurrentSurvey class Command(BaseCommand): diff --git a/apps/wei/urls.py b/apps/wei/urls.py index 423b9da4..e254801e 100644 --- a/apps/wei/urls.py +++ b/apps/wei/urls.py @@ -5,7 +5,8 @@ from django.urls import path from .views import CurrentWEIDetailView, WEIListView, WEICreateView, WEIDetailView, WEIUpdateView,\ BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView,\ - WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, WEIValidateRegistrationView, WEISurveyView + WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, WEIValidateRegistrationView,\ + WEISurveyView, WEISurveyEndView app_name = 'wei' @@ -28,4 +29,5 @@ urlpatterns = [ path('edit-registration//', WEIUpdateRegistrationView.as_view(), name="wei_update_registration"), path('validate//', WEIValidateRegistrationView.as_view(), name="validate_registration"), path('survey//', WEISurveyView.as_view(), name="wei_survey"), + path('survey//end/', WEISurveyEndView.as_view(), name="wei_survey_end"), ] diff --git a/apps/wei/views.py b/apps/wei/views.py index cf89f8de..d1d5a75f 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -6,8 +6,9 @@ from datetime import datetime, date from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.db.models import Q +from django.shortcuts import redirect from django.urls import reverse_lazy -from django.views.generic import DetailView, UpdateView, CreateView, RedirectView +from django.views.generic import DetailView, UpdateView, CreateView, RedirectView, TemplateView from django.utils.translation import gettext_lazy as _ from django.views.generic.edit import BaseFormView from django_tables2 import SingleTableView @@ -294,6 +295,7 @@ class WEIRegister1AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): form.fields["user"].disabled = True del form.fields["first_year"] del form.fields["caution_check"] + del form.fields["information_json"] return form def form_valid(self, form): @@ -332,6 +334,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): del form.fields["ml_events_registration"] del form.fields["ml_art_registration"] del form.fields["ml_sport_registration"] + del form.fields["information_json"] return form @@ -398,6 +401,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Crea def get_form(self, form_class=None): form = super().get_form(form_class) registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) + form.fields["bus"].widget.attrs["api_url"] = "/api/wei/bus/?wei=" + str(registration.wei.pk) if registration.first_year: del form.fields["roles"] survey = CurrentSurvey(registration) @@ -457,20 +461,35 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Crea class WEISurveyView(BaseFormView, DetailView): + """ + Display the survey for the WEI for first + """ model = WEIRegistration template_name = "wei/survey.html" survey = None - def setup(self, request, *args, **kwargs): - ret = super().setup(request, *args, **kwargs) - return ret + def get(self, request, *args, **kwargs): + obj = self.get_object() + if not self.survey: + self.survey = CurrentSurvey(obj) + # If the survey is complete, then display the end page. + if self.survey.is_complete(): + return redirect(reverse_lazy('wei:wei_survey_end', args=(self.survey.registration.pk,))) + # Non first year members don't have a survey + if not obj.first_year: + return redirect(reverse_lazy('wei:wei_survey_end', args=(self.survey.registration.pk,))) + return super().get(request, *args, **kwargs) def get_form_class(self): - if not self.survey: - self.survey = CurrentSurvey(self.get_object()) + """ + Get the survey form. It may depend on the current state of the survey. + """ return self.survey.get_form_class() def get_form(self, form_class=None): + """ + Update the form with the data of the survey. + """ form = super().get_form(form_class) self.survey.update_form(form) return form @@ -482,8 +501,21 @@ class WEISurveyView(BaseFormView, DetailView): return context def form_valid(self, form): + """ + Update the survey with the data of the form. + """ self.survey.form_valid(form) return super().form_valid(form) def get_success_url(self): return reverse_lazy('wei:wei_survey', args=(self.get_object().pk,)) + + +class WEISurveyEndView(TemplateView): + template_name = "wei/survey_end.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["club"] = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei + context["title"] = _("Survey WEI") + return context diff --git a/static/js/autocomplete_model.js b/static/js/autocomplete_model.js index 8c3f6f09..aa1d220c 100644 --- a/static/js/autocomplete_model.js +++ b/static/js/autocomplete_model.js @@ -11,7 +11,7 @@ $(document).ready(function () { name_field = "name"; let input = target.val(); - $.getJSON(api_url + "?format=json&search=^" + input + api_url_suffix, function(objects) { + $.getJSON(api_url + (api_url.includes("?") ? "&" : "?") + "format=json&search=^" + input + api_url_suffix, function(objects) { let html = ""; objects.results.forEach(function (obj) { diff --git a/templates/wei/survey_end.html b/templates/wei/survey_end.html new file mode 100644 index 00000000..00b7088b --- /dev/null +++ b/templates/wei/survey_end.html @@ -0,0 +1,22 @@ +{% extends "member/noteowner_detail.html" %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block profile_info %} +{% include "wei/weiclub_info.html" %} +{% endblock %} + +{% block profile_content %} +
+
+

{% trans "Survey WEI" %}

+
+
+

+ {% blocktrans %} +The survey is now ended. Your answers have been saved. + {% endblocktrans %} +

+
+
+{% endblock %} diff --git a/templates/wei/weimembership_form.html b/templates/wei/weimembership_form.html index 75ebad0e..152d2a1f 100644 --- a/templates/wei/weimembership_form.html +++ b/templates/wei/weimembership_form.html @@ -167,3 +167,15 @@ {% endblock %} + +{% block extrajavascript %} + +{% endblock %}