import csv import uuid from datetime import date, timedelta from smtplib import SMTPRecipientsRefused from urllib.parse import parse_qs, urlencode, urlparse, urlunparse from django.contrib import messages from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.contrib.auth.views import ( LoginView as DjangoLoginView, LogoutView as DjangoLogoutView, redirect_to_login, ) from django.contrib.sites.models import Site from django.core.mail import send_mail from django.http import Http404, HttpResponse, HttpResponseForbidden from django.shortcuts import get_object_or_404, redirect, render from django.template import loader from django.urls import reverse_lazy from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.views.generic import FormView, TemplateView from django_cas_ng.views import LogoutView as CasLogoutView from icalendar import Calendar, Event as Vevent from bda.models import Spectacle, Tirage from gestioncof.autocomplete import cof_autocomplete from gestioncof.decorators import BuroRequiredMixin, buro_required, cof_required from gestioncof.forms import ( CalendarForm, ClubsForm, EventForm, EventFormset, EventStatusFilterForm, ExteAuthenticationForm, GestioncofConfigForm, PhoneForm, ProfileForm, RegistrationPassUserForm, RegistrationProfileForm, RegistrationUserForm, SurveyForm, SurveyStatusFilterForm, UserForm, ) from gestioncof.models import ( CalendarSubscription, Club, CofProfile, Event, EventCommentField, EventCommentValue, EventOption, EventOptionChoice, EventRegistration, Survey, SurveyAnswer, SurveyQuestion, SurveyQuestionAnswer, ) from shared.views import AutocompleteView, Select2QuerySetView class HomeView(LoginRequiredMixin, TemplateView): template_name = "gestioncof/home.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["surveys"] = Survey.objects.filter(old=False) context["events"] = Event.objects.filter(old=False) context["open_surveys"] = Survey.objects.filter(survey_open=True, old=False) context["active_tirages"] = Tirage.objects.filter(active=True) context["open_tirages"] = Tirage.objects.filter( active=True, ouverture__lte=timezone.now() ) context["now"] = timezone.now() return context class ResetComptes(BuroRequiredMixin, TemplateView): template_name = "gestioncof/reset_comptes.html" def post(self, request): nb_adherents = CofProfile.objects.filter(is_cof=True).count() CofProfile.objects.update( is_cof=False, date_adhesion=None, mailing_cof=False, mailing_bda=False, mailing_bda_revente=False, mailing_unernestaparis=False, ) context = super().get_context_data() context["is_done"] = True context["nb_adherents"] = nb_adherents return render(request, self.template_name, context) def login(request): if request.user.is_authenticated: return redirect("home") context = {} if request.method == "GET" and "next" in request.GET: context["next"] = request.GET["next"] return render(request, "login_switch.html", context) class LoginExtView(DjangoLoginView): template_name = "login.html" form_class = ExteAuthenticationForm def form_invalid(self, form): for e in form.non_field_errors().as_data(): if e.code in ["has_clipper", "no_password"]: return render(self.request, "login_error.html", {"error_code": e.code}) return super().form_invalid(form) class CustomCasLogoutView(CasLogoutView): """ Actuellement, le CAS de l'ENS est pété et n'a pas le bon paramètre GET pour rediriger après déconnexion. On change la redirection à la main dans la vue de logout. """ def get(self, request): # CasLogoutView.get() retourne un HttpResponseRedirect response = super().get(request) parse_result = urlparse(response.url) qd = parse_qs(parse_result.query) if "url" in qd.keys(): # Le 2e pop est nécessaire car CAS n'aime pas # les paramètres sous forme de liste qd["service"] = qd.pop("url").pop() # La méthode _replace est documentée ! new_url = parse_result._replace(query=urlencode(qd)) return redirect(urlunparse(new_url)) @login_required def logout(request, next_page=None): if next_page is None: next_page = request.GET.get("next", None) profile = getattr(request.user, "profile", None) if profile and profile.login_clipper: if next_page is None: # On ne voit pas les messages quand on se déconnecte de CAS msg = None else: msg = _("Déconnexion de GestioCOF et CAS réussie. À bientôt {}.") logout_view = CustomCasLogoutView.as_view() else: msg = _("Déconnexion de GestioCOF réussie. À bientôt {}.") logout_view = DjangoLogoutView.as_view( next_page=next_page, template_name="logout.html" ) if msg is not None: messages.success(request, msg.format(request.user.get_short_name())) return logout_view(request) @login_required def survey(request, survey_id): survey = get_object_or_404( Survey.objects.prefetch_related("questions", "questions__answers"), id=survey_id ) if not survey.survey_open or survey.old: raise Http404 success = False deleted = False if request.method == "POST": form = SurveyForm(request.POST, survey=survey) if request.POST.get("delete"): try: current_answer = SurveyAnswer.objects.get( user=request.user, survey=survey ) current_answer.delete() current_answer = None except SurveyAnswer.DoesNotExist: current_answer = None form = SurveyForm(survey=survey) success = True deleted = True else: if form.is_valid(): all_answers = [] for question_id, answers_ids in form.answers(): question = get_object_or_404( SurveyQuestion, id=question_id, survey=survey ) if type(answers_ids) != list: answers_ids = [answers_ids] if not question.multi_answers and len(answers_ids) > 1: raise Http404 for answer_id in answers_ids: if not answer_id: continue answer_id = int(answer_id) answer = SurveyQuestionAnswer.objects.get( id=answer_id, survey_question=question ) all_answers.append(answer) try: current_answer = SurveyAnswer.objects.get( user=request.user, survey=survey ) except SurveyAnswer.DoesNotExist: current_answer = SurveyAnswer(user=request.user, survey=survey) current_answer.save() current_answer.answers.set(all_answers) current_answer.save() success = True else: try: current_answer = SurveyAnswer.objects.get(user=request.user, survey=survey) form = SurveyForm(survey=survey, current_answers=current_answer.answers) except SurveyAnswer.DoesNotExist: current_answer = None form = SurveyForm(survey=survey) # Messages if success: if deleted: messages.success(request, "Votre réponse a bien été supprimée") else: messages.success( request, "Votre réponse a bien été enregistrée ! Vous " "pouvez cependant la modifier jusqu'à la fin " "du sondage.", ) return render( request, "gestioncof/survey.html", {"survey": survey, "form": form, "current_answer": current_answer}, ) def get_event_form_choices(event, form): all_choices = [] for option_id, choices_ids in form.choices(): option = get_object_or_404(EventOption, id=option_id, event=event) if type(choices_ids) != list: choices_ids = [choices_ids] if not option.multi_choices and len(choices_ids) > 1: raise Http404 for choice_id in choices_ids: if not choice_id: continue choice_id = int(choice_id) choice = EventOptionChoice.objects.get(id=choice_id, event_option=option) all_choices.append(choice) return all_choices def update_event_form_comments(event, form, registration): for commentfield_id, value in form.comments(): field = get_object_or_404(EventCommentField, id=commentfield_id, event=event) if value == field.default: continue (storage, _) = EventCommentValue.objects.get_or_create( commentfield=field, registration=registration ) storage.content = value storage.save() @login_required def event(request, event_id): event = get_object_or_404(Event, id=event_id) if (not event.registration_open) or event.old: raise Http404 success = False if request.method == "POST": form = EventForm(request.POST, event=event) if form.is_valid(): all_choices = get_event_form_choices(event, form) (current_registration, _) = EventRegistration.objects.get_or_create( user=request.user, event=event ) current_registration.options.set(all_choices) current_registration.save() success = True else: try: current_registration = EventRegistration.objects.get( user=request.user, event=event ) form = EventForm(event=event, current_choices=current_registration.options) except EventRegistration.DoesNotExist: form = EventForm(event=event) # Messages if success: messages.success( request, "Votre inscription a bien été enregistrée ! " "Vous pouvez cependant la modifier jusqu'à " "la fin des inscriptions.", ) return render(request, "gestioncof/event.html", {"event": event, "form": form}) def clean_post_for_status(initial): d = initial.copy() for k, v in d.items(): if k.startswith("id_"): del d[k] d[k[3:]] = v return d @buro_required def event_status(request, event_id): event = get_object_or_404(Event, id=event_id) registrations_query = EventRegistration.objects.filter(event=event) post_data = clean_post_for_status(request.POST) form = EventStatusFilterForm(post_data or None, event=event) if form.is_valid(): for option_id, choice_id, value in form.filters(): if option_id == "has_paid": if value == "yes": registrations_query = registrations_query.filter(paid=True) elif value == "no": registrations_query = registrations_query.filter(paid=False) continue choice = get_object_or_404( EventOptionChoice, id=choice_id, event_option__id=option_id ) if value == "none": continue if value == "yes": registrations_query = registrations_query.filter( options__id__exact=choice.id ) elif value == "no": registrations_query = registrations_query.exclude( options__id__exact=choice.id ) user_choices = registrations_query.prefetch_related("user").all() options = EventOption.objects.filter(event=event).all() choices_count = {} for option in options: for choice in option.choices.all(): choices_count[choice.id] = 0 for user_choice in user_choices: for choice in user_choice.options.all(): choices_count[choice.id] += 1 return render( request, "event_status.html", { "event": event, "user_choices": user_choices, "options": options, "choices_count": choices_count, "form": form, }, ) @buro_required def survey_status(request, survey_id): survey = get_object_or_404(Survey, id=survey_id) answers_query = SurveyAnswer.objects.filter(survey=survey) post_data = clean_post_for_status(request.POST) form = SurveyStatusFilterForm(post_data or None, survey=survey) if form.is_valid(): for question_id, answer_id, value in form.filters(): answer = get_object_or_404( SurveyQuestionAnswer, id=answer_id, survey_question__id=question_id ) if value == "none": continue if value == "yes": answers_query = answers_query.filter(answers__id__exact=answer.id) elif value == "no": answers_query = answers_query.exclude(answers__id__exact=answer.id) user_answers = answers_query.prefetch_related("user").all() questions = SurveyQuestion.objects.filter(survey=survey).all() answers_count = {} for question in questions: for answer in question.answers.all(): answers_count[answer.id] = 0 for user_answer in user_answers: for answer in user_answer.answers.all(): answers_count[answer.id] += 1 return render( request, "survey_status.html", { "survey": survey, "user_answers": user_answers, "questions": questions, "answers_count": answers_count, "form": form, }, ) @login_required def profile(request): user = request.user data = request.POST if request.method == "POST" else None user_form = UserForm(data=data, instance=user, prefix="u") profile_form_klass = ProfileForm if user.profile.is_cof else PhoneForm profile_form = profile_form_klass(data=data, instance=user.profile, prefix="p") if request.method == "POST": if user_form.is_valid() and profile_form.is_valid(): user_form.save() profile_form.save() messages.success(request, _("Votre profil a été mis à jour avec succès !")) context = {"user_form": user_form, "profile_form": profile_form} return render(request, "gestioncof/profile.html", context) def registration_set_ro_fields(user_form, profile_form): user_form.fields["username"].widget.attrs["readonly"] = True profile_form.fields["login_clipper"].widget.attrs["readonly"] = True @buro_required def registration_form2(request, login_clipper=None, username=None, fullname=None): events = Event.objects.filter(old=False).all() member = None if login_clipper: try: # check if the given user is already registered member = User.objects.get(username=login_clipper) username = member.username login_clipper = None except User.DoesNotExist: # new user, but prefill # user user_form = RegistrationUserForm( initial={ "username": login_clipper, "email": "%s@clipper.ens.fr" % login_clipper, } ) if fullname: bits = fullname.split(" ") user_form.fields["first_name"].initial = bits[0] if len(bits) > 1: user_form.fields["last_name"].initial = " ".join(bits[1:]) # profile profile_form = RegistrationProfileForm( initial={"login_clipper": login_clipper} ) registration_set_ro_fields(user_form, profile_form) # events & clubs event_formset = EventFormset(events=events, prefix="events") clubs_form = ClubsForm() if username: member = get_object_or_404(User, username=username) (profile, _) = CofProfile.objects.get_or_create(user=member) # already existing, prefill user_form = RegistrationUserForm(instance=member) profile_form = RegistrationProfileForm(instance=profile) registration_set_ro_fields(user_form, profile_form) # events current_registrations = [] for event in events: try: current_registrations.append( EventRegistration.objects.get(user=member, event=event) ) except EventRegistration.DoesNotExist: current_registrations.append(None) event_formset = EventFormset( events=events, prefix="events", current_registrations=current_registrations ) # Clubs clubs_form = ClubsForm(initial={"clubs": member.clubs.all()}) elif not login_clipper: # new user user_form = RegistrationPassUserForm() profile_form = RegistrationProfileForm() event_formset = EventFormset(events=events, prefix="events") clubs_form = ClubsForm() return render( request, "gestioncof/registration_form.html", { "member": member, "login_clipper": login_clipper, "user_form": user_form, "profile_form": profile_form, "event_formset": event_formset, "clubs_form": clubs_form, }, ) def notify_new_member(request, member: User): if not member.email: messages.warning( request, "GestioCOF n'a pas d'adresse mail pour {}, ".format(member) + "aucun email de bienvenue n'a été envoyé", ) return # Try to send a welcome email and report SMTP errors try: send_mail( "Bienvenue au COF", loader.render_to_string( "gestioncof/mails/welcome.txt", context={"member": member} ), "cof@ens.fr", [member.email], ) except SMTPRecipientsRefused: messages.error( request, "Error lors de l'envoi de l'email de bienvenue à {} ({})".format( member, member.email ), ) @buro_required def registration(request): if request.POST: request_dict = request.POST.copy() member = None login_clipper = None # ----- # Remplissage des formulaires # ----- if "password1" in request_dict or "password2" in request_dict: user_form = RegistrationPassUserForm(request_dict) else: user_form = RegistrationUserForm(request_dict) profile_form = RegistrationProfileForm(request_dict) clubs_form = ClubsForm(request_dict) events = Event.objects.filter(old=False).all() event_formset = EventFormset(events=events, data=request_dict, prefix="events") if "user_exists" in request_dict and request_dict["user_exists"]: username = request_dict["username"] try: member = User.objects.get(username=username) user_form = RegistrationUserForm(request_dict, instance=member) if member.profile.login_clipper: login_clipper = member.profile.login_clipper except User.DoesNotExist: pass else: pass # ----- # Validation des formulaires # ----- if user_form.is_valid(): member = user_form.save() profile, _ = CofProfile.objects.get_or_create(user=member) was_cof = profile.is_cof # Maintenant on remplit le formulaire de profil profile_form = RegistrationProfileForm(request_dict, instance=profile) if ( profile_form.is_valid() and event_formset.is_valid() and clubs_form.is_valid() ): # Enregistrement du profil profile = profile_form.save() if profile.is_cof and not was_cof: notify_new_member(request, member) profile.date_adhesion = date.today() profile.save() # Enregistrement des inscriptions aux événements for form in event_formset: if "status" not in form.cleaned_data: form.cleaned_data["status"] = "no" if form.cleaned_data["status"] == "no": try: current_registration = EventRegistration.objects.get( user=member, event=form.event ) current_registration.delete() except EventRegistration.DoesNotExist: pass continue all_choices = get_event_form_choices(form.event, form) ( current_registration, created_reg, ) = EventRegistration.objects.get_or_create( user=member, event=form.event ) update_event_form_comments(form.event, form, current_registration) current_registration.options.set(all_choices) current_registration.paid = form.cleaned_data["status"] == "paid" current_registration.save() # if form.event.title == "Mega 15" and created_reg: # field = EventCommentField.objects.get( # event=form.event, name="Commentaires") # try: # comments = EventCommentValue.objects.get( # commentfield=field, # registration=current_registration).content # except EventCommentValue.DoesNotExist: # comments = field.default # FIXME : il faut faire quelque chose de propre ici, # par exemple écrire un mail générique pour # l'inscription aux événements et/ou donner la # possibilité d'associer un mail aux événements # send_custom_mail(...) # Enregistrement des inscriptions aux clubs member.clubs.clear() for club in clubs_form.cleaned_data["clubs"]: club.membres.add(member) club.save() # --- # Success # --- msg = ( "L'inscription de {:s} ({:s}) a été " "enregistrée avec succès.".format( member.get_full_name(), member.email ) ) if profile.is_cof: msg += "\nIl est désormais membre du COF n°{:d} !".format( member.profile.id ) messages.success(request, msg, extra_tags="safe") return render( request, "gestioncof/registration_post.html", { "user_form": user_form, "profile_form": profile_form, "member": member, "login_clipper": login_clipper, "event_formset": event_formset, "clubs_form": clubs_form, }, ) else: return render(request, "registration.html") # ----- # Clubs # ----- @login_required def membres_club(request, name): # Vérification des permissions : l'utilisateur doit être membre du burô # ou respo du club. user = request.user club = get_object_or_404(Club, name=name) if not request.user.profile.is_buro and club not in user.clubs_geres.all(): return HttpResponseForbidden("

Permission denied

") members_no_respo = club.membres.exclude(clubs_geres=club).all() return render( request, "membres_clubs.html", {"club": club, "members_no_respo": members_no_respo}, ) @buro_required def change_respo(request, club_name, user_id): club = get_object_or_404(Club, name=club_name) user = get_object_or_404(User, id=user_id) if user in club.respos.all(): club.respos.remove(user) elif user in club.membres.all(): club.respos.add(user) else: raise Http404 return redirect("membres-club", name=club_name) @cof_required def liste_clubs(request): clubs = Club.objects if request.user.profile.is_buro: data = {"owned_clubs": clubs.all()} else: data = { "owned_clubs": request.user.clubs_geres.all(), "other_clubs": clubs.exclude(respos=request.user), } return render(request, "liste_clubs.html", data) @buro_required def export_members(request): response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = "attachment; filename=membres_cof.csv" writer = csv.writer(response) for profile in CofProfile.objects.filter(is_cof=True).all(): user = profile.user bits = [ user.id, user.username, user.first_name, user.last_name, user.email, profile.phone, profile.occupation, profile.departement, profile.type_cotiz, profile.date_adhesion, ] writer.writerow([str(bit) for bit in bits]) return response # ---------------------------------------- # Début des exports Mega machins hardcodés # ---------------------------------------- MEGA_YEAR = 2018 MEGA_EVENT_NAME = "MEGA 2018" MEGA_COMMENTFIELD_NAME = "Commentaires" MEGA_CONSCRITORGAFIELD_NAME = "Orga ? Conscrit ?" MEGA_CONSCRIT = "Conscrit" MEGA_ORGA = "Orga" def csv_export_mega(filename, qs): response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = "attachment; filename=" + filename writer = csv.writer(response) for reg in qs.all(): user = reg.user profile = user.profile comments = "---".join([comment.content for comment in reg.comments.all()]) bits = [ user.username, user.first_name, user.last_name, user.email, profile.phone, user.id, profile.comments if profile.comments else "", comments, ] writer.writerow([str(bit) for bit in bits]) return response @buro_required def export_mega_remarksonly(request): filename = "remarques_mega_{}.csv".format(MEGA_YEAR) response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = "attachment; filename=" + filename writer = csv.writer(response) event = Event.objects.get(title=MEGA_EVENT_NAME) commentfield = event.commentfields.get(name=MEGA_COMMENTFIELD_NAME) for val in commentfield.values.all(): reg = val.registration user = reg.user profile = user.profile bits = [ user.username, user.first_name, user.last_name, user.email, profile.phone, profile.id, profile.comments, val.content, ] writer.writerow([str(bit) for bit in bits]) return response # @buro_required # def export_mega_bytype(request, type): # types = {"orga-actif": "Orga élève", # "orga-branleur": "Orga étudiant", # "conscrit-eleve": "Conscrit élève", # "conscrit-etudiant": "Conscrit étudiant"} # # if type not in types: # raise Http404 # # event = Event.objects.get(title="MEGA 2017") # type_option = event.options.get(name="Type") # participant_type = type_option.choices.get(value=types[type]).id # qs = EventRegistration.objects.filter(event=event).filter( # options__id__exact=participant_type) # return csv_export_mega(type + '_mega_2017.csv', qs) @buro_required def export_mega_orgas(request): event = Event.objects.get(title=MEGA_EVENT_NAME) type_option = event.options.get(name=MEGA_CONSCRITORGAFIELD_NAME) participant_type = type_option.choices.get(value=MEGA_ORGA).id qs = EventRegistration.objects.filter(event=event).filter( options__id=participant_type ) return csv_export_mega("orgas_mega_{}.csv".format(MEGA_YEAR), qs) @buro_required def export_mega_participants(request): event = Event.objects.get(title=MEGA_EVENT_NAME) type_option = event.options.get(name=MEGA_CONSCRITORGAFIELD_NAME) participant_type = type_option.choices.get(value=MEGA_CONSCRIT).id qs = EventRegistration.objects.filter(event=event).filter( options__id=participant_type ) return csv_export_mega("conscrits_mega_{}.csv".format(MEGA_YEAR), qs) @buro_required def export_mega(request): event = Event.objects.get(title=MEGA_EVENT_NAME) qs = EventRegistration.objects.filter(event=event).order_by("user__username") return csv_export_mega("all_mega_{}.csv".format(MEGA_YEAR), qs) # ------------------------------ # Fin des exports Mega hardcodés # ------------------------------ @buro_required def utile_cof(request): return render(request, "gestioncof/utile_cof.html", {}) @buro_required def utile_bda(request): tirages = Tirage.objects.all() return render(request, "utile_bda.html", {"tirages": tirages}) @buro_required def liste_bdadiff(request): titre = "BdA diffusion" personnes = CofProfile.objects.filter(mailing_bda=True, is_cof=True).all() return render(request, "liste_mails.html", {"titre": titre, "personnes": personnes}) @buro_required def liste_bdarevente(request): titre = "BdA revente" personnes = CofProfile.objects.filter(mailing_bda_revente=True, is_cof=True).all() return render(request, "liste_mails.html", {"titre": titre, "personnes": personnes}) @buro_required def liste_diffcof(request): titre = "Diffusion COF" personnes = CofProfile.objects.filter(mailing_cof=True, is_cof=True).all() return render(request, "liste_mails.html", {"titre": titre, "personnes": personnes}) @cof_required def calendar(request): try: instance = CalendarSubscription.objects.get(user=request.user) except CalendarSubscription.DoesNotExist: instance = None if request.method == "POST": form = CalendarForm(request.POST, instance=instance) if form.is_valid(): subscription = form.save(commit=False) if instance is None: subscription.user = request.user subscription.token = uuid.uuid4() subscription.save() form.save_m2m() messages.success(request, "Calendrier mis à jour avec succès.") return render( request, "gestioncof/calendar_subscription.html", {"form": form, "token": str(subscription.token)}, ) else: messages.error(request, "Formulaire incorrect.") return render( request, "gestioncof/calendar_subscription.html", {"form": form} ) else: return render( request, "gestioncof/calendar_subscription.html", { "form": CalendarForm(instance=instance), "token": instance.token if instance else None, }, ) def calendar_ics(request, token): subscription = get_object_or_404(CalendarSubscription, token=token) shows = subscription.other_shows.all() if subscription.subscribe_to_my_shows: shows |= Spectacle.objects.filter( attribues__participant__user=subscription.user, tirage__active=True ) shows = shows.distinct() vcal = Calendar() site = Site.objects.get_current() for show in shows: vevent = Vevent() vevent.add("dtstart", show.date) vevent.add("dtend", show.date + timedelta(seconds=7200)) vevent.add("summary", show.title) vevent.add("location", show.location.name) vevent.add( "uid", "show-{:d}-{:d}@{:s}".format(show.pk, show.tirage_id, site.domain) ) vcal.add_component(vevent) if subscription.subscribe_to_events: for event in Event.objects.filter(old=False).all(): vevent = Vevent() vevent.add("dtstart", event.start_date) vevent.add("dtend", event.end_date) vevent.add("summary", event.title) vevent.add("location", event.location) vevent.add("description", event.description) vevent.add("uid", "event-{:d}@{:s}".format(event.pk, site.domain)) vcal.add_component(vevent) response = HttpResponse(content=vcal.to_ical()) response["Content-Type"] = "text/calendar" return response class ConfigUpdate(FormView): form_class = GestioncofConfigForm template_name = "gestioncof/banner_update.html" success_url = reverse_lazy("home") def dispatch(self, request, *args, **kwargs): if request.user is None or not request.user.is_superuser: return redirect_to_login(request.get_full_path()) return super().dispatch(request, *args, **kwargs) def form_valid(self, form): form.save() return super().form_valid(form) ## # Autocomplete views # # https://django-autocomplete-light.readthedocs.io/en/master/tutorial.html#create-an-autocomplete-view ## class UserAutocompleteView(BuroRequiredMixin, Select2QuerySetView): model = User search_fields = ("username", "first_name", "last_name") class RegistrationAutocompleteView(BuroRequiredMixin, AutocompleteView): template_name = "gestioncof/search_results.html" search_composer = cof_autocomplete