From 14283adbd53b0daa5212f1dd15b6cc58a04227bd Mon Sep 17 00:00:00 2001 From: ThomasBidot <77505438+ThomasBidot@users.noreply.github.com> Date: Fri, 16 Apr 2021 18:37:32 +0200 Subject: [PATCH] Dev-> Master (ordering students by updated_date, school filter, export_csv for visits) (#34) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Password reset feature (#8) * Add Django Rest auth module * Try to make the send reset password email work * Modified template mail for reset * Add Django Rest auth module * Try to make the send reset password email work * Modified template mail for reset * test * Added utf-8 support to exported csv and switched delimiter from , to ; in admin interface * Disabled emails while in dev * Added multi selection filter in admin * Fixed mail settings * Added year field to Tutor serializer * Fixed year updated before registration form filled * commit for automatic deploy * Testing CI * Added filtering in admin for registration validation * Added filter to student admin * Added school info * Fixed typo in filters * Sorties (#30) * Custom participation_inline css * Ordered student profiles by last modification date in admin * Ajouté infos sorties * Sortie thomas (#32) * Updated student admin filters (#28) * Password reset feature (#8) * Add Django Rest auth module * Try to make the send reset password email work * Modified template mail for reset * Add Django Rest auth module * Try to make the send reset password email work * Modified template mail for reset * test * Added utf-8 support to exported csv and switched delimiter from , to ; in admin interface * Disabled emails while in dev * Added multi selection filter in admin * Fixed mail settings * Added year field to Tutor serializer * Fixed year updated before registration form filled * commit for automatic deploy * Testing CI * Added filtering in admin for registration validation * Added filter to student admin Co-authored-by: chiahetcho <44137047+chiahetcho@users.noreply.github.com> Co-authored-by: florimondmanca <florimond.manca@gmail.com> Co-authored-by: Arthur Guédon <arthur.guedon@student-cs.fr> Co-authored-by: Arthur Guédon <60623551+arthurgdn@users.noreply.github.com> * Ajout filtre sur register * Ajout filtre participation * Ajout de 3 filtres par etablissements * Ajout 3 filtres pour etablissement V2 Co-authored-by: Seon82 <46298009+Seon82@users.noreply.github.com> Co-authored-by: chiahetcho <44137047+chiahetcho@users.noreply.github.com> Co-authored-by: florimondmanca <florimond.manca@gmail.com> Co-authored-by: Arthur Guédon <arthur.guedon@student-cs.fr> Co-authored-by: Arthur Guédon <60623551+arthurgdn@users.noreply.github.com> Co-authored-by: Bidot-Naude Thomas <thomas.bidotnaude@student-cs.fr> Co-authored-by: Witeden <58004019+Witeden@users.noreply.github.com> * Sortie thomas (export CSV sorties) (#33) * Updated student admin filters (#28) * Password reset feature (#8) * Add Django Rest auth module * Try to make the send reset password email work * Modified template mail for reset * Add Django Rest auth module * Try to make the send reset password email work * Modified template mail for reset * test * Added utf-8 support to exported csv and switched delimiter from , to ; in admin interface * Disabled emails while in dev * Added multi selection filter in admin * Fixed mail settings * Added year field to Tutor serializer * Fixed year updated before registration form filled * commit for automatic deploy * Testing CI * Added filtering in admin for registration validation * Added filter to student admin Co-authored-by: chiahetcho <44137047+chiahetcho@users.noreply.github.com> Co-authored-by: florimondmanca <florimond.manca@gmail.com> Co-authored-by: Arthur Guédon <arthur.guedon@student-cs.fr> Co-authored-by: Arthur Guédon <60623551+arthurgdn@users.noreply.github.com> * Ajout filtre sur register * Ajout filtre participation * Ajout de 3 filtres par etablissements * Ajout 3 filtres pour etablissement V2 * Ajout export CSV sorties Co-authored-by: Seon82 <46298009+Seon82@users.noreply.github.com> Co-authored-by: chiahetcho <44137047+chiahetcho@users.noreply.github.com> Co-authored-by: florimondmanca <florimond.manca@gmail.com> Co-authored-by: Arthur Guédon <arthur.guedon@student-cs.fr> Co-authored-by: Arthur Guédon <60623551+arthurgdn@users.noreply.github.com> Co-authored-by: Bidot-Naude Thomas <thomas.bidotnaude@student-cs.fr> Co-authored-by: Dylan Sechet <dylan.sechet82@gmail.com> Co-authored-by: Seon82 <46298009+Seon82@users.noreply.github.com> Co-authored-by: chiahetcho <44137047+chiahetcho@users.noreply.github.com> Co-authored-by: florimondmanca <florimond.manca@gmail.com> Co-authored-by: Arthur Guédon <arthur.guedon@student-cs.fr> Co-authored-by: Arthur Guédon <60623551+arthurgdn@users.noreply.github.com> Co-authored-by: Bidot-Naude Thomas <thomas.bidotnaude@student-cs.fr> Co-authored-by: Witeden <58004019+Witeden@users.noreply.github.com> * Update runtime.txt Changement version python * Ordering students by updated_date Co-authored-by: chiahetcho <44137047+chiahetcho@users.noreply.github.com> Co-authored-by: florimondmanca <florimond.manca@gmail.com> Co-authored-by: Dylan Sechet <dylan.sechet82@gmail.com> Co-authored-by: Arthur Guédon <arthur.guedon@student-cs.fr> Co-authored-by: Arthur Guédon <60623551+arthurgdn@users.noreply.github.com> Co-authored-by: Seon82 <46298009+Seon82@users.noreply.github.com> Co-authored-by: Bidot-Naude Thomas <thomas.bidotnaude@student-cs.fr> Co-authored-by: Witeden <58004019+Witeden@users.noreply.github.com> --- oser_backend/settings/common.py | 6 +- profiles/admin.py | 3 +- .../migrations/0015_student_updated_date.py | 18 ++++ profiles/models.py | 2 + projects/MultiSelectFieldListFilter.py | 68 +++++++++++++++ projects/admin.py | 30 ++++++- register/MultiSelectFieldListFilter.py | 68 +++++++++++++++ register/admin.py | 42 +++++++++- runtime.txt | 2 +- visits/admin.py | 82 ++++++++++++++++--- visits/templates/visits/visit_tabular.md | 79 ++++++++++++++++++ 11 files changed, 379 insertions(+), 21 deletions(-) create mode 100644 profiles/migrations/0015_student_updated_date.py create mode 100644 projects/MultiSelectFieldListFilter.py create mode 100644 register/MultiSelectFieldListFilter.py create mode 100644 visits/templates/visits/visit_tabular.md diff --git a/oser_backend/settings/common.py b/oser_backend/settings/common.py index 9579af4..84ccf82 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 f7fc71a..9c8efa2 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 - actions = ["export_as_csv"] \ No newline at end of file + ordering = ['-updated_date'] + actions = ["export_as_csv"] diff --git a/profiles/migrations/0015_student_updated_date.py b/profiles/migrations/0015_student_updated_date.py new file mode 100644 index 0000000..4e672b5 --- /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 20952a3..4c9d912 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 0000000..28bbb89 --- /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 5a65a46..e43c198 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 0000000..28bbb89 --- /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 8273b9e..482cba4 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/runtime.txt b/runtime.txt index 486fcce..0c89a2d 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.6.5 +python-3.6.13 diff --git a/visits/admin.py b/visits/admin.py index 0f1462e..7d49e11 100644 --- a/visits/admin.py +++ b/visits/admin.py @@ -8,10 +8,32 @@ from django.utils.safestring import mark_safe 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. @@ -41,6 +63,7 @@ class RegistrationsOpenFilter(admin.SimpleListFilter): class VisitForm(forms.ModelForm): """Custom admin form for Visit.""" + class Meta: # noqa model = Visit fields = '__all__' @@ -73,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,7 +155,7 @@ class ParticipationAdmin(admin.ModelAdmin): """Admin panel for visit participations.""" list_display = ('submitted', 'visit', 'user_link', 'accepted', 'present') - list_filter = ('submitted', 'accepted', 'present') + list_filter = (SchoolFilter, 'submitted', 'accepted', 'present') actions = [accept_selected_participations, reject_selected_participations] def user_link(self, participation: Participation): @@ -128,20 +168,42 @@ class ParticipationAdmin(admin.ModelAdmin): actions = ["export_as_csv"] + + 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 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)" + @admin.register(Visit.organizers.through) @@ -177,7 +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 0000000..f96903b --- /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 -- GitLab