diff --git a/oser_backend/settings/common.py b/oser_backend/settings/common.py index 9579af4b53b12213685b37613ac01aa7bdc42a30..84ccf8246385333d1cfbb3d32340090720319435 100644 --- a/oser_backend/settings/common.py +++ b/oser_backend/settings/common.py @@ -255,16 +255,16 @@ EMAIL_PORT = 587 EMAIL_USE_TLS = True # Toggle sandbox mode (when running in DEBUG mode) -SENDGRID_SANDBOX_MODE_IN_DEBUG=False +SENDGRID_SANDBOX_MODE_IN_DEBUG = False # echo to stdout or any other file-like object that is passed to the backend via the stream kwarg. -SENDGRID_ECHO_TO_STDOUT=True +SENDGRID_ECHO_TO_STDOUT = True # Mails app config MAILS_ENABLED = True MAILS_NOTIFICATIONS_ADDRESS = 'notifications@oser-cs.fr' -MAILS_RAISE_EXCEPTIONS = True +MAILS_RAISE_EXCEPTIONS = False # Visits app config VISITS_TEAM_EMAIL = os.environ.get('VISITS_TEAM_EMAIL', diff --git a/profiles/admin.py b/profiles/admin.py index f7fc71a9d3b7986b4b743f0b447cbac778d6b89a..6aefa1a15a823939b603430a933abd9852706f31 100644 --- a/profiles/admin.py +++ b/profiles/admin.py @@ -50,4 +50,5 @@ class StudentAdmin(ProfileAdminMixin, admin.ModelAdmin,ExportCsvMixin): list_filter = (('school',MultiSelectFieldListFilter), 'year', 'registration__validated') class Meta: # noqa model = Student + ordering = ['updated_date'] actions = ["export_as_csv"] \ No newline at end of file diff --git a/profiles/migrations/0015_student_updated_date.py b/profiles/migrations/0015_student_updated_date.py new file mode 100644 index 0000000000000000000000000000000000000000..4e672b5fbb499a6886e6d4868f6ae9ccc47f8d35 --- /dev/null +++ b/profiles/migrations/0015_student_updated_date.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2021-01-15 17:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0014_auto_20201116_1118'), + ] + + operations = [ + migrations.AddField( + model_name='student', + name='updated_date', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/profiles/models.py b/profiles/models.py index 20952a31436e83bbaeed223aad3fbd091638b319..4c9d912bbe6d1fb16a44290e0d73de85c898c5d1 100644 --- a/profiles/models.py +++ b/profiles/models.py @@ -38,6 +38,8 @@ class Student(ProfileMixin, models.Model): detail_view_name = 'api:student-detail' + updated_date = models.DateTimeField(auto_now=True) + user = models.OneToOneField( 'users.User', on_delete=models.CASCADE, diff --git a/projects/MultiSelectFieldListFilter.py b/projects/MultiSelectFieldListFilter.py new file mode 100644 index 0000000000000000000000000000000000000000..28bbb89e6eae617f147881cdc9e5a5bce7a5a420 --- /dev/null +++ b/projects/MultiSelectFieldListFilter.py @@ -0,0 +1,68 @@ +from django.contrib import admin +from django.contrib.admin.utils import reverse_field_path +from django.utils.translation import gettext_lazy as _ + + +class MultiSelectFieldListFilter(admin.FieldListFilter): + def __init__(self, field, request, params, model, model_admin, field_path): + self.lookup_kwarg = field_path + '__in' + self.lookup_kwarg_isnull = field_path + '__isnull' + + super().__init__(field, request, params, model, model_admin, field_path) + + self.lookup_val = self.used_parameters.get(self.lookup_kwarg, []) + if len(self.lookup_val) == 1 and self.lookup_val[0] == '': + self.lookup_val = [] + self.lookup_val_isnull = self.used_parameters.get( + self.lookup_kwarg_isnull) + + self.empty_value_display = model_admin.get_empty_value_display() + parent_model, reverse_path = reverse_field_path(model, field_path) + # Obey parent ModelAdmin queryset when deciding which options to show + if model == parent_model: + queryset = model_admin.get_queryset(request) + else: + queryset = parent_model._default_manager.all() + self.lookup_choices = queryset.distinct().order_by( + field.name).values_list(field.name, flat=True) + + def expected_parameters(self): + return [self.lookup_kwarg, self.lookup_kwarg_isnull] + + def choices(self, changelist): + yield { + 'selected': not self.lookup_val and self.lookup_val_isnull is None, + 'query_string': changelist.get_query_string(remove=[self.lookup_kwarg, self.lookup_kwarg_isnull]), + 'display': _('All'), + } + include_none = False + for val in self.lookup_choices: + if val is None: + include_none = True + continue + val = str(val) + + if val in self.lookup_val: + values = [v for v in self.lookup_val if v != val] + else: + values = self.lookup_val + [val] + + if values: + yield { + 'selected': val in self.lookup_val, + 'query_string': changelist.get_query_string({self.lookup_kwarg: ','.join(values)}, [self.lookup_kwarg_isnull]), + 'display': val, + } + else: + yield { + 'selected': val in self.lookup_val, + 'query_string': changelist.get_query_string(remove=[self.lookup_kwarg]), + 'display': val, + } + + if include_none: + yield { + 'selected': bool(self.lookup_val_isnull), + 'query_string': changelist.get_query_string({self.lookup_kwarg_isnull: 'True'}, [self.lookup_kwarg]), + 'display': self.empty_value_display, + } diff --git a/projects/admin.py b/projects/admin.py index 5a65a46fdfe9f65c4e90bd4f5293542db2ec39bb..e43c198933a4a26170dc30281e0b5bcfc49817f1 100644 --- a/projects/admin.py +++ b/projects/admin.py @@ -5,6 +5,8 @@ from django.contrib import admin from dynamicforms.views import download_multiple_forms_entries from dynamicforms.models import Form from .models import Edition, Participation, Project, EditionForm +from django.contrib.admin import SimpleListFilter +from profiles.models import Student @admin.register(Project) @@ -37,7 +39,26 @@ class OrganizersInline(admin.TabularInline): extra = 0 -@admin.register(Edition) +class SchoolFilter(admin.SimpleListFilter): + title = 'établissement' + parameter_name = 'profiles__school' + + def lookups(self, request, model_admin): + list_of_school = [] + query = Student.objects.values_list( + "school", flat=True).distinct() + for school in query: + list_of_school.append((school, school)) + return list_of_school + + def queryset(self, request, queryset): + if self.value(): + emails = Student.objects.filter( + school=self.value()).values_list("user__email", flat=True) + return queryset.filter(user__email__in=emails) + + +@ admin.register(Edition) class EditionAdmin(admin.ModelAdmin): """Admin panel for editions.""" @@ -73,7 +94,7 @@ class EditionAdmin(admin.ModelAdmin): num_cancelled.short_description = 'Annulés' -@admin.register(EditionForm) +@ admin.register(EditionForm) class EditionFormAdmin(admin.ModelAdmin): """Admin panel for edition forms.""" @@ -81,11 +102,12 @@ class EditionFormAdmin(admin.ModelAdmin): list_filter = ('edition', 'deadline',) -@admin.register(Participation) +@ admin.register(Participation) class ParticipationAdmin(admin.ModelAdmin): """Participation admin panel.""" list_display = ('user', 'edition', 'submitted', 'state') - list_filter = ('edition', 'submitted', 'state',) + list_filter = (SchoolFilter, + 'edition', 'submitted', 'state',) readonly_fields = ('submitted',) search_fields = ('user__first_name', 'user__last_name', 'user__email',) diff --git a/register/MultiSelectFieldListFilter.py b/register/MultiSelectFieldListFilter.py new file mode 100644 index 0000000000000000000000000000000000000000..28bbb89e6eae617f147881cdc9e5a5bce7a5a420 --- /dev/null +++ b/register/MultiSelectFieldListFilter.py @@ -0,0 +1,68 @@ +from django.contrib import admin +from django.contrib.admin.utils import reverse_field_path +from django.utils.translation import gettext_lazy as _ + + +class MultiSelectFieldListFilter(admin.FieldListFilter): + def __init__(self, field, request, params, model, model_admin, field_path): + self.lookup_kwarg = field_path + '__in' + self.lookup_kwarg_isnull = field_path + '__isnull' + + super().__init__(field, request, params, model, model_admin, field_path) + + self.lookup_val = self.used_parameters.get(self.lookup_kwarg, []) + if len(self.lookup_val) == 1 and self.lookup_val[0] == '': + self.lookup_val = [] + self.lookup_val_isnull = self.used_parameters.get( + self.lookup_kwarg_isnull) + + self.empty_value_display = model_admin.get_empty_value_display() + parent_model, reverse_path = reverse_field_path(model, field_path) + # Obey parent ModelAdmin queryset when deciding which options to show + if model == parent_model: + queryset = model_admin.get_queryset(request) + else: + queryset = parent_model._default_manager.all() + self.lookup_choices = queryset.distinct().order_by( + field.name).values_list(field.name, flat=True) + + def expected_parameters(self): + return [self.lookup_kwarg, self.lookup_kwarg_isnull] + + def choices(self, changelist): + yield { + 'selected': not self.lookup_val and self.lookup_val_isnull is None, + 'query_string': changelist.get_query_string(remove=[self.lookup_kwarg, self.lookup_kwarg_isnull]), + 'display': _('All'), + } + include_none = False + for val in self.lookup_choices: + if val is None: + include_none = True + continue + val = str(val) + + if val in self.lookup_val: + values = [v for v in self.lookup_val if v != val] + else: + values = self.lookup_val + [val] + + if values: + yield { + 'selected': val in self.lookup_val, + 'query_string': changelist.get_query_string({self.lookup_kwarg: ','.join(values)}, [self.lookup_kwarg_isnull]), + 'display': val, + } + else: + yield { + 'selected': val in self.lookup_val, + 'query_string': changelist.get_query_string(remove=[self.lookup_kwarg]), + 'display': val, + } + + if include_none: + yield { + 'selected': bool(self.lookup_val_isnull), + 'query_string': changelist.get_query_string({self.lookup_kwarg_isnull: 'True'}, [self.lookup_kwarg]), + 'display': self.empty_value_display, + } diff --git a/register/admin.py b/register/admin.py index 8273b9e29e0f9b2aac75a2601e2104e3737ecd3b..482cba4ff89eaf2663cc4fcad5925d681873ffca 100644 --- a/register/admin.py +++ b/register/admin.py @@ -2,8 +2,45 @@ from django.contrib import admin from .models import Registration +from profiles.models import Student -# Register your models here. + +class SchoolFilter(admin.SimpleListFilter): + title = 'établissement' + parameter_name = 'profiles__school' + + def lookups(self, request, model_admin): + list_of_school = [] + query = Student.objects.values_list( + "school", flat=True).distinct() + for school in query: + list_of_school.append((school, school)) + return list_of_school + + def queryset(self, request, queryset): + if self.value(): + emails = Student.objects.filter( + school=self.value()).values_list("user__email", flat=True) + return queryset.filter(email__in=emails) + + +class SchoolFilter(admin.SimpleListFilter): + title = 'établissement' + parameter_name = 'profiles__school' + + def lookups(self, request, model_admin): + list_of_school = [] + query = Student.objects.values_list( + "school", flat=True).distinct() + for school in query: + list_of_school.append((school, school)) + return list_of_school + + def queryset(self, request, queryset): + if self.value(): + emails = Student.objects.filter( + school=self.value()).values_list("user__email", flat=True) + return queryset.filter(email__in=emails) @admin.register(Registration) @@ -12,4 +49,5 @@ class RegistrationAdmin(admin.ModelAdmin): list_display = ('last_name', 'first_name', 'submitted') readonly_fields = ('submitted',) - list_filter = ('submitted', 'validated') + list_filter = (SchoolFilter, + 'submitted', 'validated') diff --git a/visits/admin.py b/visits/admin.py index 66e542ab6057d667f140b810abd71f14a24dbc03..7d49e11c710c84f56700e27fbdc1cb07e30b476b 100644 --- a/visits/admin.py +++ b/visits/admin.py @@ -9,10 +9,31 @@ from django.http import HttpResponse import csv from .models import Participation, Place, Visit from profiles.models import Student +from users.models import User +import codecs # Register your models here. +class SchoolFilter(admin.SimpleListFilter): + title = 'établissement' + parameter_name = 'profiles__school' + + def lookups(self, request, model_admin): + list_of_school = [] + query = Student.objects.values_list( + "school", flat=True).distinct() + for school in query: + list_of_school.append((school, school)) + return list_of_school + + def queryset(self, request, queryset): + if self.value(): + emails = Student.objects.filter( + school=self.value()).values_list("user__email", flat=True) + return queryset.filter(user__email__in=emails) + + class RegistrationsOpenFilter(admin.SimpleListFilter): """Custom filter to filter visits by their registration openness. @@ -42,6 +63,7 @@ class RegistrationsOpenFilter(admin.SimpleListFilter): class VisitForm(forms.ModelForm): """Custom admin form for Visit.""" + class Meta: # noqa model = Visit fields = '__all__' @@ -74,12 +96,29 @@ class VisitForm(forms.ModelForm): self.add_error('end_time', error) -class ParticipationInline(admin.StackedInline): +class ParticipationInline(admin.TabularInline): """Inline for Participation.""" - + # template = "visits/visit_tabular.md" + actions = ["export_as_csv"] model = Visit.participants.through extra = 0 + fields = ('name', 'school', 'user', 'submitted', 'present') + readonly_fields = ('name', 'school', 'user', 'submitted') + def school(self, participation: Participation): + """Return a link to the participation's user's school.""" + school = Student.objects.get(user = participation.user).school + return school + school.short_description = "Établissement" + + def name(self, participation: Participation): + """Returns the participation's user's name""" + return participation.user.first_name + " " + participation.user.last_name + name.short_description = "Nom" + + + class Media: + css = { "all" : ("css/hide_admin_original.css",) } def accept_selected_participations(modeladmin, request, queryset): """Accept selected participations in list view.""" @@ -115,8 +154,8 @@ reject_selected_participations.short_description = ( class ParticipationAdmin(admin.ModelAdmin): """Admin panel for visit participations.""" - list_display = ('submitted', 'visit', 'user_link', 'school', 'accepted', 'present') - list_filter = ('submitted', 'accepted', 'present') + list_display = ('submitted', 'visit', 'user_link', 'accepted', 'present') + list_filter = (SchoolFilter, 'submitted', 'accepted', 'present') actions = [accept_selected_participations, reject_selected_participations] def user_link(self, participation: Participation): @@ -140,17 +179,30 @@ class ParticipationAdmin(admin.ModelAdmin): def export_as_csv(self, request, queryset): meta = self.model._meta field_names = [field.name for field in meta.fields] - response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename={}.csv'.format( meta) - writer = csv.writer(response) - writer.writerow(field_names) + response.write(codecs.BOM_UTF8) # force response to be UTF-8 + writer = csv.writer(response, delimiter=';') + + writer.writerow(['first_name', 'last_name', 'school', + 'phone_number', 'scholarship'] + field_names) + + list_email = queryset.values_list("user__email", flat=True) + nb_user = 0 for obj in queryset: - row = writer.writerow([getattr(obj, field) - for field in field_names]) + + name = User.objects.filter( + email=str(list_email[nb_user])).values('first_name', 'last_name', 'phone_number') + school = Student.objects.filter( + user__email=str(list_email[nb_user])).values('school', 'scholarship') + + row = writer.writerow([name[0]['first_name'], name[0]['last_name'], school[0]['school'], name[0]['phone_number'], school[0]['scholarship']] + [getattr(obj, field) + for field in field_names]) + nb_user += 1 return response - export_as_csv.short_description = "Exporter au format CSV" + + export_as_csv.short_description = "Exporter sélection (en .csv)" @@ -187,6 +239,7 @@ class VisitAdmin(admin.ModelAdmin): return obj.participants.count() num_participants.short_description = 'Participants' + @admin.register(Place) class PlaceAdmin(admin.ModelAdmin): """Admin panel for places.""" diff --git a/visits/templates/visits/visit_tabular.md b/visits/templates/visits/visit_tabular.md new file mode 100644 index 0000000000000000000000000000000000000000..f96903bb78ded1b7140e407cb9691f54716a9821 --- /dev/null +++ b/visits/templates/visits/visit_tabular.md @@ -0,0 +1,79 @@ +{% load i18n admin_urls static admin_modify %} +<div class="js-inline-admin-formset inline-group" id="{{ inline_admin_formset.formset.prefix }}-group" + data-inline-type="tabular" + data-inline-formset="{{ inline_admin_formset.inline_formset_data }}"> + <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}"> +{{ inline_admin_formset.formset.management_form }} +<fieldset class="module {{ inline_admin_formset.classes }}"> + {% if inline_admin_formset.formset.max_num == 1 %} + <h2>{{ inline_admin_formset.opts.verbose_name|capfirst }}</h2> + {% else %} + <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2> + {% endif %} + {{ inline_admin_formset.formset.non_form_errors }} + <table> + <thead><tr> + <th class="original"></th> + {% for field in inline_admin_formset.fields %} + {% if not field.widget.is_hidden %} + <th class="column-{{ field.name }}{% if field.required %} required{% endif %}">{{ field.label|capfirst }} + {% if field.help_text %}<img src="{% static "admin/img/icon-unknown.svg" %}" class="help help-tooltip" width="10" height="10" alt="({{ field.help_text|striptags }})" title="{{ field.help_text|striptags }}">{% endif %} + </th> + {% endif %} + {% endfor %} + {% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission %}<th>{% translate "Delete?" %}</th>{% endif %} + </tr></thead> + + <tbody> + {% for inline_admin_form in inline_admin_formset %} + {% if inline_admin_form.form.non_field_errors %} + <tr class="row-form-errors"><td colspan="{{ inline_admin_form|cell_count }}">{{ inline_admin_form.form.non_field_errors }}</td></tr> + {% endif %} + <tr class="form-row {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}{% if forloop.last and inline_admin_formset.has_add_permission %} empty-form{% endif %}" + id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}"> + <td class="original"> + {% if inline_admin_form.original or inline_admin_form.show_url %}<p> + {% if inline_admin_form.original %} + {{ inline_admin_form.original }} + {% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}<a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="{% if inline_admin_formset.has_change_permission %}inlinechangelink{% else %}inlineviewlink{% endif %}">{% if inline_admin_formset.has_change_permission %}{% translate "Change" %}{% else %}{% translate "View" %}{% endif %}</a>{% endif %} + {% endif %} + {% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% translate "View on site" %}</a>{% endif %} + </p>{% endif %} + {% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %} + {% if inline_admin_form.fk_field %}{{ inline_admin_form.fk_field.field }}{% endif %} + {% spaceless %} + {% for fieldset in inline_admin_form %} + {% for line in fieldset %} + {% for field in line %} + {% if not field.is_readonly and field.field.is_hidden %}{{ field.field }}{% endif %} + {% endfor %} + {% endfor %} + {% endfor %} + {% endspaceless %} + </td> + {% for fieldset in inline_admin_form %} + {% for line in fieldset %} + {% for field in line %} + {% if field.is_readonly or not field.field.is_hidden %} + <td{% if field.field.name %} class="field-{{ field.field.name }}"{% endif %}> + {% if field.is_readonly %} + <p>{{ field.contents }}</p> + {% else %} + {{ field.field.errors.as_ul }} + {{ field.field }} + {% endif %} + </td> + {% endif %} + {% endfor %} + {% endfor %} + {% endfor %} + {% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission %} + <td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td> + {% endif %} + </tr> + {% endfor %} + </tbody> + </table> +</fieldset> + </div> +</div> \ No newline at end of file