From d63c782e563f36f504c27bdfcba4837a5417d51c Mon Sep 17 00:00:00 2001 From: Dorian Lesbre <dorian.lesbre@gmail.com> Date: Sat, 6 Mar 2021 14:09:26 +0100 Subject: [PATCH] Moved shared mixin to new shared app --- accounts/admin.py | 4 +- accounts/forms.py | 122 +---------------------------------------- home/admin.py | 20 +------ interludes/settings.py | 2 + shared/__init__.py | 0 shared/admin.py | 19 +++++++ shared/apps.py | 5 ++ shared/forms.py | 122 +++++++++++++++++++++++++++++++++++++++++ shared/models.py | 3 + shared/tests.py | 3 + shared/views.py | 3 + 11 files changed, 163 insertions(+), 140 deletions(-) create mode 100644 shared/__init__.py create mode 100644 shared/admin.py create mode 100644 shared/apps.py create mode 100644 shared/forms.py create mode 100644 shared/models.py create mode 100644 shared/tests.py create mode 100644 shared/views.py diff --git a/accounts/admin.py b/accounts/admin.py index 0558e42..59d7ab8 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -2,12 +2,13 @@ from django.contrib import admin from django.contrib.auth.models import Group from accounts.models import EmailUser +from shared.admin import ExportCsvMixin # no need for groups - we only have regular users and superusers admin.site.unregister(Group) @admin.register(EmailUser) -class EmailUserAdmin(admin.ModelAdmin): +class EmailUserAdmin(admin.ModelAdmin, ExportCsvMixin): """option d'affichage des activités dans la vue django admin""" list_display = ("email", "last_name", "first_name", "is_superuser", "is_active", "email_confirmed",) list_filter = ("is_superuser","is_active", "email_confirmed",) @@ -18,3 +19,4 @@ class EmailUserAdmin(admin.ModelAdmin): ordering = ("last_name", "first_name") readonly_fields = ("date_joined", "last_login",) list_per_page = 200 + actions = ["export_as_csv"] diff --git a/accounts/forms.py b/accounts/forms.py index c38d990..055b098 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -4,6 +4,7 @@ from django.contrib.auth.forms import UserCreationForm from django.utils.safestring import mark_safe from accounts.models import EmailUser +from shared.forms import FormRenderMixin def password_criterions_html(): """Wraps password criterions into nice html used by other forms""" @@ -20,126 +21,6 @@ def password_criterions_html(): ) return mark_safe(criterions_html) -class FormRenderMixin: - """ A mixin that can be included in any form to make it render to html as we want - it on this website. - - The following class variables can be adjusted to tweak display: - - * `tooltip_helptexts`: a list of fields whose helptexts, if any, should be rendered - as a question mark's tooltip instead of being inlined. - * `field_groups`: if `None`, the fields will be rendered in order, as eg. `as_p` - would. - Else, can be set to any nested list structure, containing each field name exactly - once. The structure `[['a', 'b'], ['c']]` would then group together the fields a - and b, then group together the single field c. - - """ - - tooltip_helptexts = [] - field_groups = None - - class BadFieldGroups(Exception): - pass - - def as_html(self): - """ Render the form to html """ - - def get_field_treelike(): - def map_to_field(treelike): - if isinstance(treelike, str): - if treelike in self.fields: - return { - "field": self[treelike], - "tooltip": treelike in self.tooltip_helptexts, - } - raise self.BadFieldFroups - return list(map(map_to_field, treelike)) - - if self.field_groups is not None: - return map_to_field(self.field_groups) - else: - return [ - list( - map( - lambda field: { - "field": self[field], - "tooltip": field in self.tooltip_helptexts, - }, - self.fields, - ) - ) - ] - - def gen_html(treelike): - def gen_node(subtree): - if isinstance(subtree, list): - return '<div class="fieldgroup">\n{}</div>'.format( - gen_html(subtree) - ) - else: # Simple field - inline_helptext_html = ( - ( - ' <span class="helptext inline_helptext">' - "{inline_helptext}</span>\n" - ).format(inline_helptext=subtree["field"].help_text) - if subtree["field"].help_text and not subtree["tooltip"] - else "" - ) - tooltip_html = ( - ( - '<span class="tooltip" tabindex="0">\n' - '<i class="fa fa-question-circle" aria-hidden="true"></i>\n' - '<span class="tooltiptext">\n' - " {tooltiphtml}\n" - "</span>\n" - "</span>" - ).format(tooltiphtml=subtree["field"].help_text) - if subtree["field"].help_text and subtree["tooltip"] - else "" - ) - - field_classes = "formfield" - if subtree["field"].errors: - field_classes += " error_field" - - labelled_input_classes = "labelled_input" - if subtree["field"].field.widget.input_type in [ - "checkbox", - "radio", - ]: - labelled_input_classes += " checkbox_input" - - html = ( - '<div class="{field_classes}" id="formfield_{label_for}">\n' - ' <div class="{labelled_input_classes}">\n' - ' <div class="label_line">\n' - ' <label for="{label_for}">{label_text} :</label>\n{errors}' - " </div>\n" - " {field_html}\n" - ' <div class="help">{tooltip}{inline_helptext_html}</div>\n' - " </div>\n" - "</div>" - ).format( - field_classes=field_classes, - labelled_input_classes=labelled_input_classes, - errors=subtree["field"].errors or "", - label_for=subtree["field"].id_for_label, - label_text=subtree["field"].label, - tooltip=tooltip_html, - inline_helptext_html=inline_helptext_html, - field_html=subtree["field"], - ) - return html - - return "\n".join(map(gen_node, treelike)) - - fields_html = gen_html(get_field_treelike()) - with_errors = "{form_errors}\n{fields}\n".format( - form_errors=self.non_field_errors() if self.non_field_errors() else "", - fields=fields_html, - ) - return mark_safe(with_errors) class CreateAccountForm(FormRenderMixin, UserCreationForm): """Form used to register a new user""" @@ -147,6 +28,7 @@ class CreateAccountForm(FormRenderMixin, UserCreationForm): model = EmailUser fields = ('email', 'first_name', 'last_name', 'password1', 'password2',) + class UpdateAccountForm(FormRenderMixin, forms.ModelForm): """Form used to update name/email""" class Meta: diff --git a/home/admin.py b/home/admin.py index e9b0297..e8803c7 100644 --- a/home/admin.py +++ b/home/admin.py @@ -3,31 +3,13 @@ from django.contrib import admin from django.http import HttpResponse from home.models import InterludesActivity, InterludesParticipant, ActivityList +from shared.admin import ExportCsvMixin # Titre de la vue (tag <h1>) admin.site.site_header = "Administration site interludes" # Tag html <title> admin.site.site_title = "Admin Interludes" -class ExportCsvMixin: - """class abstraite pour permettre l'export CSV rapide d'un modele""" - def export_as_csv(self, request, queryset): - """renvoie un fichier CSV contenant l'information du 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) - for obj in queryset: - writer.writerow([getattr(obj, field) for field in field_names]) - - return response - - export_as_csv.short_description = "Exporter au format CSV" - @admin.register(InterludesActivity) class InterludesActivityAdmin(admin.ModelAdmin, ExportCsvMixin): diff --git a/interludes/settings.py b/interludes/settings.py index a70d8aa..ba3a5df 100644 --- a/interludes/settings.py +++ b/interludes/settings.py @@ -40,9 +40,11 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.sitemaps', + 'home.apps.HomeConfig', 'accounts.apps.AccountsConfig', 'site_settings.apps.SiteSettingsConfig', + 'shared.apps.SharedConfig' ] MIDDLEWARE = [ diff --git a/shared/__init__.py b/shared/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shared/admin.py b/shared/admin.py new file mode 100644 index 0000000..5d3431f --- /dev/null +++ b/shared/admin.py @@ -0,0 +1,19 @@ + +class ExportCsvMixin: + """class abstraite pour permettre l'export CSV rapide d'un modele""" + def export_as_csv(self, request, queryset): + """renvoie un fichier CSV contenant l'information du 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) + for obj in queryset: + writer.writerow([getattr(obj, field) for field in field_names]) + + return response + + export_as_csv.short_description = "Exporter au format CSV" diff --git a/shared/apps.py b/shared/apps.py new file mode 100644 index 0000000..46de263 --- /dev/null +++ b/shared/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SharedConfig(AppConfig): + name = 'shared' diff --git a/shared/forms.py b/shared/forms.py new file mode 100644 index 0000000..d3b5964 --- /dev/null +++ b/shared/forms.py @@ -0,0 +1,122 @@ + + +class FormRenderMixin: + """ A mixin that can be included in any form to make it render to html as we want + it on this website. + + The following class variables can be adjusted to tweak display: + + * `tooltip_helptexts`: a list of fields whose helptexts, if any, should be rendered + as a question mark's tooltip instead of being inlined. + * `field_groups`: if `None`, the fields will be rendered in order, as eg. `as_p` + would. + Else, can be set to any nested list structure, containing each field name exactly + once. The structure `[['a', 'b'], ['c']]` would then group together the fields a + and b, then group together the single field c. + + """ + + tooltip_helptexts = [] + field_groups = None + + class BadFieldGroups(Exception): + pass + + def as_html(self): + """ Render the form to html """ + + def get_field_treelike(): + def map_to_field(treelike): + if isinstance(treelike, str): + if treelike in self.fields: + return { + "field": self[treelike], + "tooltip": treelike in self.tooltip_helptexts, + } + raise self.BadFieldFroups + return list(map(map_to_field, treelike)) + + if self.field_groups is not None: + return map_to_field(self.field_groups) + else: + return [ + list( + map( + lambda field: { + "field": self[field], + "tooltip": field in self.tooltip_helptexts, + }, + self.fields, + ) + ) + ] + + def gen_html(treelike): + def gen_node(subtree): + if isinstance(subtree, list): + return '<div class="fieldgroup">\n{}</div>'.format( + gen_html(subtree) + ) + else: # Simple field + inline_helptext_html = ( + ( + ' <span class="helptext inline_helptext">' + "{inline_helptext}</span>\n" + ).format(inline_helptext=subtree["field"].help_text) + if subtree["field"].help_text and not subtree["tooltip"] + else "" + ) + tooltip_html = ( + ( + '<span class="tooltip" tabindex="0">\n' + '<i class="fa fa-question-circle" aria-hidden="true"></i>\n' + '<span class="tooltiptext">\n' + " {tooltiphtml}\n" + "</span>\n" + "</span>" + ).format(tooltiphtml=subtree["field"].help_text) + if subtree["field"].help_text and subtree["tooltip"] + else "" + ) + + field_classes = "formfield" + if subtree["field"].errors: + field_classes += " error_field" + + labelled_input_classes = "labelled_input" + if subtree["field"].field.widget.input_type in [ + "checkbox", + "radio", + ]: + labelled_input_classes += " checkbox_input" + + html = ( + '<div class="{field_classes}" id="formfield_{label_for}">\n' + ' <div class="{labelled_input_classes}">\n' + ' <div class="label_line">\n' + ' <label for="{label_for}">{label_text} :</label>\n{errors}' + " </div>\n" + " {field_html}\n" + ' <div class="help">{tooltip}{inline_helptext_html}</div>\n' + " </div>\n" + "</div>" + ).format( + field_classes=field_classes, + labelled_input_classes=labelled_input_classes, + errors=subtree["field"].errors or "", + label_for=subtree["field"].id_for_label, + label_text=subtree["field"].label, + tooltip=tooltip_html, + inline_helptext_html=inline_helptext_html, + field_html=subtree["field"], + ) + return html + + return "\n".join(map(gen_node, treelike)) + + fields_html = gen_html(get_field_treelike()) + with_errors = "{form_errors}\n{fields}\n".format( + form_errors=self.non_field_errors() if self.non_field_errors() else "", + fields=fields_html, + ) + return mark_safe(with_errors) diff --git a/shared/models.py b/shared/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/shared/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/shared/tests.py b/shared/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/shared/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/shared/views.py b/shared/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/shared/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. -- GitLab