diff --git a/README.rst b/README.rst index e5d0251f38d496da608ba3c99526b31678378caf..664d44feccd825af419d30650fa31a27e97234c2 100644 --- a/README.rst +++ b/README.rst @@ -1,38 +1,16 @@ -AlekSIS — All-libre extensible kit for school information systems -================================================================= +AlekSIS (School Information System) — Core (Core functionality and app framework) +================================================================================= -Warning -------- - -**This is an alpha version of AlekSIS, the free school information system. -The AlekSIS team is looking for schools who want to help shape the 2.0 -final release and supports interested schools in operating AlekSIS.** - -What AlekSIS is ----------------- - -`AlekSIS`_ is a web-based school information system (SIS) which can be used to -manage and/or publish organisational subjects of educational institutions. - -Formerly two separate projects (BiscuIT and SchoolApps), developed by -`Teckids e.V.`_ and a team of students at `Katharineum zu Lübeck`_, they -were merged into the AlekSIS project in 2020. +AlekSIS standard distribution +----------------------------- -AlekSIS is a platform based on Django, that provides central funstions -and data structures that can be used by apps that are developed and provided -seperately. The AlekSIS team also maintains a set of official apps which -make AlekSIS a fully-featured software solutions for the information -management needs of schools. +The AlekSIS standard distribution with information about all official apps +can be found on `EduGit`_. -By design, the platform can be used by schools to write their own apps for -specific needs they face, also in coding classes. Students are empowered to -create real-world applications that bring direct value to their environment. +Features +-------- -AlekSIS is part of the `schul-frei`_ project as a component in sustainable -educational networks. - -Core features --------------- +The AlekSIS-Core currently provides the following features: * For users: @@ -53,24 +31,6 @@ Core features * Authentication via LDAP * Automatic backup of database, static and media files -Official apps -------------- - -+--------------------------------------+---------------------------------------------------------------------------------------------+ -| App name | Purpose | -+======================================+=============================================================================================+ -| `AlekSIS-App-Chronos`_ | The Chronos app provides functionality for digital timetables. | -+--------------------------------------+---------------------------------------------------------------------------------------------+ -| `AlekSIS-App-DashboardFeeds`_ | The DashboardFeeds app provides functionality to add RSS or Atom feeds to dashboard | -+--------------------------------------+---------------------------------------------------------------------------------------------+ -| `AlekSIS-App-Hjelp`_ | The Hjelp app provides functionality for aiding users. | -+--------------------------------------+---------------------------------------------------------------------------------------------+ -| `AlekSIS-App-LDAP`_ | The LDAP app provides functionality to import users and groups from LDAP | -+--------------------------------------+---------------------------------------------------------------------------------------------+ -| `AlekSIS-App-Untis`_ | This app provides import and export functions to interact with Untis, a timetable software. | -+--------------------------------------+---------------------------------------------------------------------------------------------+ - - Licence ------- @@ -91,13 +51,6 @@ full licence text or on the `European Union Public Licence`_ website https://joinup.ec.europa.eu/collection/eupl/guidelines-users-and-developers (including all other official language versions). -.. _AlekSIS: https://aleksis.org/ -.. _Teckids e.V.: https://www.teckids.org/ -.. _Katharineum zu Lübeck: https://www.katharineum.de/ +.. _AlekSIS: https://edugit.org/AlekSIS/Official/AlekSIS .. _European Union Public Licence: https://eupl.eu/ -.. _schul-frei: https://schul-frei.org/ -.. _AlekSIS-App-Chronos: https://edugit.org/AlekSIS/official/AlekSIS-App-Chronos -.. _AlekSIS-App-DashboardFeeds: https://edugit.org/AlekSIS/official/AlekSIS-App-DashboardFeeds -.. _AlekSIS-App-Hjelp: https://edugit.org/AlekSIS/official/AlekSIS-App-Hjelp -.. _AlekSIS-App-LDAP: https://edugit.org/AlekSIS/official/AlekSIS-App-LDAP -.. _AlekSIS-App-Untis: https://edugit.org/AlekSIS/official/AlekSIS-App-Untis +.. _EduGit: https://edugit.org/AlekSIS/official/AlekSIS diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py index 9297980c7cc8bbd494f6124ee914f6d542b1863f..f4599d4d0e13ac02d01fda147331a3997d0f8dd0 100644 --- a/aleksis/core/forms.py +++ b/aleksis/core/forms.py @@ -10,7 +10,15 @@ from dynamic_preferences.forms import PreferenceForm from material import Fieldset, Layout, Row from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm -from .models import AdditionalField, Announcement, Group, GroupType, Person, SchoolTerm +from .models import ( + AdditionalField, + Announcement, + DashboardWidget, + Group, + GroupType, + Person, + SchoolTerm, +) from .registries import ( group_preferences_registry, person_preferences_registry, @@ -345,3 +353,20 @@ class SchoolTermForm(ExtensibleForm): class Meta: model = SchoolTerm exclude = [] + + +class DashboardWidgetOrderForm(ExtensibleForm): + pk = forms.ModelChoiceField( + queryset=DashboardWidget.objects.all(), + widget=forms.HiddenInput(attrs={"class": "pk-input"}), + ) + order = forms.IntegerField(initial=0, widget=forms.HiddenInput(attrs={"class": "order-input"})) + + class Meta: + model = DashboardWidget + fields = [] + + +DashboardWidgetOrderFormSet = forms.formset_factory( + form=DashboardWidgetOrderForm, max_num=0, extra=0 +) diff --git a/aleksis/core/locale/ar/LC_MESSAGES/django.po b/aleksis/core/locale/ar/LC_MESSAGES/django.po index 92ad8782664c5f8ec329b96d4054bdc057596d40..a3e781c6b389c7e3c6814fe9491358e6810b1b41 100644 --- a/aleksis/core/locale/ar/LC_MESSAGES/django.po +++ b/aleksis/core/locale/ar/LC_MESSAGES/django.po @@ -1506,8 +1506,8 @@ msgstr "" msgid "" "\n" " To start using a token generator, please use your\n" -" smartphone to scan the QR code below. For example, use Google\n" -" Authenticator. Then, enter the token generated by the app.\n" +" favourite two factor authentication (TOTP) app to scan the QR code below.\n" +" Then, enter the token generated by the app.\n" " " msgstr "" diff --git a/aleksis/core/locale/de_DE/LC_MESSAGES/django.po b/aleksis/core/locale/de_DE/LC_MESSAGES/django.po index e59806834f65d4acced6f3adea79b24ccf3b2f3f..6f6d8fc83f732fbd2abdedbb8f4793e2591c1ca1 100644 --- a/aleksis/core/locale/de_DE/LC_MESSAGES/django.po +++ b/aleksis/core/locale/de_DE/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: AlekSIS (School Information System) 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-08-02 16:29+0200\n" -"PO-Revision-Date: 2020-08-02 15:00+0000\n" +"PO-Revision-Date: 2020-12-19 12:57+0000\n" "Last-Translator: Jonathan Weth <teckids@jonathanweth.de>\n" "Language-Team: German <https://translate.edugit.org/projects/aleksis/aleksis/" "de/>\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.0.1\n" +"X-Generator: Weblate 4.3.2\n" #: filters.py:37 templates/core/base.html:77 templates/core/group/list.html:20 #: templates/core/person/list.html:24 templates/search/search.html:7 @@ -412,7 +412,7 @@ msgstr "Kann Kindgruppen zu Gruppen zuordnen" #: models.py:330 msgid "Long name" -msgstr "Langer Name" +msgstr "Langname" #: models.py:340 templates/core/group/full.html:65 msgid "Members" @@ -1655,14 +1655,14 @@ msgstr "" msgid "" "\n" " To start using a token generator, please use your\n" -" smartphone to scan the QR code below. For example, use Google\n" -" Authenticator. Then, enter the token generated by the app.\n" +" favourite two factor authentication (TOTP) app to scan the QR code below.\n" +" Then, enter the token generated by the app.\n" " " msgstr "" "\n" -" Um mit dem Codegenerator zu starten, benutzen Sie bitte Ihr Smartphone,\n" -"um diesen QR-Code zu scannen (z. B. den Google Authenticator). Dann geben Sie \n" -"den in der App angezeigten Code an:\n" +" Um mit dem Codegenerator zu starten, benutzen Sie bitte Ihre\n" +"App für Zwei-Faktor-Authentifizierung (TOTP), um diesen QR-Code zu scannen.\n" +"Dann geben Sie den in der App angezeigten Code an:\n" " " #: templates/two_factor/core/setup.html:34 diff --git a/aleksis/core/locale/fr/LC_MESSAGES/django.po b/aleksis/core/locale/fr/LC_MESSAGES/django.po index d800a36f8aa7db3f7279e2ddfc496e3a4f10ff8f..a8556727d98887123c05abb853a0bd46e829ead4 100644 --- a/aleksis/core/locale/fr/LC_MESSAGES/django.po +++ b/aleksis/core/locale/fr/LC_MESSAGES/django.po @@ -1558,8 +1558,8 @@ msgstr "" msgid "" "\n" " To start using a token generator, please use your\n" -" smartphone to scan the QR code below. For example, use Google\n" -" Authenticator. Then, enter the token generated by the app.\n" +" favourite two factor authentication (TOTP) app to scan the QR code below.\n" +" Then, enter the token generated by the app.\n" " " msgstr "" diff --git a/aleksis/core/locale/la/LC_MESSAGES/django.po b/aleksis/core/locale/la/LC_MESSAGES/django.po index 0fe692359d53218a555763890f363f7c9ed9e382..6fa38602c123109a48b5f34b07415b76a666f5a5 100644 --- a/aleksis/core/locale/la/LC_MESSAGES/django.po +++ b/aleksis/core/locale/la/LC_MESSAGES/django.po @@ -8,27 +8,26 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-08-02 16:29+0200\n" -"PO-Revision-Date: 2020-04-27 13:03+0000\n" +"PO-Revision-Date: 2020-12-19 12:57+0000\n" "Last-Translator: Julian <leuckerj@gmail.com>\n" -"Language-Team: Latin <https://translate.edugit.org/projects/aleksis/aleksis/la/>\n" +"Language-Team: Latin <https://translate.edugit.org/projects/aleksis/aleksis/" +"la/>\n" "Language: la\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.0.1\n" +"X-Generator: Weblate 4.3.2\n" #: filters.py:37 templates/core/base.html:77 templates/core/group/list.html:20 #: templates/core/person/list.html:24 templates/search/search.html:7 #: templates/search/search.html:22 msgid "Search" -msgstr "" +msgstr "Quaerere" #: filters.py:53 -#, fuzzy -#| msgid "Short name" msgid "Search by name" -msgstr "Breve nomen" +msgstr "Quaerere cum breve nomine" #: filters.py:65 #, fuzzy @@ -249,7 +248,7 @@ msgstr "" #: models.py:36 msgid "Date and time" -msgstr "" +msgstr "Dies et hora" #: models.py:37 msgid "Decimal number" @@ -1631,8 +1630,8 @@ msgstr "" msgid "" "\n" " To start using a token generator, please use your\n" -" smartphone to scan the QR code below. For example, use Google\n" -" Authenticator. Then, enter the token generated by the app.\n" +" favourite two factor authentication (TOTP) app to scan the QR code below.\n" +" Then, enter the token generated by the app.\n" " " msgstr "" diff --git a/aleksis/core/locale/nb_NO/LC_MESSAGES/django.po b/aleksis/core/locale/nb_NO/LC_MESSAGES/django.po index 6cfc5ee7e1a2e39ea35727fd66d5bd22d8e8b235..5be24e272186dd1985d749b948562bb13f222bc7 100644 --- a/aleksis/core/locale/nb_NO/LC_MESSAGES/django.po +++ b/aleksis/core/locale/nb_NO/LC_MESSAGES/django.po @@ -1505,8 +1505,8 @@ msgstr "" msgid "" "\n" " To start using a token generator, please use your\n" -" smartphone to scan the QR code below. For example, use Google\n" -" Authenticator. Then, enter the token generated by the app.\n" +" favourite two factor authentication (TOTP) app to scan the QR code below.\n" +" Then, enter the token generated by the app.\n" " " msgstr "" diff --git a/aleksis/core/locale/tr_TR/LC_MESSAGES/django.po b/aleksis/core/locale/tr_TR/LC_MESSAGES/django.po index d1efaccfa330d6f6989f9baf39bca163cd3cadf2..d0417b32bd8aa0df398290eacf5a23491641abd2 100644 --- a/aleksis/core/locale/tr_TR/LC_MESSAGES/django.po +++ b/aleksis/core/locale/tr_TR/LC_MESSAGES/django.po @@ -1505,8 +1505,8 @@ msgstr "" msgid "" "\n" " To start using a token generator, please use your\n" -" smartphone to scan the QR code below. For example, use Google\n" -" Authenticator. Then, enter the token generated by the app.\n" +" favourite two factor authentication (TOTP) app to scan the QR code below.\n" +" Then, enter the token generated by the app.\n" " " msgstr "" diff --git a/aleksis/core/menus.py b/aleksis/core/menus.py index a690f3b6a72db7e3d8b383cdfc9d39df56c0d4aa..fc9ba63604dabf57e1c4101fc8d7f5700548cc6c 100644 --- a/aleksis/core/menus.py +++ b/aleksis/core/menus.py @@ -93,6 +93,17 @@ MENUS = { ), ], }, + { + "name": _("Dashboard widgets"), + "url": "dashboard_widgets", + "icon": "dashboard", + "validators": [ + ( + "aleksis.core.util.predicates.permission_validator", + "core.view_dashboardwidget", + ), + ], + }, { "name": _("Data management"), "url": "data_management", diff --git a/aleksis/core/migrations/0006_dashboard_widget_size.py b/aleksis/core/migrations/0006_dashboard_widget_size.py new file mode 100644 index 0000000000000000000000000000000000000000..713ff1792ce87e4124025a34561cd602004ea8c7 --- /dev/null +++ b/aleksis/core/migrations/0006_dashboard_widget_size.py @@ -0,0 +1,36 @@ +# Generated by Django 3.1.4 on 2020-12-20 15:55 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_timestamped_activity_notification'), + ] + + operations = [ + migrations.AddField( + model_name='dashboardwidget', + name='size_l', + field=models.PositiveSmallIntegerField(default=6, help_text='> 992 px, 12 columns', validators=[django.core.validators.MaxValueValidator(12)], verbose_name='Size on desktop devices'), + ), + migrations.AddField( + model_name='dashboardwidget', + name='size_m', + field=models.PositiveSmallIntegerField(default=12, help_text='> 600 px, 12 columns', validators=[django.core.validators.MaxValueValidator(12)], verbose_name='Size on tablet devices'), + ), + migrations.AddField( + model_name='dashboardwidget', + name='size_s', + field=models.PositiveSmallIntegerField(default=12, help_text='<= 600 px, 12 columns', validators=[django.core.validators.MaxValueValidator(12)], verbose_name='Size on mobile devices'), + ), + migrations.AddField( + model_name='dashboardwidget', + name='size_xl', + field=models.PositiveSmallIntegerField(default=4, help_text='> 1200 px>, 12 columns', validators=[django.core.validators.MaxValueValidator(12)], verbose_name='Size on large desktop devices'), + ), + ] diff --git a/aleksis/core/migrations/0007_dashboard_widget_order.py b/aleksis/core/migrations/0007_dashboard_widget_order.py new file mode 100644 index 0000000000000000000000000000000000000000..9387f4ca7c0dbb3307d2a3681f091e4bf631f847 --- /dev/null +++ b/aleksis/core/migrations/0007_dashboard_widget_order.py @@ -0,0 +1,35 @@ +# Generated by Django 3.1.4 on 2020-12-21 13:38 + +import django.contrib.sites.managers +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('sites', '0002_alter_domain_unique'), + ('core', '0006_dashboard_widget_size'), + ] + + operations = [ + migrations.CreateModel( + name='DashboardWidgetOrder', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extended_data', models.JSONField(default=dict, editable=False)), + ('order', models.PositiveIntegerField(verbose_name='Order')), + ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.person', verbose_name='Person')), + ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')), + ('widget', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.dashboardwidget', verbose_name='Dashboard widget')), + ], + options={ + 'verbose_name': 'Dashboard widget order', + 'verbose_name_plural': 'Dashboard widget orders', + }, + managers=[ + ('objects', django.contrib.sites.managers.CurrentSiteManager()), + ], + ), + ] diff --git a/aleksis/core/models.py b/aleksis/core/models.py index ae13cb588e41550dc747d9aabd9558f207315938..f82ea02ce04e5e6919ca3a39ba899e5080548d82 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -9,6 +9,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site from django.core.exceptions import ValidationError +from django.core.validators import MaxValueValidator from django.db import models, transaction from django.db.models import QuerySet from django.forms.widgets import Media @@ -235,6 +236,12 @@ class Person(ExtensibleModel): years -= 1 return years + @property + def dashboard_widgets(self): + return [ + w.widget for w in DashboardWidgetOrder.objects.filter(person=self).order_by("order") + ] + def save(self, *args, **kwargs): super().save(*args, **kwargs) @@ -683,6 +690,31 @@ class DashboardWidget(PolymorphicModel, PureDjangoModel): title = models.CharField(max_length=150, verbose_name=_("Widget Title")) active = models.BooleanField(verbose_name=_("Activate Widget")) + size_s = models.PositiveSmallIntegerField( + verbose_name=_("Size on mobile devices"), + help_text=_("<= 600 px, 12 columns"), + validators=[MaxValueValidator(12)], + default=12, + ) + size_m = models.PositiveSmallIntegerField( + verbose_name=_("Size on tablet devices"), + help_text=_("> 600 px, 12 columns"), + validators=[MaxValueValidator(12)], + default=12, + ) + size_l = models.PositiveSmallIntegerField( + verbose_name=_("Size on desktop devices"), + help_text=_("> 992 px, 12 columns"), + validators=[MaxValueValidator(12)], + default=6, + ) + size_xl = models.PositiveSmallIntegerField( + verbose_name=_("Size on large desktop devices"), + help_text=_("> 1200 px>, 12 columns"), + validators=[MaxValueValidator(12)], + default=4, + ) + def get_context(self): """Get the context dictionary to pass to the widget template.""" raise NotImplementedError("A widget subclass needs to implement the get_context method.") @@ -703,6 +735,18 @@ class DashboardWidget(PolymorphicModel, PureDjangoModel): verbose_name_plural = _("Dashboard Widgets") +class DashboardWidgetOrder(ExtensibleModel): + widget = models.ForeignKey( + DashboardWidget, on_delete=models.CASCADE, verbose_name=_("Dashboard widget") + ) + person = models.ForeignKey(Person, on_delete=models.CASCADE, verbose_name=_("Person")) + order = models.PositiveIntegerField(verbose_name=_("Order")) + + class Meta: + verbose_name = _("Dashboard widget order") + verbose_name_plural = _("Dashboard widget orders") + + class CustomMenu(ExtensibleModel): """A custom menu to display in the footer.""" diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py index 1ad1099bd441844a5ad4a4baef269174aff1aed0..8ad48fbf18db6cac37270680792aa784a49eb77c 100644 --- a/aleksis/core/rules.py +++ b/aleksis/core/rules.py @@ -294,3 +294,15 @@ solve_data_problem_predicate = ( has_person & view_data_check_results_predicate & has_global_perm("core.solve_data_problem") ) rules.add_perm("core.solve_data_problem", solve_data_problem_predicate) + +view_dashboard_widget_predicate = has_person & has_global_perm("core.view_dashboardwidget") +rules.add_perm("core.view_dashboardwidget", view_dashboard_widget_predicate) + +create_dashboard_widget_predicate = has_person & has_global_perm("core.add_dashboardwidget") +rules.add_perm("core.create_dashboardwidget", create_dashboard_widget_predicate) + +edit_dashboard_widget_predicate = has_person & has_global_perm("core.change_dashboardwidget") +rules.add_perm("core.edit_dashboardwidget", edit_dashboard_widget_predicate) + +delete_dashboard_widget_predicate = has_person & has_global_perm("core.delete_dashboardwidget") +rules.add_perm("core.delete_dashboardwidget", delete_dashboard_widget_predicate) diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index cd92f28e4a3ef71bcb6220711db196452641d3cc..e8870dc656e05c566ab1e83131d7fc4c70e0b9d5 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -44,6 +44,23 @@ DEBUG_TOOLBAR_CONFIG = { "SHOW_TOOLBAR_CALLBACK": "aleksis.core.util.core_helpers.dt_show_toolbar", } +DEBUG_TOOLBAR_PANELS = [ + "debug_toolbar.panels.versions.VersionsPanel", + "debug_toolbar.panels.timer.TimerPanel", + "debug_toolbar.panels.settings.SettingsPanel", + "debug_toolbar.panels.headers.HeadersPanel", + "debug_toolbar.panels.request.RequestPanel", + "debug_toolbar.panels.sql.SQLPanel", + "debug_toolbar.panels.cache.CachePanel", + "debug_toolbar.panels.staticfiles.StaticFilesPanel", + "debug_toolbar.panels.templates.TemplatesPanel", + "debug_toolbar.panels.signals.SignalsPanel", + "debug_toolbar.panels.logging.LoggingPanel", + "debug_toolbar.panels.redirects.RedirectsPanel", + "debug_toolbar.panels.profiling.ProfilingPanel", +] + + ALLOWED_HOSTS = _settings.get("http.allowed_hosts", []) # Application definition @@ -99,6 +116,7 @@ INSTALLED_APPS = [ "colorfield", "django_bleach", "favicon", + "django_filters", ] merge_app_settings("INSTALLED_APPS", INSTALLED_APPS, True) @@ -183,6 +201,11 @@ if _settings.get("caching.memcached.enabled", False): "LOCATION": _settings.get("caching.memcached.address", "127.0.0.1:11211"), } } + INSTALLED_APPS.append("cachalot") + DEBUG_TOOLBAR_PANELS.append("cachalot.panels.CachalotPanel") + CACHALOT_TIMEOUT = _settings.get("caching.cachalot.timeout", None) + CACHALOT_DATABASES = set(["default"]) + SILENCED_SYSTEM_CHECKS.append("cachalot.W001") # Password validation # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators @@ -329,6 +352,8 @@ YARN_INSTALLED_APPS = [ "select2", "select2-materialize", "paper-css", + "jquery-sortablejs", + "sortablejs", ] merge_app_settings("YARN_INSTALLED_APPS", YARN_INSTALLED_APPS, True) @@ -352,6 +377,8 @@ ANY_JS = { "css_url": JS_URL + "/select2-materialize/select2-materialize.css", "js_url": JS_URL + "/select2-materialize/index.js", }, + "sortablejs": {"js_url": JS_URL + "/sortablejs/dist/sortable.umd.js"}, + "jquery-sortablejs": {"js_url": JS_URL + "/jquery-sortablejs/jquery-sortable.js"}, } merge_app_settings("ANY_JS", ANY_JS, True) diff --git a/aleksis/core/static/js/edit_dashboard.js b/aleksis/core/static/js/edit_dashboard.js new file mode 100644 index 0000000000000000000000000000000000000000..0cc90de60305497a682d219b121e77550d273d1d --- /dev/null +++ b/aleksis/core/static/js/edit_dashboard.js @@ -0,0 +1,22 @@ +function refreshOrder() { + $(".order-input").val(0); + $("#widgets > .col").each(function (index) { + const order = (index + 1) * 10; + let pk = $(this).attr("data-pk"); + let sel = $("#order-form input[value=" + pk + "].pk-input").next(); + sel.val(order); + }) +} + +$(document).ready(function () { + $('#not-used-widgets').sortable({ + group: 'widgets', + animation: 150, + onEnd: refreshOrder + }); + $('#widgets').sortable({ + group: 'widgets', + animation: 150, + onEnd: refreshOrder + }); +}); diff --git a/aleksis/core/static/js/serviceworker.js b/aleksis/core/static/js/serviceworker.js index 93d9797c78db9756cba6f1f6caa46fc8c14ae57b..818e27cb7d28e820431a4ac28ba6fdf0c422deb7 100644 --- a/aleksis/core/static/js/serviceworker.js +++ b/aleksis/core/static/js/serviceworker.js @@ -1,35 +1,9 @@ -// This is the AlekSIS service worker - -const CACHE = "aleksis-cache"; - -const precacheFiles = [ - '', -]; -const offlineFallbackPage = '/offline'; - -const avoidCachingPaths = [ - '/admin', - '/settings', - '/accounts/login' -]; // TODO: More paths are needed - -function pathComparer(requestUrl, pathRegEx) { - return requestUrl.match(new RegExp(pathRegEx)); -} +// This is the AlekSIS service worker -function comparePaths(requestUrl, pathsArray) { - if (requestUrl) { - for (let index = 0; index < pathsArray.length; index++) { - const pathRegEx = pathsArray[index]; - if (pathComparer(requestUrl, pathRegEx)) { - return true; - } - } - } +const CACHE = 'aleksis-cache'; - return false; -} +const offlineFallbackPage = 'offline/'; self.addEventListener("install", function (event) { console.log("[AlekSIS PWA] Install Event processing."); @@ -40,10 +14,7 @@ self.addEventListener("install", function (event) { event.waitUntil( caches.open(CACHE).then(function (cache) { console.log("[AlekSIS PWA] Caching pages during install."); - - return cache.addAll(precacheFiles).then(function () { - return cache.add(offlineFallbackPage); - }); + return cache.add(offlineFallbackPage); }) ); }); @@ -95,11 +66,11 @@ function fromCache(event) { } function updateCache(request, response) { - if (!comparePaths(request.url, avoidCachingPaths)) { + if (response.headers.get('cache-control') && response.headers.get('cache-control').includes('no-cache')) { + return Promise.resolve(); + } else { return caches.open(CACHE).then(function (cache) { return cache.put(request, response); }); } - - return Promise.resolve(); } diff --git a/aleksis/core/static/style.scss b/aleksis/core/static/style.scss index e2ea3bbd9953477469350e1c477556947bf8f0e2..81f443f75ab79c34121390bf03da9ab1e611dd5b 100644 --- a/aleksis/core/static/style.scss +++ b/aleksis/core/static/style.scss @@ -70,6 +70,10 @@ header, main, footer { } } +.materialize-circle { + @extend .circle; +} + /**********/ /* HEADER */ /**********/ @@ -622,3 +626,7 @@ main .alert p:first-child, main .alert div:first-child { overflow: visible; width: 100%; } + +.draggable { + cursor: grab; +} diff --git a/aleksis/core/tables.py b/aleksis/core/tables.py index f562b61a151487c5047ef2861255a00ef7a5f785..54d3f3e5c75e4c5ac0dca4b8ccfe868f70b7efb2 100644 --- a/aleksis/core/tables.py +++ b/aleksis/core/tables.py @@ -70,3 +70,24 @@ class GroupTypesTable(tables.Table): delete = tables.LinkColumn( "delete_group_type_by_id", args=[A("id")], verbose_name=_("Delete"), text=_("Delete") ) + + +class DashboardWidgetTable(tables.Table): + """Table to list dashboard widgets.""" + + class Meta: + attrs = {"class": "responsive-table highlight"} + + widget_name = tables.Column(accessor="pk") + title = tables.LinkColumn("edit_dashboard_widget", args=[A("id")]) + active = tables.BooleanColumn(yesno="check,cancel", attrs={"span": {"class": "material-icons"}}) + delete = tables.LinkColumn( + "delete_dashboard_widget", + args=[A("id")], + text=_("Delete"), + attrs={"a": {"class": "btn-flat waves-effect waves-red red-text"}}, + verbose_name=_("Actions"), + ) + + def render_widget_name(self, value, record): + return record._meta.verbose_name diff --git a/aleksis/core/templates/core/base.html b/aleksis/core/templates/core/base.html index 07db50c7fb3a29dc169543ae03c4f26dff1ec25f..2087829da24c157b7dc0a797da776d7bb1cc9a3b 100644 --- a/aleksis/core/templates/core/base.html +++ b/aleksis/core/templates/core/base.html @@ -170,6 +170,8 @@ {% include_js "materialize" %} +{% include_js "sortablejs" %} +{% include_js "jquery-sortablejs" %} <script type="text/javascript" src="{% static 'js/search.js' %}"></script> <script type="text/javascript" src="{% static 'js/main.js' %}"></script> </body> diff --git a/aleksis/core/templates/core/dashboard_widget/create.html b/aleksis/core/templates/core/dashboard_widget/create.html new file mode 100644 index 0000000000000000000000000000000000000000..cfaa296eefc7afb0abce56ce1802eeba2ef70a76 --- /dev/null +++ b/aleksis/core/templates/core/dashboard_widget/create.html @@ -0,0 +1,23 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} +{% load material_form i18n data_helpers %} + +{% block browser_title %} + {% verbose_name_object model as widget_title %} + {% blocktrans with widget=widget_title %}Create {{ widget }}{% endblocktrans %} +{% endblock %} +{% block page_title %} + {% verbose_name_object model as widget_title %} + {% blocktrans with widget=widget_title %}Create {{ widget }}{% endblocktrans %} +{% endblock %} + +{% block content %} + + <form method="post"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/partials/save_button.html" %} + </form> + +{% endblock %} diff --git a/aleksis/core/templates/core/dashboard_widget/edit.html b/aleksis/core/templates/core/dashboard_widget/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..64dfe0eed64c36257fed529475f355d0ee2e8fc0 --- /dev/null +++ b/aleksis/core/templates/core/dashboard_widget/edit.html @@ -0,0 +1,23 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} +{% load material_form i18n data_helpers %} + +{% block browser_title %} + {% verbose_name_object object as widget_title %} + {% blocktrans with widget=widget_title %}Edit {{ widget }}{% endblocktrans %} +{% endblock %} +{% block page_title %} + {% verbose_name_object object as widget_title %} + {% blocktrans with widget=widget_title %}Edit {{ widget }}{% endblocktrans %} +{% endblock %} + +{% block content %} + + <form method="post"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/partials/save_button.html" %} + </form> + +{% endblock %} diff --git a/aleksis/core/templates/core/dashboard_widget/list.html b/aleksis/core/templates/core/dashboard_widget/list.html new file mode 100644 index 0000000000000000000000000000000000000000..ab384e8620f7c6499adb3e663fdb98e4eb598b2c --- /dev/null +++ b/aleksis/core/templates/core/dashboard_widget/list.html @@ -0,0 +1,22 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} + +{% load i18n data_helpers %} +{% load render_table from django_tables2 %} + +{% block browser_title %}{% blocktrans %}Dashboard widgets{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}Dashboard widgets{% endblocktrans %}{% endblock %} + +{% block content %} + + {% for ct, model in widget_types %} + <a class="btn green waves-effect waves-light" href="{% url 'create_dashboard_widget' ct.app_label ct.model %}"> + <i class="material-icons left">add</i> + {% verbose_name_object model as widget_name %} + {% blocktrans with name=widget_name %}Create {{ name }}{% endblocktrans %} + </a> + {% endfor %} + + {% render_table table %} +{% endblock %} diff --git a/aleksis/core/templates/core/edit_dashboard.html b/aleksis/core/templates/core/edit_dashboard.html new file mode 100644 index 0000000000000000000000000000000000000000..a15f24bff8ebcca6a7f9b4ea081d9d8c008f3b0a --- /dev/null +++ b/aleksis/core/templates/core/edit_dashboard.html @@ -0,0 +1,41 @@ +{% extends 'core/base.html' %} +{% load i18n static dashboard any_js %} + +{% block browser_title %}{% blocktrans %}Edit dashboard{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}Edit dashboard{% endblocktrans %}{% endblock %} + +{% block content %} + <div class="alert primary"> + <p> + <i class="material-icons left">info</i> + On this page you can arrange your personal dashboard. You can drag any items from "Available widgets" to "Your + Dashboard" or change the order by moving the widgets. After you have finished, please don't forget to click on + "Save". + </p> + </div> + + <form action="" method="post" id="order-form"> + {% csrf_token %} + {{ formset.management_form }} + {% for form in formset %} + {{ form.as_p }} + {% endfor %} + {% include "core/partials/save_button.html" %} + </form> + + <h5>{% trans "Available widgets" %}</h5> + <div class="row card-panel grey lighten-3" id="not-used-widgets"> + {% for widget in not_used_widgets %} + {% include "core/partials/edit_dashboard_widget.html" %} + {% endfor %} + </div> + + <h5>{% trans "Your dashboard" %}</h5> + <div class="row card-panel grey lighten-3" id="widgets"> + {% for widget in widgets %} + {% include "core/partials/edit_dashboard_widget.html" %} + {% endfor %} + </div> + + <script src="{% static "js/edit_dashboard.js" %}"></script> +{% endblock %} diff --git a/aleksis/core/templates/core/index.html b/aleksis/core/templates/core/index.html index bcf0bf1b4180756d53f1519b8ee2371ee3e4e658..93b80ddefffaa8cf72db56a7ef503a694f0514f5 100644 --- a/aleksis/core/templates/core/index.html +++ b/aleksis/core/templates/core/index.html @@ -2,13 +2,21 @@ {% load i18n static dashboard %} {% block browser_title %}{% blocktrans %}Home{% endblocktrans %}{% endblock %} -{% block page_title %}{{ request.site.preferences.general__title }}{% endblock %} +{% block no_page_title %}{% endblock %} {% block extra_head %} {{ media }} {% endblock %} {% block content %} + <a class="btn-flat waves-effect waves-light right" href="{% url "edit_dashboard" %}"> + <i class="material-icons left">edit</i> + {% trans "Edit dashboard" %} + </a> + <h4> + {{ request.site.preferences.general__title }} + </h4> + {% if user.is_authenticated %} {% for notification in unread_notifications %} <div class="alert primary scale-transition"> @@ -31,9 +39,19 @@ <div class="row" id="live_load"> {% for widget in widgets %} - <div class="col s12 m12 l6 xl4"> + <div class="col s{{ widget.size_s }} m{{ widget.size_m }} l{{ widget.size_l }} xl{{ widget.size_xl }}"> {% include_widget widget %} </div> + {% empty %} + <div class="col s12 grey-text center"> + <i class="material-icons medium ">widgets</i> + <p class="flow-text"> + {% blocktrans %} + You haven't selected any dashboard widgets. Please click on "Edit dashboard" to add widgets to your + personal dashboard. + {% endblocktrans %} + </p> + </div> {% endfor %} </div> diff --git a/aleksis/core/templates/core/partials/edit_dashboard_widget.html b/aleksis/core/templates/core/partials/edit_dashboard_widget.html new file mode 100644 index 0000000000000000000000000000000000000000..b842c0937f05c4e90a4d7337871460130fa43012 --- /dev/null +++ b/aleksis/core/templates/core/partials/edit_dashboard_widget.html @@ -0,0 +1,9 @@ +<div class="col draggable s{{ widget.size_s }} m{{ widget.size_m }} l{{ widget.size_l }} xl{{ widget.size_xl }}" + data-pk="{{ widget.pk }}"> + <div class="card placeholder"> + <div class="card-content"> + <i class="material-icons left small">drag_handle</i> + <span class="card-title">{{ widget.title }}</span> + </div> + </div> +</div> diff --git a/aleksis/core/templates/core/pages/offline.html b/aleksis/core/templates/offline.html similarity index 87% rename from aleksis/core/templates/core/pages/offline.html rename to aleksis/core/templates/offline.html index a6a70dc19f074e8c3f3ede50b5e9b5b80b5682f1..bd741268a8902ac6708f33e353337695c03158a3 100644 --- a/aleksis/core/templates/core/pages/offline.html +++ b/aleksis/core/templates/offline.html @@ -2,6 +2,8 @@ {% load i18n %} +{% block browser_title %}{% blocktrans %}Network error{% endblocktrans %}{% endblock %} + {% block content %} <h3><i class="material-icons left medium" style="font-size: 2.92rem;">signal_wifi_off</i>{% blocktrans %}No internet connection.{% endblocktrans %}</h3> diff --git a/aleksis/core/templates/two_factor/core/setup.html b/aleksis/core/templates/two_factor/core/setup.html index 2eb4ecb2828ac60104f284b2d75857e9b6be303b..8048403964a5bd4b7c9761d0189f18bb53524a22 100644 --- a/aleksis/core/templates/two_factor/core/setup.html +++ b/aleksis/core/templates/two_factor/core/setup.html @@ -22,8 +22,8 @@ <p> {% blocktrans %} To start using a token generator, please use your - smartphone to scan the QR code below. For example, use Google - Authenticator. Then, enter the token generated by the app. + favourite two factor authentication (TOTP) app to scan the QR code below. + Then, enter the token generated by the app. {% endblocktrans %} </p> <p> diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index 161730a310f43e015a472e5d1565400b6362ed37..930b3fbc8e8bec19bd32cadb6a6fd6bd7833f0f7 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -57,6 +57,7 @@ urlpatterns = [ path("group/<int:id_>/edit", views.edit_group, name="edit_group_by_id"), path("group/<int:id_>/delete", views.delete_group, name="delete_group_by_id"), path("", views.index, name="index"), + path("dashboard/edit/", views.EditDashboardView.as_view(), name="edit_dashboard"), path( "notifications/mark-read/<int:id_>", views.notification_mark_read, @@ -160,6 +161,22 @@ urlpatterns = [ views.SolveDataCheckView.as_view(), name="data_check_solve", ), + path("dashboard_widgets/", views.DashboardWidgetListView.as_view(), name="dashboard_widgets"), + path( + "dashboard_widgets/<int:pk>/edit/", + views.DashboardWidgetEditView.as_view(), + name="edit_dashboard_widget", + ), + path( + "dashboard_widgets/<int:pk>/delete/", + views.DashboardWidgetDeleteView.as_view(), + name="delete_dashboard_widget", + ), + path( + "dashboard_widgets/<str:app>/<str:model>/new/", + views.DashboardWidgetCreateView.as_view(), + name="create_dashboard_widget", + ), ] # Serve static files from STATIC_ROOT to make it work with runserver diff --git a/aleksis/core/util/manage.py b/aleksis/core/util/manage.py new file mode 100644 index 0000000000000000000000000000000000000000..64441f62dfdadbb589b973dcd65633e100bf647b --- /dev/null +++ b/aleksis/core/util/manage.py @@ -0,0 +1,12 @@ +"""Management utilities for an AlekSIS installation.""" + +import os +import sys + +from django.core.management import execute_from_command_line + + +def aleksis_cmd(): + """Run django-admin command with correct settings path.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "aleksis.core.settings") + execute_from_command_line(sys.argv) diff --git a/aleksis/core/views.py b/aleksis/core/views.py index 4f2bdb1c5f9032d57a6f5f546d77a35c67118922..fd4b163bd454d6008054780903376b12b7141629 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -1,14 +1,18 @@ -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Type from django.apps import apps from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied from django.core.paginator import Paginator from django.db.models import QuerySet +from django.forms.models import BaseModelForm, modelform_factory from django.http import HttpRequest, HttpResponse, HttpResponseNotFound from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse_lazy +from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ +from django.views.decorators.cache import never_cache from django.views.generic.base import View from django.views.generic.detail import DetailView from django.views.generic.list import ListView @@ -31,6 +35,7 @@ from .filters import GroupFilter, PersonFilter from .forms import ( AnnouncementForm, ChildGroupsForm, + DashboardWidgetOrderFormSet, EditAdditionalFieldForm, EditGroupForm, EditGroupTypeForm, @@ -41,11 +46,12 @@ from .forms import ( SchoolTermForm, SitePreferenceForm, ) -from .mixins import AdvancedCreateView, AdvancedEditView +from .mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView from .models import ( AdditionalField, Announcement, DashboardWidget, + DashboardWidgetOrder, DataCheckResult, Group, GroupType, @@ -60,6 +66,7 @@ from .registries import ( ) from .tables import ( AdditionalFieldsTable, + DashboardWidgetTable, GroupsTable, GroupTypesTable, PersonsTable, @@ -86,7 +93,7 @@ def index(request: HttpRequest) -> HttpResponse: announcements = Announcement.objects.at_time().for_person(request.user.person) context["announcements"] = announcements - widgets = DashboardWidget.objects.filter(active=True) + widgets = request.user.person.dashboard_widgets media = DashboardWidget.get_media(widgets) context["widgets"] = widgets @@ -95,11 +102,6 @@ def index(request: HttpRequest) -> HttpResponse: return render(request, "core/index.html", context) -def offline(request: HttpRequest) -> HttpResponse: - """Offline message for PWA.""" - return render(request, "core/pages/offline.html") - - def about(request: HttpRequest) -> HttpResponse: """About page listing all apps.""" context = {} @@ -120,6 +122,7 @@ class SchoolTermListView(SingleTableView, PermissionRequiredMixin): template_name = "core/school_term/list.html" +@method_decorator(never_cache, name="dispatch") class SchoolTermCreateView(AdvancedCreateView, PermissionRequiredMixin): """Create view for school terms.""" @@ -131,6 +134,7 @@ class SchoolTermCreateView(AdvancedCreateView, PermissionRequiredMixin): success_message = _("The school term has been created.") +@method_decorator(never_cache, name="dispatch") class SchoolTermEditView(AdvancedEditView, PermissionRequiredMixin): """Edit view for school terms.""" @@ -238,6 +242,7 @@ def groups(request: HttpRequest) -> HttpResponse: return render(request, "core/group/list.html", context) +@never_cache @permission_required("core.link_persons_accounts") def persons_accounts(request: HttpRequest) -> HttpResponse: """View allowing to batch-process linking of users to persons.""" @@ -258,6 +263,7 @@ def persons_accounts(request: HttpRequest) -> HttpResponse: return render(request, "core/person/accounts.html", context) +@never_cache @permission_required("core.assign_child_groups_to_groups") def groups_child_groups(request: HttpRequest) -> HttpResponse: """View for batch-processing assignment from child groups to groups.""" @@ -295,6 +301,7 @@ def groups_child_groups(request: HttpRequest) -> HttpResponse: return render(request, "core/group/child_groups.html", context) +@never_cache @permission_required("core.edit_person", fn=objectgetter_optional(Person)) def edit_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse: """Edit view for a single person, defaulting to logged-in person.""" @@ -333,6 +340,7 @@ def get_group_by_id(request: HttpRequest, id_: Optional[int] = None): return None +@never_cache @permission_required("core.edit_group", fn=objectgetter_optional(Group, None, False)) def edit_group(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse: """View to edit or create a group.""" @@ -426,6 +434,7 @@ def announcements(request: HttpRequest) -> HttpResponse: return render(request, "core/announcement/list.html", context) +@never_cache @permission_required( "core.create_or_edit_announcement", fn=objectgetter_optional(Announcement, None, False) ) @@ -493,6 +502,7 @@ class PermissionSearchView(PermissionRequiredMixin, SearchView): return render(self.request, self.template, context) +@never_cache def preferences( request: HttpRequest, registry_name: str = "person", @@ -578,6 +588,7 @@ def delete_group(request: HttpRequest, id_: int) -> HttpResponse: return redirect("groups") +@never_cache @permission_required( "core.change_additionalfield", fn=objectgetter_optional(AdditionalField, None, False) ) @@ -643,6 +654,7 @@ def delete_additional_field(request: HttpRequest, id_: int) -> HttpResponse: return redirect("additional_fields") +@never_cache @permission_required("core.change_grouptype", fn=objectgetter_optional(GroupType, None, False)) def edit_group_type(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse: """View to edit or create a group_type.""" @@ -752,3 +764,133 @@ class SolveDataCheckView(PermissionRequiredMixin, RevisionMixin, DetailView): return redirect("check_data") else: return HttpResponseNotFound() + + +class DashboardWidgetListView(SingleTableView, PermissionRequiredMixin): + """Table of all dashboard widgets.""" + + model = DashboardWidget + table_class = DashboardWidgetTable + permission_required = "core.view_dashboardwidget" + template_name = "core/dashboard_widget/list.html" + + def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: + context = super().get_context_data(**kwargs) + context["widget_types"] = [ + (ContentType.objects.get_for_model(m, False), m) + for m in DashboardWidget.__subclasses__() + ] + return context + + +@method_decorator(never_cache, name="dispatch") +class DashboardWidgetEditView(AdvancedEditView, PermissionRequiredMixin): + """Edit view for dashboard widgets.""" + + def get_form_class(self) -> Type[BaseModelForm]: + return modelform_factory(self.object.__class__, fields=self.fields) + + model = DashboardWidget + fields = "__all__" + permission_required = "core.edit_dashboardwidget" + template_name = "core/dashboard_widget/edit.html" + success_url = reverse_lazy("dashboard_widgets") + success_message = _("The dashboard widget has been saved.") + + +@method_decorator(never_cache, name="dispatch") +class DashboardWidgetCreateView(AdvancedCreateView, PermissionRequiredMixin): + """Create view for dashboard widgets.""" + + def get_model(self, request, *args, **kwargs): + app_label = kwargs.get("app") + model = kwargs.get("model") + ct = get_object_or_404(ContentType, app_label=app_label, model=model) + return ct.model_class() + + def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: + context = super().get_context_data(**kwargs) + context["model"] = self.model + return context + + def get(self, request, *args, **kwargs): + self.model = self.get_model(request, *args, **kwargs) + return super().get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + self.model = self.get_model(request, *args, **kwargs) + return super().post(request, *args, **kwargs) + + fields = "__all__" + permission_required = "core.add_dashboardwidget" + template_name = "core/dashboard_widget/create.html" + success_url = reverse_lazy("dashboard_widgets") + success_message = _("The dashboard widget has been created.") + + +class DashboardWidgetDeleteView(PermissionRequiredMixin, AdvancedDeleteView): + """Delete view for dashboard widgets.""" + + model = DashboardWidget + permission_required = "core.delete_dashboardwidget" + template_name = "core/pages/delete.html" + success_url = reverse_lazy("dashboard_widgets") + success_message = _("The dashboard widget has been deleted.") + + +class EditDashboardView(View): + """View for editing dashboard widget order.""" + + def get_context_data(self, request): + context = {} + + widgets = request.user.person.dashboard_widgets + not_used_widgets = DashboardWidget.objects.exclude(pk__in=[w.pk for w in widgets]) + context["widgets"] = widgets + context["not_used_widgets"] = not_used_widgets + + order = 10 + initial = [] + for widget in widgets: + initial.append({"pk": widget, "order": order}) + order += 10 + for widget in not_used_widgets: + initial.append({"pk": widget, "order": 0}) + + formset = DashboardWidgetOrderFormSet( + request.POST or None, initial=initial, prefix="widget_order" + ) + context["formset"] = formset + + return context + + def post(self, request): + context = self.get_context_data(request) + + if context["formset"].is_valid(): + added_objects = [] + for form in context["formset"]: + if not form.cleaned_data["order"]: + continue + + obj, created = DashboardWidgetOrder.objects.update_or_create( + widget=form.cleaned_data["pk"], + person=request.user.person, + defaults={"order": form.cleaned_data["order"]}, + ) + + added_objects.append(obj.pk) + + DashboardWidgetOrder.objects.filter(person=request.user.person).exclude( + pk__in=added_objects + ).delete() + + messages.success( + request, _("Your dashboard configuration has been saved successfully.") + ) + return redirect("index") + + def get(self, request): + context = self.get_context_data(request) + + return render(request, "core/edit_dashboard.html", context=context) diff --git a/poetry.lock b/poetry.lock index 2896275e72cc9c004709eb5a089be772087e9fea..be0e61cd822675c59e9c9fc318fd2f104c7f900a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -109,16 +109,16 @@ pytz = ">=2015.7" [[package]] name = "bandit" -version = "1.6.2" +version = "1.7.0" description = "Security oriented static analyser for python code." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.5" [package.dependencies] colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} GitPython = ">=1.0.1" -PyYAML = ">=3.13" +PyYAML = ">=5.3.1" six = ">=1.10.0" stevedore = ">=1.20.0" @@ -184,7 +184,7 @@ django = ["Django (>=2.2,<4.0)"] [[package]] name = "celery" -version = "5.0.2" +version = "5.0.5" description = "Distributed Task Queue." category = "main" optional = true @@ -192,8 +192,9 @@ python-versions = ">=3.6," [package.dependencies] billiard = ">=3.6.3.0,<4.0" -click = ">=7.0" +click = ">=7.0,<8.0" click-didyoumean = ">=0.0.3" +click-plugins = ">=1.1.1" click-repl = ">=0.1.6" Django = {version = ">=1.11", optional = true, markers = "extra == \"django\""} kombu = ">=5.0.0,<6.0" @@ -223,6 +224,7 @@ mongodb = ["pymongo[srv] (>=3.3.0)"] msgpack = ["msgpack"] pymemcache = ["python-memcached"] pyro = ["pyro4"] +pytest = ["pytest-celery"] redis = ["redis (>=3.2.0)"] s3 = ["boto3 (>=1.9.125)"] slmq = ["softlayer-messaging (>=1.0.3)"] @@ -260,7 +262,7 @@ websockets = ["channels"] [[package]] name = "certifi" -version = "2020.11.8" +version = "2020.12.5" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -268,11 +270,11 @@ python-versions = "*" [[package]] name = "chardet" -version = "3.0.4" +version = "4.0.0" description = "Universal encoding detector for Python 2 and 3" category = "main" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "click" @@ -293,6 +295,20 @@ python-versions = "*" [package.dependencies] click = "*" +[[package]] +name = "click-plugins" +version = "1.1.1" +description = "An extension module for click to enable registering CLI commands via setuptools entry-points." +category = "main" +optional = true +python-versions = "*" + +[package.dependencies] +click = ">=4.0" + +[package.extras] +dev = ["pytest (>=3.6)", "pytest-cov", "wheel", "coveralls"] + [[package]] name = "click-repl" version = "0.1.6" @@ -357,7 +373,7 @@ python-versions = "*" [[package]] name = "django" -version = "3.1.3" +version = "3.1.4" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false @@ -429,6 +445,17 @@ python-versions = "*" [package.dependencies] Django = ">=1.8" +[[package]] +name = "django-cachalot" +version = "2.3.3" +description = "Caches your Django ORM queries and automatically invalidates them." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Django = ">=2" + [[package]] name = "django-cache-memoize" version = "0.1.7" @@ -704,7 +731,7 @@ django = "*" [[package]] name = "django-model-utils" -version = "4.0.0" +version = "4.1.1" description = "Django model mixins and utilities" category = "main" optional = false @@ -825,7 +852,7 @@ management-command = ["django-compressor (>=2.4)"] [[package]] name = "django-select2" -version = "7.4.2" +version = "7.5.0" description = "Select2 option fields for Django" category = "main" optional = false @@ -887,7 +914,7 @@ six = ">=1" [[package]] name = "django-timezone-field" -version = "4.0" +version = "4.1.1" description = "A Django app providing database and form fields for pytz timezone objects." category = "main" optional = true @@ -897,6 +924,9 @@ python-versions = ">=3.5" django = ">=2.2" pytz = "*" +[package.extras] +rest_framework = ["djangorestframework (>=3.0.0)"] + [[package]] name = "django-two-factor-auth" version = "1.13" @@ -990,11 +1020,11 @@ yaml = ["ruamel.yaml"] [[package]] name = "faker" -version = "4.17.1" +version = "5.0.2" description = "Faker is a Python package that generates fake data for you." category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] python-dateutil = ">=2.4" @@ -1185,18 +1215,19 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "3.1.0" +version = "3.3.0" description = "Read metadata from Python packages" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "rst.linker"] -testing = ["packaging", "pep517", "unittest2", "importlib-resources (>=1.3)"] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -1325,7 +1356,7 @@ python-versions = "*" [[package]] name = "packaging" -version = "20.4" +version = "20.8" description = "Core utilities for Python packages" category = "main" optional = false @@ -1333,7 +1364,6 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pyparsing = ">=2.0.2" -six = "*" [[package]] name = "pathspec" @@ -1372,7 +1402,7 @@ scramp = "1.2.0" [[package]] name = "phonenumbers" -version = "8.12.13" +version = "8.12.15" description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." category = "main" optional = false @@ -1424,7 +1454,7 @@ wcwidth = "*" [[package]] name = "psutil" -version = "5.7.3" +version = "5.8.0" description = "Cross-platform lib for process and system monitoring in Python." category = "main" optional = false @@ -1443,7 +1473,7 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" [[package]] name = "py" -version = "1.9.0" +version = "1.10.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false @@ -1505,7 +1535,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" -version = "2.7.2" +version = "2.7.3" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false @@ -1534,25 +1564,24 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pytest" -version = "6.1.2" +version = "6.2.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=17.4.0" +attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<1.0" +pluggy = ">=0.12,<1.0.0a1" py = ">=1.8.2" toml = "*" [package.extras] -checkqa_mypy = ["mypy (==0.780)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] @@ -1714,7 +1743,7 @@ python-versions = "*" [[package]] name = "requests" -version = "2.25.0" +version = "2.25.1" description = "Python HTTP for Humans." category = "main" optional = false @@ -1722,7 +1751,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<4" +chardet = ">=3.0.2,<5" idna = ">=2.5,<3" urllib3 = ">=1.21.1,<1.27" @@ -1831,7 +1860,7 @@ python-versions = "*" [[package]] name = "spdx-license-list" -version = "0.5.1" +version = "0.5.2" description = "A simple tool/library for working with SPDX license definitions." category = "main" optional = false @@ -1972,7 +2001,7 @@ python-versions = ">=3.5" [[package]] name = "stevedore" -version = "3.2.2" +version = "3.3.0" description = "Manage dynamic plugins for Python applications" category = "dev" optional = false @@ -1992,7 +2021,7 @@ python-versions = "*" [[package]] name = "testfixtures" -version = "6.15.0" +version = "6.17.0" description = "A collection of helpers and mock objects for unit tests and doc tests." category = "dev" optional = false @@ -2047,7 +2076,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tqdm" -version = "4.54.0" +version = "4.54.1" description = "Fast, Extensible Progress Meter" category = "main" optional = false @@ -2058,7 +2087,7 @@ dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown", "wheel"] [[package]] name = "twilio" -version = "6.48.0" +version = "6.50.1" description = "Twilio API client and TwiML generator" category = "main" optional = false @@ -2082,7 +2111,7 @@ python-versions = "*" name = "typing-extensions" version = "3.7.4.3" description = "Backported and Experimental Type Hints for Python 3.5+" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -2153,7 +2182,7 @@ ldap = ["django-auth-ldap"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "796fb4bd6779730135a6d2c08f09646dd3f91cdb59dfe2210bdfabd5050f0d2f" +content-hash = "65a4b7b891965a330e7fec9ed64213579550aac1ac8295aa8c3022a4978d488c" [metadata.files] alabaster = [ @@ -2188,8 +2217,8 @@ babel = [ {file = "Babel-2.9.0.tar.gz", hash = "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05"}, ] bandit = [ - {file = "bandit-1.6.2-py2.py3-none-any.whl", hash = "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952"}, - {file = "bandit-1.6.2.tar.gz", hash = "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"}, + {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"}, + {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"}, ] billiard = [ {file = "billiard-3.6.3.0-py3-none-any.whl", hash = "sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede"}, @@ -2212,8 +2241,8 @@ calendarweek = [ {file = "calendarweek-0.4.7.tar.gz", hash = "sha256:7655d6a4c3b4f6a4e01aa7d23b49cd121db0399050e9c08cd8d1210155be25dd"}, ] celery = [ - {file = "celery-5.0.2-py3-none-any.whl", hash = "sha256:930c3acd55349d028c4e7104a7d377729cbcca19d9fce470c17172d9e7f9a8b6"}, - {file = "celery-5.0.2.tar.gz", hash = "sha256:012c814967fe89e3f5d2cf49df2dba3de5f29253a7f4f2270e8fce6b901b4ebf"}, + {file = "celery-5.0.5-py3-none-any.whl", hash = "sha256:5e8d364e058554e83bbb116e8377d90c79be254785f357cb2cec026e79febe13"}, + {file = "celery-5.0.5.tar.gz", hash = "sha256:f4efebe6f8629b0da2b8e529424de376494f5b7a743c321c8a2ddc2b1414921c"}, ] celery-haystack = [ {file = "celery-haystack-0.10.tar.gz", hash = "sha256:b6e2a3c70071bef0838ca1a7d9f14fae6c2ecf385704092e59b82147a1ee552e"}, @@ -2224,12 +2253,12 @@ celery-progress = [ {file = "celery_progress-0.0.14-py3-none-any.whl", hash = "sha256:6d95c01fe044dd5dbb1e2d507724f9ace70bde796bc6db51ba19c8a95e94da07"}, ] certifi = [ - {file = "certifi-2020.11.8-py2.py3-none-any.whl", hash = "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd"}, - {file = "certifi-2020.11.8.tar.gz", hash = "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"}, + {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, + {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, ] chardet = [ - {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, - {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, @@ -2238,13 +2267,16 @@ click = [ click-didyoumean = [ {file = "click-didyoumean-0.0.3.tar.gz", hash = "sha256:112229485c9704ff51362fe34b2d4f0b12fc71cc20f6d2b3afabed4b8bfa6aeb"}, ] +click-plugins = [ + {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, + {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, +] click-repl = [ {file = "click-repl-0.1.6.tar.gz", hash = "sha256:b9f29d52abc4d6059f8e276132a111ab8d94980afe6a5432b9d996544afa95d5"}, {file = "click_repl-0.1.6-py3-none-any.whl", hash = "sha256:9c4c3d022789cae912aad8a3f5e1d7c2cdd016ee1225b5212ad3e8691563cda5"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] colour = [ {file = "colour-0.1.5-py2.py3-none-any.whl", hash = "sha256:33f6db9d564fadc16e59921a56999b79571160ce09916303d35346dddc17978c"}, @@ -2294,8 +2326,8 @@ dj-database-url = [ {file = "dj_database_url-0.5.0-py2.py3-none-any.whl", hash = "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9"}, ] django = [ - {file = "Django-3.1.3-py3-none-any.whl", hash = "sha256:14a4b7cd77297fba516fc0d92444cc2e2e388aa9de32d7a68d4a83d58f5a4927"}, - {file = "Django-3.1.3.tar.gz", hash = "sha256:14b87775ffedab2ef6299b73343d1b4b41e5d4e2aa58c6581f114dbec01e3f8f"}, + {file = "Django-3.1.4-py3-none-any.whl", hash = "sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2"}, + {file = "Django-3.1.4.tar.gz", hash = "sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03"}, ] django-any-js = [ {file = "django-any-js-1.0.3.post0.tar.gz", hash = "sha256:1da88b44b861b0f54f6b8ea0eb4c7c4fa1a5772e9a4320532cd4e0871a4e23f7"}, @@ -2316,6 +2348,10 @@ django-bulk-update = [ {file = "django-bulk-update-2.2.0.tar.gz", hash = "sha256:5ab7ce8a65eac26d19143cc189c0f041d5c03b9d1b290ca240dc4f3d6aaeb337"}, {file = "django_bulk_update-2.2.0-py2.py3-none-any.whl", hash = "sha256:49a403392ae05ea872494d74fb3dfa3515f8df5c07cc277c3dc94724c0ee6985"}, ] +django-cachalot = [ + {file = "django-cachalot-2.3.3.tar.gz", hash = "sha256:ba3a6cabf834139196179c4f6d77409ae9170267ee8ce40e27bbf6c3f6733b2b"}, + {file = "django_cachalot-2.3.3-py3-none-any.whl", hash = "sha256:55f94e94f7000f5f6bd92188d3d7535cfdef79f2e697e36daf69cba8f435e156"}, +] django-cache-memoize = [ {file = "django-cache-memoize-0.1.7.tar.gz", hash = "sha256:5e96349b0159aec1eb79257199a1902ea3ed538231ce7b4fee12e563127ca657"}, {file = "django_cache_memoize-0.1.7-py2.py3-none-any.whl", hash = "sha256:bc7f53725558244af62197d0125732d7ec88ecc1281a3a2f37d77ae1a8c269d3"}, @@ -2411,8 +2447,8 @@ django-middleware-global-request = [ {file = "django-middleware-global-request-0.1.2.tar.gz", hash = "sha256:f6490759bc9f7dbde4001709554e29ca715daf847f2222914b4e47117dca9313"}, ] django-model-utils = [ - {file = "django-model-utils-4.0.0.tar.gz", hash = "sha256:adf09e5be15122a7f4e372cb5a6dd512bbf8d78a23a90770ad0983ee9d909061"}, - {file = "django_model_utils-4.0.0-py2.py3-none-any.whl", hash = "sha256:9cf882e5b604421b62dbe57ad2b18464dc9c8f963fc3f9831badccae66c1139c"}, + {file = "django-model-utils-4.1.1.tar.gz", hash = "sha256:eb5dd05ef7d7ce6bc79cae54ea7c4a221f6f81e2aad7722933aee66489e7264b"}, + {file = "django_model_utils-4.1.1-py3-none-any.whl", hash = "sha256:ef7c440024e797796a3811432abdd2be8b5225ae64ef346f8bfc6de7d8e5d73c"}, ] django-otp = [ {file = "django-otp-1.0.2.tar.gz", hash = "sha256:f523fb9dec420f28a29d3e2ad72ac06f64588956ed4f2b5b430d8e957ebb8287"}, @@ -2450,8 +2486,8 @@ django-sass-processor = [ {file = "django-sass-processor-0.8.2.tar.gz", hash = "sha256:9b46a12ca8bdcb397d46fbcc49e6a926ff9f76a93c5efeb23b495419fd01fc7a"}, ] django-select2 = [ - {file = "django-select2-7.4.2.tar.gz", hash = "sha256:9d3330fa0083a03fb69fceb5dcd2e78065cfd08e45c89d4fd727fce4673d3e08"}, - {file = "django_select2-7.4.2-py2.py3-none-any.whl", hash = "sha256:06531d563ce33c3133682ae2bb9e6d762103a863d0054ffef51bae8b4cfcca6c"}, + {file = "django-select2-7.5.0.tar.gz", hash = "sha256:df71dedba9a362041b65e3cd692cb8b4f9e1e17a19681c7b4e61f331868bae0c"}, + {file = "django_select2-7.5.0-py2.py3-none-any.whl", hash = "sha256:6662aa1c21d4839b8fff38e4c9d402ed3da81f7c5ef7f7e703c862255ba3b9ed"}, ] django-settings-context-processor = [ {file = "django-settings-context-processor-0.2.tar.gz", hash = "sha256:d37c853d69a3069f5abbf94c7f4f6fc0fac38bbd0524190cd5a250ba800e496a"}, @@ -2468,8 +2504,8 @@ django-templated-email = [ {file = "django-templated-email-2.3.0.tar.gz", hash = "sha256:536c4e5ae099eabfb9aab36087d4d7799948c654e73da55a744213d086d5bb33"}, ] django-timezone-field = [ - {file = "django-timezone-field-4.0.tar.gz", hash = "sha256:7e3620fe2211c2d372fad54db8f86ff884098d018d56fda4dca5e64929e05ffc"}, - {file = "django_timezone_field-4.0-py3-none-any.whl", hash = "sha256:758b7d41084e9ea2e89e59eb616e9b6326e6fbbf9d14b6ef062d624fe8cc6246"}, + {file = "django-timezone-field-4.1.1.tar.gz", hash = "sha256:b5b587aabed8db66eb3453691522164915c1aa1b326d8ddeadc8832a8580faeb"}, + {file = "django_timezone_field-4.1.1-py3-none-any.whl", hash = "sha256:068dc2c9b11c2230e126f511a515609d46f8cc49278b293e7536be07997fe892"}, ] django-two-factor-auth = [ {file = "django-two-factor-auth-1.13.tar.gz", hash = "sha256:24c2850a687c86800f4aa4131b7cebadf56f35be04ca359c4990578df1cc249a"}, @@ -2495,8 +2531,8 @@ dynaconf = [ {file = "dynaconf-3.1.2.tar.gz", hash = "sha256:9b34ab2f811a81755f5eb4beac77a69e1e0887528c7e37fc4bc83fed52dcf502"}, ] faker = [ - {file = "Faker-4.17.1-py3-none-any.whl", hash = "sha256:5398268e1d751ffdb3ed36b8a790ed98659200599b368eec38a02eed15bce997"}, - {file = "Faker-4.17.1.tar.gz", hash = "sha256:d4183b8f57316de3be27cd6c3b40e9f9343d27c95c96179f027316c58c2c239e"}, + {file = "Faker-5.0.2-py3-none-any.whl", hash = "sha256:5b17c95cfb013a22b062b8df18286f08ce4ea880f9948ec74295e5a42dbb2e44"}, + {file = "Faker-5.0.2.tar.gz", hash = "sha256:00ce4342c221b1931b2f35d46f5027d35bc62a4ca3a34628b2c5b514b4ca958a"}, ] flake8 = [ {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, @@ -2560,11 +2596,10 @@ imagesize = [ {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-3.1.0-py2.py3-none-any.whl", hash = "sha256:590690d61efdd716ff82c39ca9a9d4209252adfe288a4b5721181050acbd4175"}, - {file = "importlib_metadata-3.1.0.tar.gz", hash = "sha256:d9b8a46a0885337627a6430db287176970fff18ad421becec1d64cfc763c2099"}, + {file = "importlib_metadata-3.3.0-py3-none-any.whl", hash = "sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"}, + {file = "importlib_metadata-3.3.0.tar.gz", hash = "sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed"}, ] iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ @@ -2658,8 +2693,8 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] packaging = [ - {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, - {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, + {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"}, + {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"}, ] pathspec = [ {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, @@ -2677,8 +2712,8 @@ pg8000 = [ {file = "pg8000-1.16.6.tar.gz", hash = "sha256:8fc1e6a62ccb7c9830f1e7e9288e2d20eaf373cc8875b5c55b7d5d9b7717be91"}, ] phonenumbers = [ - {file = "phonenumbers-8.12.13-py2.py3-none-any.whl", hash = "sha256:9de2937034deb040eb9ac56519b0887e0fe89811e57f6f5c88359e3be20ae3b5"}, - {file = "phonenumbers-8.12.13.tar.gz", hash = "sha256:96d02120a3481e22d8a8eb5e4595ceec1930855749f6e4a06ef931881f59f562"}, + {file = "phonenumbers-8.12.15-py2.py3-none-any.whl", hash = "sha256:13d499f7114c4b37c54ee844b188d5e7441951a7da41de5fc1a25ff8fdceef80"}, + {file = "phonenumbers-8.12.15.tar.gz", hash = "sha256:b734bfcf33e87ddae72196a40b3d1af35abd0beb263816ae18e1bff612926406"}, ] pillow = [ {file = "Pillow-8.0.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:b63d4ff734263ae4ce6593798bcfee6dbfb00523c82753a3a03cbc05555a9cc3"}, @@ -2723,17 +2758,34 @@ prompt-toolkit = [ {file = "prompt_toolkit-3.0.8.tar.gz", hash = "sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c"}, ] psutil = [ - {file = "psutil-5.7.3-cp27-none-win32.whl", hash = "sha256:1cd6a0c9fb35ece2ccf2d1dd733c1e165b342604c67454fd56a4c12e0a106787"}, - {file = "psutil-5.7.3-cp27-none-win_amd64.whl", hash = "sha256:e02c31b2990dcd2431f4524b93491941df39f99619b0d312dfe1d4d530b08b4b"}, - {file = "psutil-5.7.3-cp35-cp35m-win32.whl", hash = "sha256:56c85120fa173a5d2ad1d15a0c6e0ae62b388bfb956bb036ac231fbdaf9e4c22"}, - {file = "psutil-5.7.3-cp35-cp35m-win_amd64.whl", hash = "sha256:fa38ac15dbf161ab1e941ff4ce39abd64b53fec5ddf60c23290daed2bc7d1157"}, - {file = "psutil-5.7.3-cp36-cp36m-win32.whl", hash = "sha256:01bc82813fbc3ea304914581954979e637bcc7084e59ac904d870d6eb8bb2bc7"}, - {file = "psutil-5.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:6a3e1fd2800ca45083d976b5478a2402dd62afdfb719b30ca46cd28bb25a2eb4"}, - {file = "psutil-5.7.3-cp37-cp37m-win32.whl", hash = "sha256:fbcac492cb082fa38d88587d75feb90785d05d7e12d4565cbf1ecc727aff71b7"}, - {file = "psutil-5.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:5d9106ff5ec2712e2f659ebbd112967f44e7d33f40ba40530c485cc5904360b8"}, - {file = "psutil-5.7.3-cp38-cp38-win32.whl", hash = "sha256:ade6af32eb80a536eff162d799e31b7ef92ddcda707c27bbd077238065018df4"}, - {file = "psutil-5.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:2cb55ef9591b03ef0104bedf67cc4edb38a3edf015cf8cf24007b99cb8497542"}, - {file = "psutil-5.7.3.tar.gz", hash = "sha256:af73f7bcebdc538eda9cc81d19db1db7bf26f103f91081d780bbacfcb620dee2"}, + {file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"}, + {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0ae6f386d8d297177fd288be6e8d1afc05966878704dad9847719650e44fc49c"}, + {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:12d844996d6c2b1d3881cfa6fa201fd635971869a9da945cf6756105af73d2df"}, + {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:02b8292609b1f7fcb34173b25e48d0da8667bc85f81d7476584d889c6e0f2131"}, + {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6ffe81843131ee0ffa02c317186ed1e759a145267d54fdef1bc4ea5f5931ab60"}, + {file = "psutil-5.8.0-cp27-none-win32.whl", hash = "sha256:ea313bb02e5e25224e518e4352af4bf5e062755160f77e4b1767dd5ccb65f876"}, + {file = "psutil-5.8.0-cp27-none-win_amd64.whl", hash = "sha256:5da29e394bdedd9144c7331192e20c1f79283fb03b06e6abd3a8ae45ffecee65"}, + {file = "psutil-5.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:74fb2557d1430fff18ff0d72613c5ca30c45cdbfcddd6a5773e9fc1fe9364be8"}, + {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:74f2d0be88db96ada78756cb3a3e1b107ce8ab79f65aa885f76d7664e56928f6"}, + {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99de3e8739258b3c3e8669cb9757c9a861b2a25ad0955f8e53ac662d66de61ac"}, + {file = "psutil-5.8.0-cp36-cp36m-win32.whl", hash = "sha256:36b3b6c9e2a34b7d7fbae330a85bf72c30b1c827a4366a07443fc4b6270449e2"}, + {file = "psutil-5.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:52de075468cd394ac98c66f9ca33b2f54ae1d9bff1ef6b67a212ee8f639ec06d"}, + {file = "psutil-5.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c6a5fd10ce6b6344e616cf01cc5b849fa8103fbb5ba507b6b2dee4c11e84c935"}, + {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:61f05864b42fedc0771d6d8e49c35f07efd209ade09a5afe6a5059e7bb7bf83d"}, + {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0dd4465a039d343925cdc29023bb6960ccf4e74a65ad53e768403746a9207023"}, + {file = "psutil-5.8.0-cp37-cp37m-win32.whl", hash = "sha256:1bff0d07e76114ec24ee32e7f7f8d0c4b0514b3fae93e3d2aaafd65d22502394"}, + {file = "psutil-5.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fcc01e900c1d7bee2a37e5d6e4f9194760a93597c97fee89c4ae51701de03563"}, + {file = "psutil-5.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6223d07a1ae93f86451d0198a0c361032c4c93ebd4bf6d25e2fb3edfad9571ef"}, + {file = "psutil-5.8.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d225cd8319aa1d3c85bf195c4e07d17d3cd68636b8fc97e6cf198f782f99af28"}, + {file = "psutil-5.8.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:28ff7c95293ae74bf1ca1a79e8805fcde005c18a122ca983abf676ea3466362b"}, + {file = "psutil-5.8.0-cp38-cp38-win32.whl", hash = "sha256:ce8b867423291cb65cfc6d9c4955ee9bfc1e21fe03bb50e177f2b957f1c2469d"}, + {file = "psutil-5.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:90f31c34d25b1b3ed6c40cdd34ff122b1887a825297c017e4cbd6796dd8b672d"}, + {file = "psutil-5.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6323d5d845c2785efb20aded4726636546b26d3b577aded22492908f7c1bdda7"}, + {file = "psutil-5.8.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:245b5509968ac0bd179287d91210cd3f37add77dad385ef238b275bad35fa1c4"}, + {file = "psutil-5.8.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:90d4091c2d30ddd0a03e0b97e6a33a48628469b99585e2ad6bf21f17423b112b"}, + {file = "psutil-5.8.0-cp39-cp39-win32.whl", hash = "sha256:ea372bcc129394485824ae3e3ddabe67dc0b118d262c568b4d2602a7070afdb0"}, + {file = "psutil-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:f4634b033faf0d968bb9220dd1c793b897ab7f1189956e1aa9eae752527127d3"}, + {file = "psutil-5.8.0.tar.gz", hash = "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6"}, ] psycopg2 = [ {file = "psycopg2-2.8.6-cp27-cp27m-win32.whl", hash = "sha256:068115e13c70dc5982dfc00c5d70437fe37c014c808acce119b5448361c03725"}, @@ -2751,8 +2803,8 @@ psycopg2 = [ {file = "psycopg2-2.8.6.tar.gz", hash = "sha256:fb23f6c71107c37fd667cb4ea363ddeb936b348bbd6449278eb92c189699f543"}, ] py = [ - {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, - {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] pyasn1 = [ {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, @@ -2792,8 +2844,6 @@ pycryptodome = [ {file = "pycryptodome-3.9.9-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:5598dc6c9dbfe882904e54584322893eff185b98960bbe2cdaaa20e8a437b6e5"}, {file = "pycryptodome-3.9.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1cfdb92dca388e27e732caa72a1cc624520fe93752a665c3b6cd8f1a91b34916"}, {file = "pycryptodome-3.9.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5f19e6ef750f677d924d9c7141f54bade3cd56695bbfd8a9ef15d0378557dfe4"}, - {file = "pycryptodome-3.9.9-cp27-cp27m-win32.whl", hash = "sha256:a3d8a9efa213be8232c59cdc6b65600276508e375e0a119d710826248fd18d37"}, - {file = "pycryptodome-3.9.9-cp27-cp27m-win_amd64.whl", hash = "sha256:50826b49fbca348a61529693b0031cdb782c39060fb9dca5ac5dff858159dc5a"}, {file = "pycryptodome-3.9.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:19cb674df6c74a14b8b408aa30ba8a89bd1c01e23505100fb45f930fbf0ed0d9"}, {file = "pycryptodome-3.9.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:28f75e58d02019a7edc7d4135203d2501dfc47256d175c72c9798f9a129a49a7"}, {file = "pycryptodome-3.9.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:6d3baaf82681cfb1a842f1c8f77beac791ceedd99af911e4f5fabec32bae2259"}, @@ -2804,26 +2854,17 @@ pycryptodome = [ {file = "pycryptodome-3.9.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:7798e73225a699651888489fbb1dbc565e03a509942a8ce6194bbe6fb582a41f"}, {file = "pycryptodome-3.9.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:46e96aeb8a9ca8b1edf9b1fd0af4bf6afcf3f1ca7fa35529f5d60b98f3e4e959"}, {file = "pycryptodome-3.9.9-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:843e5f10ecdf9d307032b8b91afe9da1d6ed5bb89d0bbec5c8dcb4ba44008e11"}, - {file = "pycryptodome-3.9.9-cp36-cp36m-win32.whl", hash = "sha256:b68794fba45bdb367eeb71249c26d23e61167510a1d0c3d6cf0f2f14636e62ee"}, - {file = "pycryptodome-3.9.9-cp36-cp36m-win_amd64.whl", hash = "sha256:60febcf5baf70c566d9d9351c47fbd8321da9a4edf2eff45c4c31c86164ca794"}, {file = "pycryptodome-3.9.9-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:4ed27951b0a17afd287299e2206a339b5b6d12de9321e1a1575261ef9c4a851b"}, {file = "pycryptodome-3.9.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9000877383e2189dafd1b2fc68c6c726eca9a3cfb6d68148fbb72ccf651959b6"}, {file = "pycryptodome-3.9.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:faa682c404c218e8788c3126c9a4b8fbcc54dc245b5b6e8ea5b46f3b63bd0c84"}, {file = "pycryptodome-3.9.9-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:62c488a21c253dadc9f731a32f0ac61e4e436d81a1ea6f7d1d9146ed4d20d6bd"}, - {file = "pycryptodome-3.9.9-cp37-cp37m-win32.whl", hash = "sha256:834b790bbb6bd18956f625af4004d9c15eed12d5186d8e57851454ae76d52215"}, - {file = "pycryptodome-3.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:70d807d11d508433daf96244ec1c64e55039e8a35931fc5ea9eee94dbe3cb6b5"}, {file = "pycryptodome-3.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:27397aee992af69d07502126561d851ba3845aa808f0e55c71ad0efa264dd7d4"}, {file = "pycryptodome-3.9.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d7ec2bd8f57c559dd24e71891c51c25266a8deb66fc5f02cc97c7fb593d1780a"}, {file = "pycryptodome-3.9.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e15bde67ccb7d4417f627dd16ffe2f5a4c2941ce5278444e884cb26d73ecbc61"}, {file = "pycryptodome-3.9.9-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:5c3c4865730dfb0263f822b966d6d58429d8b1e560d1ddae37685fd9e7c63161"}, - {file = "pycryptodome-3.9.9-cp38-cp38-win32.whl", hash = "sha256:76b1a34d74bb2c91bce460cdc74d1347592045627a955e9a252554481c17c52f"}, - {file = "pycryptodome-3.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:6e4227849e4231a3f5b35ea5bdedf9a82b3883500e5624f00a19156e9a9ef861"}, {file = "pycryptodome-3.9.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2a68df525b387201a43b27b879ce8c08948a430e883a756d6c9e3acdaa7d7bd8"}, {file = "pycryptodome-3.9.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:a4599c0ca0fc027c780c1c45ed996d5bef03e571470b7b1c7171ec1e1a90914c"}, {file = "pycryptodome-3.9.9-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b4e6b269a8ddaede774e5c3adbef6bf452ee144e6db8a716d23694953348cd86"}, - {file = "pycryptodome-3.9.9-cp39-cp39-win32.whl", hash = "sha256:a199e9ca46fc6e999e5f47fce342af4b56c7de85fae893c69ab6aa17531fb1e1"}, - {file = "pycryptodome-3.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:6e89bb3826e6f84501e8e3b205c22595d0c5492c2f271cbb9ee1c48eb1866645"}, - {file = "pycryptodome-3.9.9.tar.gz", hash = "sha256:910e202a557e1131b1c1b3f17a63914d57aac55cf9fb9b51644962841c3995c4"}, ] pydocstyle = [ {file = "pydocstyle-5.1.1-py3-none-any.whl", hash = "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"}, @@ -2834,8 +2875,8 @@ pyflakes = [ {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, ] pygments = [ - {file = "Pygments-2.7.2-py3-none-any.whl", hash = "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"}, - {file = "Pygments-2.7.2.tar.gz", hash = "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0"}, + {file = "Pygments-2.7.3-py3-none-any.whl", hash = "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"}, + {file = "Pygments-2.7.3.tar.gz", hash = "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716"}, ] pyjwt = [ {file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"}, @@ -2846,8 +2887,8 @@ pyparsing = [ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytest = [ - {file = "pytest-6.1.2-py3-none-any.whl", hash = "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe"}, - {file = "pytest-6.1.2.tar.gz", hash = "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"}, + {file = "pytest-6.2.1-py3-none-any.whl", hash = "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8"}, + {file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"}, ] pytest-cov = [ {file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"}, @@ -2947,8 +2988,8 @@ regex = [ {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"}, ] requests = [ - {file = "requests-2.25.0-py2.py3-none-any.whl", hash = "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"}, - {file = "requests-2.25.0.tar.gz", hash = "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8"}, + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, ] restructuredtext-lint = [ {file = "restructuredtext_lint-1.3.2.tar.gz", hash = "sha256:d3b10a1fe2ecac537e51ae6d151b223b78de9fafdd50e5eb6b08c243df173c80"}, @@ -3009,8 +3050,8 @@ snowballstemmer = [ {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, ] spdx-license-list = [ - {file = "spdx_license_list-0.5.1-py3-none-any.whl", hash = "sha256:32f1401e0077b46ba8b3d9c648b6503ef1d49c41aab51aa13816be2dde3b4a13"}, - {file = "spdx_license_list-0.5.1.tar.gz", hash = "sha256:64cb5de37724c64cdeccafa2ae68667ff8ccdb7b688f51c1c2be82d7ebe3a112"}, + {file = "spdx_license_list-0.5.2-py3-none-any.whl", hash = "sha256:1b338470c7b403dbecceca563a316382c7977516128ca6c1e8f7078e3ed6e7b0"}, + {file = "spdx_license_list-0.5.2.tar.gz", hash = "sha256:952996f72ab807972dc2278bb9b91e5294767211e51f09aad9c0e2ff5b82a31b"}, ] sphinx = [ {file = "Sphinx-3.3.1-py3-none-any.whl", hash = "sha256:d4e59ad4ea55efbb3c05cde3bfc83bfc14f0c95aa95c3d75346fcce186a47960"}, @@ -3053,15 +3094,15 @@ sqlparse = [ {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, ] stevedore = [ - {file = "stevedore-3.2.2-py3-none-any.whl", hash = "sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62"}, - {file = "stevedore-3.2.2.tar.gz", hash = "sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0"}, + {file = "stevedore-3.3.0-py3-none-any.whl", hash = "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"}, + {file = "stevedore-3.3.0.tar.gz", hash = "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee"}, ] termcolor = [ {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"}, ] testfixtures = [ - {file = "testfixtures-6.15.0-py2.py3-none-any.whl", hash = "sha256:e17f4f526fc90b0ac9bc7f8ca62b7dec17d9faf3d721f56bda4f0fd94d02f85a"}, - {file = "testfixtures-6.15.0.tar.gz", hash = "sha256:409f77cfbdad822d12a8ce5c4aa8fb4d0bb38073f4a5444fede3702716a2cec2"}, + {file = "testfixtures-6.17.0-py2.py3-none-any.whl", hash = "sha256:ebcc3e024d47bb58a60cdc678604151baa0c920ae2814004c89ac9066de31b2c"}, + {file = "testfixtures-6.17.0.tar.gz", hash = "sha256:fa7c170df68ca6367eda061e9ec339ae3e6d3679c31e04033f83ef97a7d7d0ce"}, ] "testing.common.database" = [ {file = "testing.common.database-2.0.3-py2.py3-none-any.whl", hash = "sha256:e3ed492bf480a87f271f74c53b262caf5d85c8bc09989a8f534fa2283ec52492"}, @@ -3080,11 +3121,11 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tqdm = [ - {file = "tqdm-4.54.0-py2.py3-none-any.whl", hash = "sha256:9e7b8ab0ecbdbf0595adadd5f0ebbb9e69010e0bd48bbb0c15e550bf2a5292df"}, - {file = "tqdm-4.54.0.tar.gz", hash = "sha256:5c0d04e06ccc0da1bd3fa5ae4550effcce42fcad947b4a6cafa77bdc9b09ff22"}, + {file = "tqdm-4.54.1-py2.py3-none-any.whl", hash = "sha256:d4f413aecb61c9779888c64ddf0c62910ad56dcbe857d8922bb505d4dbff0df1"}, + {file = "tqdm-4.54.1.tar.gz", hash = "sha256:38b658a3e4ecf9b4f6f8ff75ca16221ae3378b2e175d846b6b33ea3a20852cf5"}, ] twilio = [ - {file = "twilio-6.48.0.tar.gz", hash = "sha256:745f3dfe6685e001ddd2baa290b37377426388906ea6d3549e06fb2f669da7b3"}, + {file = "twilio-6.50.1.tar.gz", hash = "sha256:dd8371c9b4ea422d6de7526b63b587da82e8488f2b3f6b1258d2cad6e4006a65"}, ] typed-ast = [ {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, diff --git a/pyproject.toml b/pyproject.toml index 9d0c7e7a40c795c77697aefea7b04cbef1b8e284..cfb02aa3649f591f00fac32004c0a397e43a09ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,7 @@ django-favicon-plus-reloaded = "^1.0.4" django-health-check = "^3.12.1" psutil = "^5.7.0" celery-progress = "^0.0.14" +django-cachalot = "^2.3.2" django-prometheus = "^2.1.0" importlib-metadata = {version = "^3.0.0", python = "<3.9"} django-model-utils = "^4.0.0" @@ -100,6 +101,9 @@ celery = ["Celery", "django-celery-results", "django-celery-beat", "django-celer [tool.poetry.dev-dependencies] aleksis-builddeps = "*" +[tool.poetry.scripts] +aleksis-admin = 'aleksis.core.util.manage:aleksis_cmd' + [tool.black] line-length = 100 exclude = "/migrations/"