diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d1f55a61580c33875a3bcb3010f371a41d46664e..2c24e62bb4d1acd60579eb0064c0a79c08748d7f 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -36,6 +36,12 @@ To ensure code is styled correctly, before commiting, run:: tox -e reformat +Text documents +~~~~~~~~~~~~~~ + +If there is no objective reason against it, all text documents accompanying +the source use `reStructuredText`_. + Working with the Git repository ------------------------------- @@ -44,18 +50,34 @@ The Git repository shall be used as a historic documentation of development and as change management. It is important that the Git commit history describes waht was changed, by whom and why. -Feature branches -~~~~~~~~~~~~~~~~ +Help and information on Git for beginners are available in the `Git guide`_ + +Feature and issue branches +~~~~~~~~~~~~~~~~~~~~~~~~~~ All features and bug fixes should be developed in their own branch and later merged into the master branch as a whole. Of course, sometimes, it is -sensible to not do that, e.g. for fixing mere typos and the like +sensible to not do that, e.g. for fixing mere typos and the like. -WIthin the feature branch, every logical step should be commited separately. +Within the feature branch, every logical step should be commited separately. It is neither required nor desired to do micro-commits about every development step. The commit history should describe the trains of thought the design and implementation is based on. +If you work on multiple issues at the same time, you have to change between +branches. Never work on unrelated issues in the same branch. + +Branches should either contain the number and title of the related issue (as +generated by GitLab), or follow the naming convention type/name, where type +is one of bugfix, feature, or refactor. + +All changes on the code should be commited and pushed before stopping work on +in order to prevent data loss. If a logical step is continued later, you +should amend and force-push the commit. + +Issue branches should be rebased onto the current master regularly to avoid +merge conflicts. + Commit messages ~~~~~~~~~~~~~~~ @@ -72,6 +94,25 @@ square brackets if it relates to a certain part of the repository, e.g. [CI] when changing CI/CD configuration or support code, [Dev] when changing something in the development utilities, etc. +Example:: + + Solve LDAP connection problems + + - Add the ldap-with-unicorn-dust dependency + - Configure settings.py to accept the correct groups from LDAP + + Closes #10. + +Merge Requests +~~~~~~~~~~~~~~ + +If you think that the work on your feature branch is finished, you have to +create a merge request on EduGit in order to let other developers and the +maintainers take a look at it. + +See below on how to submit patches if you cannot use the development +platform. + Manifestos governing development -------------------------------- @@ -89,9 +130,8 @@ probably governed by laws defining what and when to store. In that case, giving the user control over these decisions is not possible. Developers need to decide what should resonably be followed. - The case on supporting non-free services ----------------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Defined by the `Free Software Definition`_, it is an essential freedom to be allowed to use free software for any purpose, without limitation. Thus, @@ -99,7 +139,7 @@ interoperability with non-free services shall not be ruled out, and the AlekSIS project explicitly welcomes implementing support for interoperability with non-free services. -However, to purposefullt foster free software and services, if +However, to purposefully foster free software and services, if interoperability for a certain kind of non-free service is implemented, this must be done in a generalised manner (i.e. using open protocols and interfaces). For example, if implementing interoperability with some @@ -110,11 +150,42 @@ is connected through a proprietary, single-purpose interface, measures shall be taken to also support alternative free services. -Text documents --------------- +Documentation +------------- -If there is no objective reason against it, all text documents accompanying -the source use `reStructuredText`_. +The documentation in the AlekSIS project shall consist of three layers. + +Source code comments +~~~~~~~~~~~~~~~~~~~~ + +The parts of your code that are not self-explaining have to be commented. +Ideally, source code is self-explaining, in the sense that its logical +structure, naming of variables, and the like makes it easy to read and +understand, for a reasonably talented programmer, to follow what it does. + +Docstrings +~~~~~~~~~~ + +All functions, methods, classes and modules that are newly added (or changed +extensively) must contain a docstring for other developers to understand +what it does. Docstrings of public elements will be included in the +developer documentation. + +Sphinx documentation +~~~~~~~~~~~~~~~~~~~~ + +In addition to that you should document the function or the way the app +works in the project documentation (`docs/` directory). Use that especially +for functionality which is shared by your app for other apps (public APIs). + +Your Sphinx documentation should contain what the API can and shall be sued +for, and how other apps can benefit from it. + +When creating a new app, also include documentation about it targeted at +administrators and users. At least you have to document what new developers +and users have to do in order to get a working instance of the app. + +Sphinx documentation for all official apps will be published together. Contributing to upstream @@ -126,13 +197,36 @@ generalised upstream dependency be created, under the most permissive licence possible. +How to contact the team +----------------------- + +Development platform +~~~~~~~~~~~~~~~~~~~~ + +Main development of AlekSIS is done on the `EduGit`_ platform in the +`AlekSIS group`_ and discussions are held on the linked `Mattermost team`_. + +All platforms and tools mandated for development are free software and +freely usable. EduGit accepts a variety of sources for login, so +contributors are free to decide where they want to register in order to +participate. + +If any contributor cannot use the platforms for whatever reasons, patches and +questions directed at the developers can also be e-mailed to +<aleksis-dev@lists.teckids.org>. + + .. _PEP 8: https://pep8.org/ .. _Django coding style: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/ .. _black: https://black.readthedocs.io/en/stable/ .. _Django Best Practices: https://django-best-practices.readthedocs.io/en/latest/index.html +.. _Git guide: https://rogerdudler.github.io/git-guide/ .. _How to Write a Git Commit Message: https://chris.beams.io/posts/git-commit/ .. _Sane software manifesto: https://sane-software.globalcode.info/ .. _Accessibility Manifesto: http://accessibilitymanifesto.com/ .. _User Data Manifesto: https://userdatamanifesto.org/ .. _Free Software Definition: https://www.gnu.org/philosophy/free-sw.en.html .. _reStructuredText: http://docutils.sourceforge.net/rst.html +.. _EduGit: https://edugit.org/ +.. _AlekSIS group: https://edugit.org/AlekSIS/ +.. _Mattermost team: https://mattermost.edugit.org/biscuit/ diff --git a/aleksis/core/__init__.py b/aleksis/core/__init__.py index 2484be76410f3d59a7bed7dda78c5f4e0f2a1c63..21a49f0785f3977d9a265059a32ec957c9e1eb3c 100644 --- a/aleksis/core/__init__.py +++ b/aleksis/core/__init__.py @@ -1,5 +1,11 @@ import pkg_resources +try: + from .celery import app as celery_app +except ModuleNotFoundError: + # Celery is not available + celery_app = None + try: __version__ = pkg_resources.get_distribution("AlekSIS").version except Exception: diff --git a/aleksis/core/celery.py b/aleksis/core/celery.py new file mode 100644 index 0000000000000000000000000000000000000000..2f4dce954576a5d688311c466bc2b9fb4ad4e151 --- /dev/null +++ b/aleksis/core/celery.py @@ -0,0 +1,8 @@ +import os +from celery import Celery + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "aleksis.core.settings") + +app = Celery("aleksis") # noqa +app.config_from_object('django.conf:settings', namespace='CELERY') +app.autodiscover_tasks() diff --git a/aleksis/core/locale/ar/LC_MESSAGES/djangojs.po b/aleksis/core/locale/ar/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000000000000000000000000000000000000..335d14ad4e58736619502c3176135bb205d8dbe2 --- /dev/null +++ b/aleksis/core/locale/ar/LC_MESSAGES/djangojs.po @@ -0,0 +1,32 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-01-21 21:04+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" + +#: static/js/main.js:21 +msgid "Today" +msgstr "" + +#: static/js/main.js:22 +msgid "Cancel" +msgstr "" + +#: static/js/main.js:23 +msgid "OK" +msgstr "" diff --git a/aleksis/core/locale/de_DE/LC_MESSAGES/djangojs.po b/aleksis/core/locale/de_DE/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000000000000000000000000000000000000..01864b41af67222125210762c072fb23302fda3a --- /dev/null +++ b/aleksis/core/locale/de_DE/LC_MESSAGES/djangojs.po @@ -0,0 +1,30 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-01-21 21:04+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: static/js/main.js:21 +msgid "Today" +msgstr "" + +#: static/js/main.js:22 +msgid "Cancel" +msgstr "" + +#: static/js/main.js:23 +msgid "OK" +msgstr "" diff --git a/aleksis/core/locale/fr/LC_MESSAGES/djangojs.po b/aleksis/core/locale/fr/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000000000000000000000000000000000000..d714559929037c311183f5533697aef73d8b6a12 --- /dev/null +++ b/aleksis/core/locale/fr/LC_MESSAGES/djangojs.po @@ -0,0 +1,31 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-01-21 21:04+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \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" + +#: static/js/main.js:21 +msgid "Today" +msgstr "" + +#: static/js/main.js:22 +msgid "Cancel" +msgstr "" + +#: static/js/main.js:23 +msgid "OK" +msgstr "" diff --git a/aleksis/core/locale/nb_NO/LC_MESSAGES/djangojs.po b/aleksis/core/locale/nb_NO/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000000000000000000000000000000000000..01864b41af67222125210762c072fb23302fda3a --- /dev/null +++ b/aleksis/core/locale/nb_NO/LC_MESSAGES/djangojs.po @@ -0,0 +1,30 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-01-21 21:04+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: static/js/main.js:21 +msgid "Today" +msgstr "" + +#: static/js/main.js:22 +msgid "Cancel" +msgstr "" + +#: static/js/main.js:23 +msgid "OK" +msgstr "" diff --git a/aleksis/core/locale/tr_TR/LC_MESSAGES/djangojs.po b/aleksis/core/locale/tr_TR/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000000000000000000000000000000000000..01864b41af67222125210762c072fb23302fda3a --- /dev/null +++ b/aleksis/core/locale/tr_TR/LC_MESSAGES/djangojs.po @@ -0,0 +1,30 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-01-21 21:04+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: static/js/main.js:21 +msgid "Today" +msgstr "" + +#: static/js/main.js:22 +msgid "Cancel" +msgstr "" + +#: static/js/main.js:23 +msgid "OK" +msgstr "" diff --git a/aleksis/core/migrations/0001_initial.py b/aleksis/core/migrations/0001_initial.py index f8cc0d4d5294f0ca7b9243a3ce704d94169d855c..2d330f9504b8c4fa389ec50bc091e9b814c73a29 100644 --- a/aleksis/core/migrations/0001_initial.py +++ b/aleksis/core/migrations/0001_initial.py @@ -2,6 +2,7 @@ import aleksis.core.mixins from django.conf import settings +import django.contrib.postgres.fields.jsonb from django.db import migrations, models import django.db.models.deletion import image_cropping.fields @@ -22,11 +23,12 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=60, unique=True, verbose_name='Long name of group')), ('short_name', models.CharField(max_length=16, unique=True, verbose_name='Short name of group')), + ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)), ], options={ 'ordering': ['short_name', 'name'], }, - bases=(models.Model, aleksis.core.mixins.ExtensibleModel), + bases=(aleksis.core.mixins.ExtensibleModel,), ), migrations.CreateModel( name='School', @@ -42,6 +44,7 @@ class Migration(migrations.Migration): image_cropping.fields.ImageRatioField('logo', '600x600', adapt_rotation=False, allow_fullsize=False, free_crop=False, help_text=None, hide_image_field=False, size_warning=True, verbose_name='logo cropping')), + ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)), ], options={ 'ordering': ['name', 'name_official'], @@ -55,6 +58,7 @@ class Migration(migrations.Migration): ('date_start', models.DateField(null=True, verbose_name='Effective start date of term')), ('date_end', models.DateField(null=True, verbose_name='Effective end date of term')), ('current', models.NullBooleanField(default=None, unique=True)), + ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)), ], ), migrations.CreateModel( @@ -94,11 +98,12 @@ class Migration(migrations.Migration): models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.Group')), ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='person', to=settings.AUTH_USER_MODEL)), + ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)), ], options={ 'ordering': ['last_name', 'first_name'], }, - bases=(models.Model, aleksis.core.mixins.ExtensibleModel), + bases=(aleksis.core.mixins.ExtensibleModel,), ), migrations.AddField( model_name='group', diff --git a/aleksis/core/migrations/0006_create_default_term.py b/aleksis/core/migrations/0006_create_default_term.py new file mode 100644 index 0000000000000000000000000000000000000000..724da0cd4bdbafab986d6b072930ccdf9f1050f4 --- /dev/null +++ b/aleksis/core/migrations/0006_create_default_term.py @@ -0,0 +1,27 @@ +from django.db import migrations, models + +from datetime import date + +def create_or_mark_current_term(apps, schema_editor): + db_alias = schema_editor.connection.alias + + SchoolTerm = apps.get_model('core', 'SchoolTerm') # noqa + + if not SchoolTerm.objects.filter(current=True).exists(): + if SchoolTerm.objects.using(db_alias).exists(): + term = SchoolTerm.objects.using(db_alias).latest('date_start') + term.current=True + term.save() + else: + SchoolTerm.objects.using(db_alias).create(date_start=date.today(), current=True) + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_add_verbose_names_meta'), + ] + + operations = [ + migrations.RunPython(create_or_mark_current_term) + ] diff --git a/aleksis/core/migrations/0007_create_admin_user.py b/aleksis/core/migrations/0007_create_admin_user.py new file mode 100644 index 0000000000000000000000000000000000000000..384e7cc10f2a474817d0386faade842aea72541d --- /dev/null +++ b/aleksis/core/migrations/0007_create_admin_user.py @@ -0,0 +1,25 @@ +from django.contrib.auth import get_user_model +from django.db import migrations + + +def create_superuser(apps, schema_editor): + User = get_user_model() + + if not User.objects.filter(is_superuser=True).exists(): + User.objects.create_superuser( + username='admin', + email='root@example.com', + password='admin' + ).save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_create_default_term'), + ] + + operations = [ + migrations.RunPython(create_superuser) + ] + diff --git a/aleksis/core/migrations/0008_rename_fields_notification_activity.py b/aleksis/core/migrations/0008_rename_fields_notification_activity.py new file mode 100644 index 0000000000000000000000000000000000000000..4197d0feda366e3f3a4af3489f23763b1ed7104d --- /dev/null +++ b/aleksis/core/migrations/0008_rename_fields_notification_activity.py @@ -0,0 +1,39 @@ +# Generated by Django 3.0.2 on 2020-01-22 16:49 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_create_admin_user'), + ] + + operations = [ + migrations.RenameField( + model_name='notification', + old_name='user', + new_name='recipient', + ), + migrations.RenameField( + model_name='notification', + old_name='app', + new_name='sender', + ), + migrations.RenameField( + model_name='notification', + old_name='mailed', + new_name='sent', + ), + migrations.AlterField( + model_name='activity', + name='created_at', + field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), + ), + migrations.AlterField( + model_name='notification', + name='created_at', + field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), + ), + ] diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py index ad159fed2bfb603b8d408df070e16fca3519479f..ff79940ea9e5e091b381a99a85b2d1f8a9587d1a 100644 --- a/aleksis/core/mixins.py +++ b/aleksis/core/mixins.py @@ -5,10 +5,12 @@ from django.db import models from django.db.models import QuerySet from easyaudit.models import CRUDEvent +from jsonstore.fields import JSONField, JSONFieldMixin -class ExtensibleModel(object): - """ Allow injection of code from AlekSIS apps to extend model functionality. +class ExtensibleModel(models.Model): + """ Allow injection of fields and code from AlekSIS apps to extend + model functionality. After all apps have been loaded, the code in the `model_extensions` module in every app is executed. All code that shall be injected into a model goes there. @@ -19,6 +21,8 @@ class ExtensibleModel(object): from datetime import date, timedelta + from jsonstore import CharField + from aleksis.core.models import Person @Person.property @@ -29,6 +33,8 @@ class ExtensibleModel(object): def age(self) -> timedelta: return self.date_of_birth - date.today() + Person.field(shirt_size=CharField()) + For a more advanced example, using features from the ORM, see AlekSIS-App-Chronos and AlekSIS-App-Alsijil. @@ -37,6 +43,8 @@ class ExtensibleModel(object): - Dominik George <dominik.george@teckids.org> """ + extended_data = JSONField(default=dict, editable=False) + @classmethod def _safe_add(cls, obj: Any, name: Optional[str]) -> None: # Decide the name for the attribute @@ -48,12 +56,12 @@ class ExtensibleModel(object): else: raise ValueError("%s is not a valid name." % name) - # Verify that property name does not clash with other names in the class + # Verify that attribute name does not clash with other names in the class if hasattr(cls, prop_name): raise ValueError("%s already used." % prop_name) - # Add function wrapped in property decorator if we got here - setattr(cls, prop_name, obj) + # Let Django's model magic add the attribute if we got here + cls.add_to_class(name, obj) @classmethod def property(cls, func: Callable[[], Any], name: Optional[str] = None) -> None: @@ -67,6 +75,32 @@ class ExtensibleModel(object): cls._safe_add(func, func.__name__) + @classmethod + def field(cls, **kwargs) -> None: + """ Adds the passed jsonstore field. Must be one of the fields in + django-jsonstore. + + Accepts exactly one keyword argument, with the name being the desired + model field name and the value the field instance. + """ + + # Force kwargs to be exactly one argument + if len(kwargs) != 1: + raise TypeError("field() takes 1 keyword argument but %d were given" % len(kwargs)) + name, field = kwargs.popitem() + + # Force the field to be one of the jsonstore fields + if JSONFieldMixin not in field.__class__.__mro__: + raise TypeError("Only jsonstore fields can be added to models.") + + # Force use of the one JSONField defined in this mixin + field.json_field_name = "extended_data" + + cls._safe_add(field, name) + + class Meta: + abstract = True + class CRUDMixin(models.Model): class Meta: diff --git a/aleksis/core/models.py b/aleksis/core/models.py index 9dacaa75fcc105b7ec7cfbaf571d95cf7afa23e1..ecf66c8ed4e0d15ff46cd9c0ee239bfb84bc4737 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -3,16 +3,17 @@ from typing import Optional from django.contrib.auth import get_user_model from django.contrib.auth.models import User from django.db import models -from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from image_cropping import ImageCropField, ImageRatioField from phonenumber_field.modelfields import PhoneNumberField -from .mailer import send_mail_with_template from .mixins import ExtensibleModel +from .util.notifications import send_notification +from constance import config -class School(models.Model): + +class School(ExtensibleModel): """A school that will have many other objects linked to it. AlekSIS has multi-tenant support by linking all objects to a school, and limiting all features to objects related to the same school as the @@ -39,7 +40,7 @@ class School(models.Model): verbose_name_plural = _("Schools") -class SchoolTerm(models.Model): +class SchoolTerm(ExtensibleModel): """ Information about a term (limited time frame) that data can be linked to. """ @@ -61,7 +62,7 @@ class SchoolTerm(models.Model): verbose_name_plural = _("School terms") -class Person(models.Model, ExtensibleModel): +class Person(ExtensibleModel): """ A model describing any person related to a school, including, but not limited to, students, teachers and guardians (parents). """ @@ -141,13 +142,22 @@ class Person(models.Model, ExtensibleModel): @property def full_name(self) -> str: - return "%s, %s" % (self.last_name, self.first_name) + return f"{self.last_name}, {self.first_name}" + + @property + def adressing_name(self) -> str: + if config.ADRESSING_NAME_FORMAT == "dutch": + return f"{self.last_name} {self.first_name}" + elif config.ADRESSING_NAME_FORMAT == "english": + return f"{self.last_name}, {self.first_name}" + else: + return f"{self.first_name} {self.last_name}" def __str__(self) -> str: return self.full_name -class Group(models.Model, ExtensibleModel): +class Group(ExtensibleModel): """Any kind of group of persons in a school, including, but not limited classes, clubs, and the like. """ @@ -183,7 +193,7 @@ class Activity(models.Model): app = models.CharField(max_length=100, verbose_name=_("Application")) - created_at = models.DateTimeField(default=timezone.now, verbose_name=_("Created at")) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at")) def __str__(self): return self.title @@ -194,26 +204,25 @@ class Activity(models.Model): class Notification(models.Model): - user = models.ForeignKey("Person", on_delete=models.CASCADE, related_name="notifications") + sender = models.CharField(max_length=100, verbose_name=_("Sender")) + recipient = models.ForeignKey("Person", on_delete=models.CASCADE, related_name="notifications") + title = models.CharField(max_length=150, verbose_name=_("Title")) description = models.TextField(max_length=500, verbose_name=_("Description")) link = models.URLField(blank=True, verbose_name=_("Link")) - app = models.CharField(max_length=100, verbose_name=_("Application")) - read = models.BooleanField(default=False, verbose_name=_("Read")) - mailed = models.BooleanField(default=False, verbose_name=_("Mailed")) - created_at = models.DateTimeField(default=timezone.now, verbose_name=_("Created at")) + sent = models.BooleanField(default=False, verbose_name=_("Sent")) + + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at")) def __str__(self): return self.title def save(self, **kwargs): + send_notification(self) + self.sent = True super().save(**kwargs) - if not self.mailed: - send_mail_with_template(self.title, [self.user.email], "mail/notification.txt", "mail/notification.html", - {"notification": self}) - self.mailed = True class Meta: verbose_name = _("Notification") diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index bd0f30cff8495d05537c8b5d6f12f2875910a5ce..46b070449c3bc6ff46e98ace323953b7f6252ac1 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -2,12 +2,14 @@ import os import sys from glob import glob -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ +from calendarweek.django import i18n_day_name_choices_lazy from dynaconf import LazySettings from easy_thumbnails.conf import Settings as thumbnail_settings -from .util.core_helpers import get_app_packages +from .util.core_helpers import get_app_packages, lazy_config, merge_app_settings +from .util.notifications import get_notification_choices_lazy ENVVAR_PREFIX_FOR_DYNACONF = "ALEKSIS" DIRS_FOR_DYNACONF = ["/etc/aleksis"] @@ -66,6 +68,8 @@ INSTALLED_APPS = [ "debug_toolbar", "django_select2", "hattori", + "templated_email", + "html2text", "django_otp.plugins.otp_totp", "django_otp.plugins.otp_static", "django_otp", @@ -75,8 +79,11 @@ INSTALLED_APPS = [ "two_factor", "material", "pwa", + "ckeditor", + "django_js_reverse", ] +merge_app_settings("INSTALLED_APPS", INSTALLED_APPS, True) INSTALLED_APPS += get_app_packages() STATICFILES_FINDERS = [ @@ -91,6 +98,7 @@ MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.locale.LocaleMiddleware", + "django.middleware.http.ConditionalGetMiddleware", "django_global_request.middleware.GlobalRequestMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", @@ -150,6 +158,8 @@ DATABASES = { } } +merge_app_settings("DATABASES", DATABASES, False) + if _settings.get("caching.memcached.enabled", True): CACHES = { "default": { @@ -239,6 +249,8 @@ YARN_INSTALLED_APPS = [ "select2", ] +merge_app_settings("YARN_INSTALLED_APPS", YARN_INSTALLED_APPS, True) + JS_URL = _settings.get("js_assets.url", STATIC_URL) JS_ROOT = _settings.get("js_assets.root", NODE_MODULES_ROOT + "/node_modules") @@ -255,6 +267,8 @@ ANY_JS = { }, } +merge_app_settings("ANY_JS", ANY_JS, True) + SASS_PROCESSOR_AUTO_INCLUDE = False SASS_PROCESSOR_CUSTOM_FUNCTIONS = { "get-colour": "aleksis.core.util.sass_helpers.get_colour", @@ -280,28 +294,64 @@ if _settings.get("mail.server.host", None): EMAIL_HOST_USER = _settings.get("mail.server.user") EMAIL_HOST_PASSWORD = _settings.get("mail.server.password") +TEMPLATED_EMAIL_BACKEND = 'templated_email.backends.vanilla_django' +TEMPLATED_EMAIL_AUTO_PLAIN = True + + TEMPLATE_VISIBLE_SETTINGS = ["ADMINS", "DEBUG"] CONSTANCE_BACKEND = "constance.backends.database.DatabaseBackend" CONSTANCE_ADDITIONAL_FIELDS = { + "char_field": ["django.forms.CharField", {}], "image_field": ["django.forms.ImageField", {}], "email_field": ["django.forms.EmailField", {}], "url_field": ["django.forms.URLField", {}], + "integer_field": ["django.forms.IntegerField", {}], + "password_field": ["django.forms.CharField", { + 'widget': 'django.forms.PasswordInput', + }], + "adressing-select": ['django.forms.fields.ChoiceField', { + 'widget': 'django.forms.Select', + 'choices': ((None, "-----"), + # ("german", _("<first name>") + " " + _("<last name>")), + # ("english", _("<last name>") + ", " + _("<first name>")), + # ("netherlands", _("<last name>") + " " + _("<first name>")), + ("german", "John Doe"), + ("english", "Doe, John"), + ("dutch", "Doe John"), + ) + }], + "notifications-select": ["django.forms.fields.MultipleChoiceField", { + "widget": "django.forms.CheckboxSelectMultiple", + "choices": get_notification_choices_lazy, + }], + "weekday_field": ["django.forms.fields.ChoiceField", { + 'widget': 'django.forms.Select', + "choices": i18n_day_name_choices_lazy + }], } CONSTANCE_CONFIG = { + "SITE_TITLE": ("AlekSIS", _("Site title"), "char_field"), "COLOUR_PRIMARY": ("#007bff", _("Primary colour")), "COLOUR_SECONDARY": ("#007bff", _("Secondary colour")), "MAIL_OUT_NAME": ("AlekSIS", _("Mail out name")), - "MAIL_OUT": ("aleksis@example.com", _("Mail out address"), "email_field"), + "MAIL_OUT": (DEFAULT_FROM_EMAIL, _("Mail out address"), "email_field"), "PRIVACY_URL": ("", _("Link to privacy policy"), "url_field"), "IMPRINT_URL": ("", _("Link to imprint"), "url_field"), + "ADRESSING_NAME_FORMAT": ("german", _("Name format of adresses"), "adressing-select"), + "NOTIFICATION_CHANNELS": (["email"], _("Channels to allow for notifications"), "notifications-select"), } CONSTANCE_CONFIG_FIELDSETS = { + "General settings": ("SITE_TITLE",), "Theme settings": ("COLOUR_PRIMARY", "COLOUR_SECONDARY"), "Mail settings": ("MAIL_OUT_NAME", "MAIL_OUT"), + "Notification settings": ("NOTIFICATION_CHANNELS", "ADRESSING_NAME_FORMAT"), "Footer settings": ("PRIVACY_URL", "IMPRINT_URL"), } +merge_app_settings("CONSTANCE_CONFIG", CONSTANCE_CONFIG, False) +merge_app_settings("CONSTANCE_CONFIG_FIELDSETS", CONSTANCE_CONFIG_FIELDSETS, False) + MAINTENANCE_MODE = _settings.get("maintenance.enabled", None) MAINTENANCE_MODE_IGNORE_IP_ADDRESSES = _settings.get( "maintenance.ignore_ips", _settings.get("maintenance.internal_ips", []) @@ -321,30 +371,43 @@ ANONYMIZE_ENABLED = _settings.get("maintenance.anonymisable", True) LOGIN_URL = "two_factor:login" if _settings.get("2fa.call.enabled", False): + if "two_factor.middleware.threadlocals.ThreadLocals" not in MIDDLEWARE: + MIDDLEWARE.insert( + MIDDLEWARE.index("django_otp.middleware.OTPMiddleware") + 1, + "two_factor.middleware.threadlocals.ThreadLocals", + ) TWO_FACTOR_CALL_GATEWAY = "two_factor.gateways.twilio.gateway.Twilio" if _settings.get("2fa.sms.enabled", False): + if "two_factor.middleware.threadlocals.ThreadLocals" not in MIDDLEWARE: + MIDDLEWARE.insert( + MIDDLEWARE.index("django_otp.middleware.OTPMiddleware") + 1, + "two_factor.middleware.threadlocals.ThreadLocals", + ) TWO_FACTOR_SMS_GATEWAY = "two_factor.gateways.twilio.gateway.Twilio" -if _settings.get("2fa.twilio.sid", None): - MIDDLEWARE.insert( - MIDDLEWARE.index("django_otp.middleware.OTPMiddleware") + 1, - "two_factor.middleware.threadlocals.ThreadLocals", - ) - TWILIO_SID = _settings.get("2fa.twilio.sid") - TWILIO_TOKEN = _settings.get("2fa.twilio.token") - TWILIO_CALLER_ID = _settings.get("2fa.twilio.callerid") +if _settings.get("twilio.sid", None): + TWILIO_SID = _settings.get("twilio.sid") + TWILIO_TOKEN = _settings.get("twilio.token") + TWILIO_CALLER_ID = _settings.get("twilio.callerid") + +if _settings.get("celery.enabled", False): + INSTALLED_APPS += ("django_celery_beat", "django_celery_results") + CELERY_BROKER_URL = "redis://localhost" + CELERY_RESULT_BACKEND = "django-db" + CELERY_CACHE_BACKEND = "django-cache" + CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" -_settings.populate_obj(sys.modules[__name__]) + if _settings.get("celery.email", False): + INSTALLED_APPS += ("djcelery_email",) + EMAIL_BACKEND = "djcelery_email.backends.CeleryEmailBackend" PWA_APP_NAME = "AlekSIS" # dbsettings PWA_APP_DESCRIPTION = "AlekSIS – The free school information system" # dbsettings -PWA_APP_THEME_COLOR = _settings.get("pwa.color", "#da1f3d") # dbsettings +PWA_APP_THEME_COLOR = lazy_config("COLOUR_PRIMARY") PWA_APP_BACKGROUND_COLOR = "#ffffff" PWA_APP_DISPLAY = "standalone" -PWA_APP_SCOPE = "/" PWA_APP_ORIENTATION = "any" -PWA_APP_START_URL = "/" PWA_APP_ICONS = [ # three icons to upload dbsettings {"src": STATIC_URL + "/icons/android_192.png", "sizes": "192x192"}, {"src": STATIC_URL + "/icons/android_512.png", "sizes": "512x512"}, @@ -361,6 +424,53 @@ PWA_APP_SPLASH_SCREEN = [ "media": "(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)", } ] -PWA_APP_DIR = "ltr" PWA_SERVICE_WORKER_PATH = os.path.join(STATIC_ROOT, "js", "serviceworker.js") -# PWA_APP_LANG = 'de-DE' + +CKEDITOR_CONFIGS = { + 'default': { + 'toolbar_Basic': [ + ['Source', '-', 'Bold', 'Italic'] + ], + 'toolbar_Full': [ + {'name': 'document', 'items': ['Source', '-', 'Save', 'NewPage', 'Preview', 'Print', '-', 'Templates']}, + {'name': 'clipboard', 'items': ['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo']}, + {'name': 'editing', 'items': ['Find', 'Replace', '-', 'SelectAll']}, + {'name': 'insert', + 'items': ['Image', 'Table', 'HorizontalRule', 'Smiley', 'SpecialChar', 'PageBreak', 'Iframe']}, + '/', + {'name': 'basicstyles', + 'items': ['Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat']}, + {'name': 'paragraph', + 'items': ['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Blockquote', 'CreateDiv', '-', + 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock', '-', 'BidiLtr', 'BidiRtl', + 'Language']}, + {'name': 'links', 'items': ['Link', 'Unlink', 'Anchor']}, + '/', + {'name': 'styles', 'items': ['Styles', 'Format', 'Font', 'FontSize']}, + {'name': 'colors', 'items': ['TextColor', 'BGColor']}, + {'name': 'tools', 'items': ['Maximize', 'ShowBlocks']}, + {'name': 'about', 'items': ['About']}, + {'name': 'customtools', 'items': [ + 'Preview', + 'Maximize', + ]}, + ], + 'toolbar': 'Full', + 'tabSpaces': 4, + 'extraPlugins': ','.join([ + 'uploadimage', + 'div', + 'autolink', + 'autoembed', + 'embedsemantic', + 'autogrow', + # 'devtools', + 'widget', + 'lineutils', + 'clipboard', + 'dialog', + 'dialogui', + 'elementspath' + ]), + } +} diff --git a/aleksis/core/static/common/helper.js b/aleksis/core/static/js/helper.js similarity index 81% rename from aleksis/core/static/common/helper.js rename to aleksis/core/static/js/helper.js index d68574fde82f6cbd94100f5ce2f2731d04d7ca68..844496346e451a89814ae90194db57fb67a72434 100644 --- a/aleksis/core/static/common/helper.js +++ b/aleksis/core/static/js/helper.js @@ -24,4 +24,7 @@ function getNowFormatted() { return formatDate(getNow()); } +function getJSONScript(elementId) { + return JSON.parse(document.getElementById(elementId).textContent); +} diff --git a/aleksis/core/static/js/main.js b/aleksis/core/static/js/main.js index c353ea6500ba0f57f414545f5bc59ae647b52ab4..f3439d2a573a93a094e189c142e0d9fc6ee41cf8 100644 --- a/aleksis/core/static/js/main.js +++ b/aleksis/core/static/js/main.js @@ -8,23 +8,23 @@ $(document).ready( function () { // Initialize datepicker [MAT] $('.datepicker').datepicker({ - format: 'dd.mm.yyyy', - // Translate to German + format: get_format('SHORT_DATE_FORMAT').toLowerCase().replace('d', 'dd').replace('m', 'mm').replace('y', 'yyyy'), + // Pull translations from Django helpers i18n: { - months: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'], - monthsShort: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'], - weekdays: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'], - weekdaysShort: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'], - weekdaysAbbrev: ['S', 'M', 'D', 'M', 'D', 'F', 'S'], + months: calendarweek_i18n.month_names, + monthsShort: calendarweek_i18n.month_abbrs, + weekdays: calendarweek_i18n.day_names, + weekdaysShort: calendarweek_i18n.day_abbrs, + weekdaysAbbrev: calendarweek_i18n.day_abbrs.map(([v])=> v), // Buttons - today: 'Heute', - cancel: 'Abbrechen', - done: 'OK', + today: gettext('Today'), + cancel: gettext('Cancel'), + done: gettext('OK'), }, // Set monday as first day of week - firstDay: 1, + firstDay: get_format('FIRST_DAY_OF_WEEK'), autoClose: true }); diff --git a/aleksis/core/static/style.scss b/aleksis/core/static/style.scss index 31f99faba276402136185af130993f7574a092a3..79fba591e419206d0e71065de062f236e23046a6 100644 --- a/aleksis/core/static/style.scss +++ b/aleksis/core/static/style.scss @@ -12,6 +12,30 @@ background-color: lighten($primary-color, 5%); } +.success { + @extend .light-green, .lighten-3 +} + +.success-text { + @extend .green-text; +} + +.warning { + @extend .orange, .lighten-2; +} + +.warning-text { + @extend .orange-text; +} + +.error { + @extend .red, .lighten-3; +} + +.error-text { + @extend .red-text; +} + // used in sidenav /**********/ @@ -312,13 +336,13 @@ table.striped > tbody > tr:nth-child(odd) { } .alert.success > p, .alert.success > div { - background-color: #c5e1a5; - border-color: #4caf50; + @extend .success; + border-color: color("green", "base"); } .alert.error > p, .alert.error > div { - background-color: #ef9a9a; - border-color: #b71c1c; + @extend .error; + border-color: color("red", "darken-4"); } .alert.primary > p, .alert.primary > div, .alert.info > p, .alert.info > div { @@ -327,8 +351,8 @@ table.striped > tbody > tr:nth-child(odd) { } .alert.warning p, .alert.warning div { - background-color: #ffb74d; - border-color: #e65100; + @extend .warning; + border-color: color("orange", "darken-4"); } main .alert p:first-child, main .alert div:first-child { diff --git a/aleksis/core/templates/403.html b/aleksis/core/templates/403.html index 85143a2c9309c07f591ea643ad9f0dca9d0338b5..07e4eb31b59ba6eb20d467407e12d84744a7a0b7 100644 --- a/aleksis/core/templates/403.html +++ b/aleksis/core/templates/403.html @@ -1,35 +1,32 @@ {% extends "core/base.html" %} {% load i18n %} -{% block page_title %} - {% blocktrans %}Forbidden{% endblocktrans %} -{% endblock %} {% block content %} - <div class="jumbotron jumbotron-fluid"> - <div class="container"> - <p class="lead"> - {{ exception|default:_('You are not allowed to access the requested page or object.') }} - </p> - <hr /> - <p> - {% blocktrans %} - If you think this is an error in AlekSIS, please contact your site - administrators. - {% endblocktrans %} - </p> - <ul> - {% for admin in ADMINS %} - <li> - {{ admin.0 }} - < - <a href="mailto:{{ admin.1 }}"> - {{ admin.1 }} - </a> - > - </li> - {% endfor %} - </ul> + <div class="container"> + <div class="card-panel red"> + <div class="card-content white-text"> + <div class="material-icons">error_outline</div> + <span class="card-title">{% blocktrans %}Error (403): You are not allowed to access the requested page or object.{% endblocktrans %}</span> + <p> + {% blocktrans %} + If you think this is an error in AlekSIS, please contact your site + administrators: + {% endblocktrans %} + </p> + <div class="card-caction"> + {% for admin in ADMINS %} + <li> + {{ admin.0 }} + < + <a href="mailto:{{ admin.1 }}"> + {{ admin.1 }} + </a> + > + </li> + {% endfor %} + </div> + </div> </div> </div> {% endblock %} diff --git a/aleksis/core/templates/404.html b/aleksis/core/templates/404.html index fdf4b4197cf387604b5cf81eb0d57f44a4752dd3..5b6ca7c9f07751fb4797c6841aed6428046163ac 100644 --- a/aleksis/core/templates/404.html +++ b/aleksis/core/templates/404.html @@ -1,39 +1,36 @@ {% extends "core/base.html" %} {% load i18n %} -{% block page_title %} - {% blocktrans %}Not found{% endblocktrans %} -{% endblock %} {% block content %} - <div class="jumbotron jumbotron-fluid"> - <div class="container"> - <p class="lead"> - {{ exception|default:_('The requested page or object was not found.') }} - </p> - <hr /> - <p> - {% blocktrans %} - If you were redirected by a link on an external page, - it is possible that that link was outdated. - {% endblocktrans %} - {% blocktrans %} - If you think this is an error in AlekSIS, please contact your site - administrators. - {% endblocktrans %} - </p> - <ul> - {% for admin in ADMINS %} - <li> - {{ admin.0 }} - < - <a href="mailto:{{ admin.1 }}"> - {{ admin.1 }} - </a> - > - </li> - {% endfor %} - </ul> + <div class="container"> + <div class="card-panel red"> + <div class="card-content white-text"> + <div class="material-icons">error_outline</div> + <span class="card-title">{% blocktrans %}Error (404): The requested page or object was not found.{% endblocktrans %}</span> + <p> + {% blocktrans %} + If you were redirected by a link on an external page, + it is possible that that link was outdated. + {% endblocktrans %} + {% blocktrans %} + If you think this is an error in AlekSIS, please contact your site + administrators: + {% endblocktrans %} + </p> + <div class="card-caction"> + {% for admin in ADMINS %} + <li> + {{ admin.0 }} + < + <a href="mailto:{{ admin.1 }}"> + {{ admin.1 }} + </a> + > + </li> + {% endfor %} + </div> + </div> </div> </div> {% endblock %} diff --git a/aleksis/core/templates/500.html b/aleksis/core/templates/500.html index 4d63630642d2a1d09d9227df189e65973ca261ed..185028e069d3faa51a5f1234a576377ecd507a41 100644 --- a/aleksis/core/templates/500.html +++ b/aleksis/core/templates/500.html @@ -1,25 +1,20 @@ {% extends "core/base.html" %} {% load i18n %} -{% block page_title %} - {% blocktrans %}Internal Server Error{% endblocktrans %} -{% endblock %} {% block content %} - <div class="jumbotron jumbotron-fluid"> - <div class="container"> - <p class="lead"> + <div class="container"> + <div class="card-panel red"> + <div class="card-content white-text"> + <div class="material-icons">error_outline</div> + <span class="card-title">{% blocktrans %}Error (500): An unexpected error has occured..{% endblocktrans %}</span> + <p> {% blocktrans %} - An unexpected error has occured. - {% endblocktrans %} - </p> - <hr /> - <p> - {% blocktrans %} - Your site administrators will automatically be notified about this + Your site administrators will automatically be notified about this error. - {% endblocktrans %} - </p> + {% endblocktrans %} + </p> + </div> </div> </div> {% endblock %} diff --git a/aleksis/core/templates/503.html b/aleksis/core/templates/503.html index ed7e3341e04732c85ffcb5840218df8452d900c7..7bc68a3159d1ab0dec9bd8685338a2a7e9a9049c 100644 --- a/aleksis/core/templates/503.html +++ b/aleksis/core/templates/503.html @@ -1,18 +1,31 @@ {% extends "core/base.html" %} {% load i18n %} -{% block page_title %} - {% blocktrans %}Maintenance mode{% endblocktrans %} -{% endblock %} {% block content %} - <div class="jumbotron jumbotron-fluid"> - <div class="container"> - <p class="lead"> - {% blocktrans %} - The maintenance mode is currently enabled. Please try again later. - {% endblocktrans %} - </p> + <div class="container"> + <div class="card-panel red"> + <div class="card-content white-text"> + <div class="material-icons">error_outline</div> + <span class="card-title">{% blocktrans %}The maintenance mode is currently enabled. Please try again later.{% endblocktrans %}</span> + <p> + {% blocktrans %} + This page is currently unavailable. If this error stays, contact your site administrators: + {% endblocktrans %} + </p> + <div class="card-caction"> + {% for admin in ADMINS %} + <li> + {{ admin.0 }} + < + <a href="mailto:{{ admin.1 }}"> + {{ admin.1 }} + </a> + > + </li> + {% endfor %} + </div> + </div> </div> </div> {% endblock %} diff --git a/aleksis/core/templates/core/base.html b/aleksis/core/templates/core/base.html index a8348011ed248378e8d515998aeaf4dba17dda6e..805e382d15572c30ea6593f89056030cf63ab967 100644 --- a/aleksis/core/templates/core/base.html +++ b/aleksis/core/templates/core/base.html @@ -12,7 +12,12 @@ <meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="description" content="{% blocktrans %}AlekSIS is a web-based school information system (SIS) which can be used to manage and/or publish organisational data of educational in titutions.{% endblocktrans %}"> - <title>{% blocktrans %}AlekSIS - School Information System{% endblocktrans %}</title> + <title> + {% block no_browser_title %} + {% block browser_title %}{% endblock %} — + {% endblock %} + {{ config.SITE_TITLE }} + </title> {# Favicons #} <link href="{% static "icons/favicon_16.png" %}" rel="icon" type="image/png" sizes="16x16"> @@ -26,8 +31,19 @@ {% include_css "material-design-icons" %} <link rel="stylesheet" href="{% sass_src 'style.scss' %}"> + {# Add JS URL resolver #} + <script src="{% url "js_reverse" %}" type="text/javascript"></script> + + {# Add i18n names for calendar (for use in datepicker) #} + {# Passing the locale is not necessary for the scripts to work, but prevents caching issues #} + {% get_current_language as LANGUAGE_CODE %} + <script src="{% url "javascript-catalog" %}?locale={{ LANGUAGE_CODE }}" type="text/javascript"></script> + <script src="{% url "calendarweek_i18n_js" %}?first_day=6&locale={{ LANGUAGE_CODE }}" type="text/javascript"></script> + {# Include jQuery to provide $(document).ready #} {% include_js "jQuery" %} + + {% block extra_head %}{% endblock %} </head> <body id="body"> @@ -41,7 +57,7 @@ <!-- Nav bar (logged in as, logout) --> <nav> - <a class="brand-logo" href="/">AlekSIS</a> + <a class="brand-logo" href="/">{{ config.SITE_TITLE }}</a> <div class="nav-wrapper"> <ul id="nav-mobile" class="right hide-on-med-and-down"> @@ -91,7 +107,9 @@ {% endfor %} {% endif %} - <h4>{% block page_title %}{% endblock %}</h4> + {% block no_page_title %} + <h4>{% block page_title %}{% endblock %}</h4> + {% endblock %} {% block content %}{% endblock %} </main> diff --git a/aleksis/core/templates/core/data_management.html b/aleksis/core/templates/core/data_management.html index 03e6c14b4077b86bad27f83e73d7a59c36316142..53293ffd76e9a6d07251033c356bcae9cc2ee36f 100644 --- a/aleksis/core/templates/core/data_management.html +++ b/aleksis/core/templates/core/data_management.html @@ -3,6 +3,7 @@ {% load i18n menu_generator %} +{% block browser_title %}{% blocktrans %}Data management{% endblocktrans%}{% endblock %} {% block page_title %}{% blocktrans %}Data management{% endblocktrans %}{% endblock %} {% block content %} diff --git a/aleksis/core/templates/core/edit_group.html b/aleksis/core/templates/core/edit_group.html index d91ed1874c3c3acb26fd50dc492a9a924eaa4aa3..e7b3188076125020958105ee97ab0de00bd5f766 100644 --- a/aleksis/core/templates/core/edit_group.html +++ b/aleksis/core/templates/core/edit_group.html @@ -3,6 +3,7 @@ {% extends "core/base.html" %} {% load material_form i18n %} +{% block browser_title %}{% blocktrans %}Edit group{% endblocktrans %}{% endblock %} {% block page_title %}{% blocktrans %}Edit group{% endblocktrans %}{% endblock %} {% block content %} diff --git a/aleksis/core/templates/core/edit_person.html b/aleksis/core/templates/core/edit_person.html index 359b2c3f5f6a438f61be26f22130026165b61e39..8a5d0ca39a8fa0cbfd437519a09723ce59278c2d 100644 --- a/aleksis/core/templates/core/edit_person.html +++ b/aleksis/core/templates/core/edit_person.html @@ -5,6 +5,7 @@ {% load material_form i18n %} +{% block browser_title %}{% blocktrans %}Edit person{% endblocktrans %}{% endblock %} {% block page_title %}{% blocktrans %}Edit person{% endblocktrans %}{% endblock %} diff --git a/aleksis/core/templates/core/edit_school.html b/aleksis/core/templates/core/edit_school.html index d44ebc875eef6e083b3af7f075969d6cf93c7b67..11eeaf0016bc2a1979602abda5f7e45679f2dddf 100644 --- a/aleksis/core/templates/core/edit_school.html +++ b/aleksis/core/templates/core/edit_school.html @@ -5,6 +5,7 @@ {% load material_form i18n %} +{% block browser_title %}{% blocktrans %}Edit school{% endblocktrans %}{% endblock %} {% block page_title %}{% blocktrans %}Edit school{% endblocktrans %}{% endblock %} diff --git a/aleksis/core/templates/core/edit_schoolterm.html b/aleksis/core/templates/core/edit_schoolterm.html index 23eedf778b9cbe3555a86a5fb67e82ab9595f137..8b5c0b98bc73a5476d99c2d504b698a7bc98a652 100644 --- a/aleksis/core/templates/core/edit_schoolterm.html +++ b/aleksis/core/templates/core/edit_schoolterm.html @@ -5,6 +5,7 @@ {% load material_form i18n %} +{% block browser_title %}{% blocktrans %}Edit school term{% endblocktrans %}{% endblock %} {% block page_title %}{% blocktrans %}Edit school term{% endblocktrans %}{% endblock %} {% block content %} diff --git a/aleksis/core/templates/core/group_full.html b/aleksis/core/templates/core/group_full.html index 12765668c6f6dc5dd893d8576ce1be7a6d8889b2..2b1b9ae3fa10c9e4dfeddb8c151ddd4a7e98d029 100644 --- a/aleksis/core/templates/core/group_full.html +++ b/aleksis/core/templates/core/group_full.html @@ -5,6 +5,8 @@ {% load i18n static %} {% load render_table from django_tables2 %} +{% block browser_title %}{{ group.name }}{% endblock %} + {% block content %} <h4>{{ group.name }} <small class="grey-text">{{ group.short_name }}</small></h4> <p> diff --git a/aleksis/core/templates/core/groups.html b/aleksis/core/templates/core/groups.html index 1dd7c744579260a686a1b6218964f33be608107a..fab23516a458f55347ebcc152ee6ffac20e004d5 100644 --- a/aleksis/core/templates/core/groups.html +++ b/aleksis/core/templates/core/groups.html @@ -5,6 +5,7 @@ {% load i18n %} {% load render_table from django_tables2 %} +{% block browser_title %}{% blocktrans %}Groups{% endblocktrans %}{% endblock %} {% block page_title %}{% blocktrans %}Groups{% endblocktrans %}{% endblock %} {% block content %} diff --git a/aleksis/core/templates/core/index.html b/aleksis/core/templates/core/index.html index d43238d580f2ee1899e49ffe4aca8140d349efa7..ca16b84b3cce1d70ace1f5eab3ec077701527b4c 100644 --- a/aleksis/core/templates/core/index.html +++ b/aleksis/core/templates/core/index.html @@ -1,6 +1,8 @@ {% extends 'core/base.html' %} {% load i18n %} +{% block browser_title %}{% blocktrans %}Home{% endblocktrans %}{% endblock %} + {% block content %} <div id="dashboard"> <p class="flow-text">{% blocktrans %}AlekSIS (School Information System){% endblocktrans %}</p> @@ -46,6 +48,40 @@ <p>{% blocktrans %}No activities available yet.{% endblocktrans %}</p> {% endif %} </div> + </div> + {% endfor %} + + <div class="row"> + {% for widget in widgets %} + <div class="col s12 m12 l6 xl4"> + {{ widget }} + </div> + {% endfor %} + </div> + + <div class="row"> + <div class="col s12 m6"> + <h5>{% blocktrans %}Last activities{% endblocktrans %}</h5> + + {% if activities %} + <ul class="collection"> + {% for activity in activities %} + <li class="collection-item"> + <span class="badge new primary-color">{{ activity.app }}</span> + <span class="title">{{ activity.title }}</span> + <p> + <i class="material-icons left">access_time</i> {{ activity.created_at }} + </p> + <p> + {{ activity.description }} + </p> + </li> + {% endfor %} + </ul> + {% else %} + <p>{% blocktrans %}No activities available yet.{% endblocktrans %}</p> + {% endif %} + </div> <div class="col s12 m6"> <h5>{% blocktrans %}Recent notifications{% endblocktrans %}</h5> diff --git a/aleksis/core/templates/core/person_full.html b/aleksis/core/templates/core/person_full.html index 43eca91f431f589667cdc49dc7cda367fc0afeb9..8f9ceed60aa7ca1a99bde9e3682f5988d2f29f75 100644 --- a/aleksis/core/templates/core/person_full.html +++ b/aleksis/core/templates/core/person_full.html @@ -5,6 +5,7 @@ {% load i18n static cropping %} {% load render_table from django_tables2 %} +{% block browser_title %}{{ person.first_name }} {{ person.last_name }}{% endblock %} {% block content %} <h4>{{ person.first_name }} {{ person.last_name }}</h4> diff --git a/aleksis/core/templates/core/persons.html b/aleksis/core/templates/core/persons.html index ea9d433c49a9f580457efdcdf6f502f84ce154a8..dfecfb7c52b64cbdf49e70d06d1148e82128577f 100644 --- a/aleksis/core/templates/core/persons.html +++ b/aleksis/core/templates/core/persons.html @@ -5,6 +5,7 @@ {% load i18n %} {% load render_table from django_tables2 %} +{% block browser_title %}{% blocktrans %}Persons{% endblocktrans %}{% endblock %} {% block page_title %}{% blocktrans %}Persons{% endblocktrans %}{% endblock %} {% block content %} diff --git a/aleksis/core/templates/core/persons_accounts.html b/aleksis/core/templates/core/persons_accounts.html index 98fa121203532b08b440ee0df4c97a9e9f1a7ab7..672b089aaa949560e958d829597e1b7abd986265 100644 --- a/aleksis/core/templates/core/persons_accounts.html +++ b/aleksis/core/templates/core/persons_accounts.html @@ -4,6 +4,7 @@ {% load i18n %} +{% block browser_title %}{% blocktrans %}Link persons to accounts{% endblocktrans %}{% endblock %} {% block page_title %} {% blocktrans %}Link persons to accounts{% endblocktrans %} {% endblock %} diff --git a/aleksis/core/templates/core/save_button.html b/aleksis/core/templates/core/save_button.html index d0b5cf16bb96800f09f961bbf5ea47b533da7f70..ebc48f2db3d4529f292e3352dbf0cd45ccec2868 100644 --- a/aleksis/core/templates/core/save_button.html +++ b/aleksis/core/templates/core/save_button.html @@ -1,4 +1,5 @@ {% load i18n %} <button type="submit" class="btn waves-effect waves-light green"> - <i class="material-icons left">save</i> {% trans "Save" %} + {% trans "Save" as default %} + <i class="material-icons left">{{ icon|default:"save" }}</i> {{ caption|default:default }} </button> diff --git a/aleksis/core/templates/core/school_management.html b/aleksis/core/templates/core/school_management.html index bfba2af5cd4fbe7944c71c1fe13014f28dfc0ef7..10fa00621866199f18877f8836d4e762efc474e4 100644 --- a/aleksis/core/templates/core/school_management.html +++ b/aleksis/core/templates/core/school_management.html @@ -3,6 +3,7 @@ {% load i18n menu_generator %} +{% block browser_title %}{% blocktrans %}School management{% endblocktrans %}{% endblock %} {% block page_title %}{% blocktrans %}School management{% endblocktrans %}{% endblock %} {% block content %} diff --git a/aleksis/core/templates/core/system_status.html b/aleksis/core/templates/core/system_status.html index 9f74944e462bb97edac0d72c1acc7d7e38201f9e..2b0e80732538d250ac9e35902b4d7363160bc6ea 100644 --- a/aleksis/core/templates/core/system_status.html +++ b/aleksis/core/templates/core/system_status.html @@ -2,7 +2,7 @@ {% extends "core/base.html" %} {% load i18n %} -{#{% block bootstrap4_title %}{% blocktrans %}System status{% endblocktrans %} - {{ block.super }}{% endblock %}#} +{% block browser_title %}{% blocktrans %}System status{% endblocktrans %}{% endblock %} {% block page_title %}{% blocktrans %}System status{% endblocktrans %}{% endblock %} diff --git a/aleksis/core/templates/core/turnable.html b/aleksis/core/templates/core/turnable.html index d4db0cd039939be07844f9e9c8d6116702be3352..e122b2fa4396282fe0941376161fe3086cda4deb 100644 --- a/aleksis/core/templates/core/turnable.html +++ b/aleksis/core/templates/core/turnable.html @@ -1,26 +1,10 @@ {# -*- engine:django -*- #} -{% extends "core/base.html" %} - - -{% block content %} - - <div class="d-flex justify-content-between"> - <div> - <h2>{{ current_head }}</h2> - </div> - <div class="btn-group" role="group" aria-label="URL actions"> - <a href="{{ url_prev }}" class="btn btn-dark"> - <i class="material-icons">chevron_left</i> - </a> - <a href="{{ url_next }}" class="btn btn-dark"> - <i class="material-icons">chevron_right</i> - </a> - </div> - </div> - - {% block current_content %} - - {% endblock %} - -{% endblock %} +<div class="row"> + <a href="{{ url_prev }}" class="btn-flat left"> + <i class="material-icons small">chevron_left</i> + </a> + <a href="{{ url_next }}" class="btn-flat right"> + <i class="material-icons small">chevron_right</i> + </a> +</div> diff --git a/aleksis/core/templates/sms/notification.txt b/aleksis/core/templates/sms/notification.txt new file mode 100644 index 0000000000000000000000000000000000000000..db06b797ee8b649a9357fa287591f802177af054 --- /dev/null +++ b/aleksis/core/templates/sms/notification.txt @@ -0,0 +1,6 @@ +{% load i18n %} +🔔 {{ notification.title }} + +{{ notification.description }} + +{{ notification.sender }}{% if notification.link %} · {% endif %}{{ notification.link }} diff --git a/aleksis/core/templates/templated_email/notification.email b/aleksis/core/templates/templated_email/notification.email new file mode 100644 index 0000000000000000000000000000000000000000..bb39f5861876b1dc1c4c39639de649ec6d6590eb --- /dev/null +++ b/aleksis/core/templates/templated_email/notification.email @@ -0,0 +1,22 @@ +{% load i18n %} + +{% block subject %} {% trans "New notification for" %} {{ notification_user }} {% endblock %} + +{% block html %} +<main> + <p>{% trans "Dear" %} {{ notification_user }}, <br> + {% trans "we got a new notification for you:" %}</p> + <blockquote> + <p>{{ notification.description }}</p> + {% if notification.link %} + <a href="{{ notification.link }}">{% trans "More information" %} →</a> + {% endif %} + </blockquote> + + {% blocktrans with trans_sender=notification.sender trans_created_at=notification.created_at %} + <p>By {{ trans_sender }} at {{ trans_created_at }}</p> + + <i>Your AlekSIS team</i> + {% endblocktrans %} +</main> +{% endblock %} diff --git a/aleksis/core/templatetags/templatetags/msg_box.py b/aleksis/core/templatetags/msg_box.py similarity index 100% rename from aleksis/core/templatetags/templatetags/msg_box.py rename to aleksis/core/templatetags/msg_box.py diff --git a/aleksis/core/templatetags/templatetags/__init__.py b/aleksis/core/templatetags/templatetags/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/aleksis/core/templatetags/templatetags/copy_filter.py b/aleksis/core/templatetags/templatetags/copy_filter.py deleted file mode 100644 index 9ee6181b430174b1f8dec5eff3276531f7e7e44b..0000000000000000000000000000000000000000 --- a/aleksis/core/templatetags/templatetags/copy_filter.py +++ /dev/null @@ -1,8 +0,0 @@ -import copy as copylib - -from django import template - -register = template.Library() - -register.filter("copy", copylib.copy) -register.filter("deepcopy", copylib.deepcopy) diff --git a/aleksis/core/templatetags/templatetags/tex.py b/aleksis/core/templatetags/templatetags/tex.py deleted file mode 100644 index 333c3d31d819eb4c3b41ef6513d3b2b9df3ff30e..0000000000000000000000000000000000000000 --- a/aleksis/core/templatetags/templatetags/tex.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Django filters, needed when creating LaTeX files with the django template language - -Written by Rocco Schulz (http://is-gr8.com/), modified by SchoolApps-Team -""" -from django.template.defaultfilters import stringfilter, register -from django.template.loader import render_to_string - - -@register.filter -@stringfilter -def brackets(value): - """ - surrounds the value with { } - You have to use this filter whenever you would need something like - {{{ field }}} in a template. - """ - return "{%s}" % value - - -REPLACEMENTS = { - "§": "\\textsection{}", - "$": "\\textdollar{}", - "LaTeX": "\\LaTeX \\ ", - " TeX": " \\TeX \\ ", - "€": "\\euro", - ">": "$>$", - "<": "$<$" -} - -ESCAPES = ("&", "{", "}", "%") - - -@register.filter -@stringfilter -def texify(value): - """ - escapes/replaces special character with appropriate latex commands - """ - tex_value = [] - - # escape special symbols - for char in value: - tex_value.append("%s" % ("\\%s" % char if char in ESCAPES else char)) - tex_value = "".join(tex_value) - - # replace symbols / words with latex commands - for key, value in REPLACEMENTS.items(): - tex_value = tex_value.replace(key, value) - - return "%s" % tex_value diff --git a/aleksis/core/templatetags/templatetags/url_name.py b/aleksis/core/templatetags/templatetags/url_name.py deleted file mode 100644 index 20f63c673b00f0d736652db62937f9dc68947db2..0000000000000000000000000000000000000000 --- a/aleksis/core/templatetags/templatetags/url_name.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.urls import resolve -from django import template - -register = template.Library() - - -def get_url_name(request): # Only one argument. - """Gets url_name""" - try: - return resolve(request.path_info).url_name - except Exception as e: - return e - - -register.filter("url_name", get_url_name) diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index 2229a5dbfd731a1124ed10944d496c8a9527e785..63929088ba38406d0e560fcaee218404ad1462bf 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -4,8 +4,11 @@ from django.conf.urls.static import static from django.contrib import admin from django.contrib.auth import views as auth_views from django.urls import include, path +from django.views.i18n import JavaScriptCatalog +import calendarweek.django import debug_toolbar +from django_js_reverse.views import urls_js from two_factor.urls import urlpatterns as tf_urls from . import views @@ -36,12 +39,18 @@ urlpatterns = [ path("impersonate/", include("impersonate.urls")), path("__i18n__/", include("django.conf.urls.i18n")), path("select2/", include("django_select2.urls")), + path("jsreverse.js", urls_js, name='js_reverse'), + path("calendarweek_i18n.js", calendarweek.django.i18n_js, name="calendarweek_i18n_js"), + path('gettext.js', JavaScriptCatalog.as_view(), name='javascript-catalog'), ] # Serve static files from STATIC_ROOT to make it work with runserver # collectstatic is also required in development for this urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) +# Serve media files from MEDIA_ROOT to make it work with runserver +urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + # Add URLs for optional features if hasattr(settings, "TWILIO_ACCOUNT_SID"): from two_factor.gateways.twilio.urls import urlpatterns as tf_twilio_urls # noqa diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py index 4fb88fa49e5f6b12f5b22357ee39d9896eb36466..bc0700b03b8c755220b5bedfd609aa5d810a86d1 100644 --- a/aleksis/core/util/core_helpers.py +++ b/aleksis/core/util/core_helpers.py @@ -1,10 +1,13 @@ +import os import pkgutil from importlib import import_module -from typing import Sequence, Union +from typing import Any, Callable, Sequence, Union +from uuid import uuid4 from django.conf import settings from django.db.models import Model from django.http import HttpRequest +from django.utils.functional import lazy def dt_show_toolbar(request: HttpRequest) -> bool: @@ -30,19 +33,55 @@ def get_app_packages() -> Sequence[str]: except ImportError: return [] - pkgs = [] - for pkg in pkgutil.iter_modules(aleksis.apps.__path__): - mod = import_module("aleksis.apps.%s" % pkg[1]) + return ["aleksis.apps.%s" % pkg[1] for pkg in pkgutil.iter_modules(aleksis.apps.__path__)] - # Add additional apps defined in module's INSTALLED_APPS constant - additional_apps = getattr(mod, "INSTALLED_APPS", []) - for app in additional_apps: - if app not in pkgs: - pkgs.append(app) - pkgs.append("aleksis.apps.%s" % pkg[1]) +def merge_app_settings(setting: str, original: Union[dict, list], deduplicate: bool = False) -> Union[dict, list]: + """ Get a named settings constant from all apps and merge it into the original. + To use this, add a settings.py file to the app, in the same format as Django's + main settings.py. - return pkgs + Note: Only selected names will be imported frm it to minimise impact of + potentially malicious apps! + """ + + for pkg in get_app_packages(): + try: + mod_settings = import_module(pkg + ".settings") + except ImportError: + # Import errors are non-fatal. They mean that the app has no settings.py. + continue + + app_setting = getattr(mod_settings, setting, None) + if not app_setting: + # The app might not have this setting or it might be empty. Ignore it in that case. + continue + + for entry in app_setting: + if entry in original: + if not deduplicate: + raise AttributeError("%s already set in original." % entry) + else: + if isinstance(original, list): + original.append(entry) + elif isinstance(original, dict): + original[entry] = app_setting[entry] + else: + raise TypeError("Only dict and list settings can be merged.") + + +def lazy_config(key: str) -> Callable[[str], Any]: + """ Lazily get a config value from constance. Useful to bind constance + configs to other global settings to make them available to third-party + apps that are not aware of constance. + """ + + def _get_config(key: str) -> Any: + from constance import config # noqa + return getattr(config, key) + + # The type is guessed from the default value to improve lazy()'s behaviour + return lazy(_get_config, type(settings.CONSTANCE_CONFIG[key][0]))(key) def is_impersonate(request: HttpRequest) -> bool: @@ -65,3 +104,38 @@ def has_person(obj: Union[HttpRequest, Model]) -> bool: return False return getattr(obj, "person", None) is not None + + +def celery_optional(orig: Callable) -> Callable: + """ Decorator that makes Celery optional for a function. + + If Celery is configured and available, it wraps the function in a Task + and calls its delay method when invoked; if not, it leaves it untouched + and it is executed synchronously. + """ + + def wrapped(*args, **kwargs): + if hasattr(settings, "CELERY_RESULT_BACKEND"): + from ..celery import app # noqa + task = app.task(orig) + + task.delay(*args, **kwargs) + else: + orig(*args, **kwargs) + + return wrapped + + +def path_and_rename(instance, filename: str, upload_to: str = "files") -> str: + """ Updates path of an uploaded file and renames it to a random UUID in Django FileField """ + + _, ext = os.path.splitext(filename) + + # set filename as random string + new_filename = '{}.{}'.format(uuid4().hex, ext) + + # Create upload directory if necessary + os.makedirs(os.path.join(settings.MEDIA_ROOT, upload_to), exist_ok=True) + + # return the whole path to the file + return os.path.join(upload_to, new_filename) diff --git a/aleksis/core/util/helper.py b/aleksis/core/util/helper.py deleted file mode 100644 index 04b910255cffad5db90f9c4077b9016a21ae5b82..0000000000000000000000000000000000000000 --- a/aleksis/core/util/helper.py +++ /dev/null @@ -1,22 +0,0 @@ -import os -from uuid import uuid4 - -from django.template.loader_tags import register - - -def path_and_rename(instance, filename): - upload_to = 'menus' - ext = filename.split('.')[-1].lower() - # get filename - if instance.pk: - filename = '{}.{}'.format(instance.pk, ext) - else: - # set filename as random string - filename = '{}.{}'.format(uuid4().hex, ext) - # return the whole path to the file - return os.path.join(upload_to, filename) - - -@register.inclusion_tag("components/msgbox.html") -def msg_box(msg, status="success", icon="info"): - return {"msg": msg, "status": status, "icon": icon} diff --git a/aleksis/core/util/network.py b/aleksis/core/util/network.py index d48970c15e8861ac0f077863e90b54c8434b7299..aa305ba160aa5034f05e53c24cd867c40ca65056 100644 --- a/aleksis/core/util/network.py +++ b/aleksis/core/util/network.py @@ -18,7 +18,8 @@ def get_newest_articles(domain: str = WP_DOMAIN, author_whitelist: list = None, author_blacklist: list = None, category_whitelist: list = None, - category_blacklist: list = None + category_blacklist: list = None, + filter_vs_composer: bool = False, ): """ This function returns the newest articles/posts of a WordPress site. @@ -29,6 +30,7 @@ def get_newest_articles(domain: str = WP_DOMAIN, :param author_blacklist: If the author's id (an integer) is in this list, the article won't be shown :param category_whitelist: If this list is filled, only articles which are in one of this categories will be shown :param category_blacklist: If the category's id (an integer) is in this list, the article won't be shown + :param filter_vs_composer: Remove unnecessary Visual Composer Tags :return: a list of the newest posts/articles """ # Make mutable default arguments unmutable @@ -68,7 +70,7 @@ def get_newest_articles(domain: str = WP_DOMAIN, image_url: str = "" # Replace VS composer tags if activated - if settings.latest_article_settings.replace_vs_composer_stuff: + if filter_vs_composer: excerpt = VS_COMPOSER_REGEX.sub("", post["excerpt"]["rendered"]) else: excerpt = post["excerpt"]["rendered"] @@ -81,7 +83,7 @@ def get_newest_articles(domain: str = WP_DOMAIN, "image_url": image_url, } ) - if len(posts) >= limit and limit >= 0: + if len(posts) >= limit >= 0: break return posts diff --git a/aleksis/core/util/notifications.py b/aleksis/core/util/notifications.py new file mode 100644 index 0000000000000000000000000000000000000000..5f6f8fded9a00323b2695e27268d4d4a744c0525 --- /dev/null +++ b/aleksis/core/util/notifications.py @@ -0,0 +1,107 @@ +""" Utility code for notification system """ + +from typing import Sequence, Union + +from django.apps import apps +from django.conf import settings +from django.template.loader import get_template +from django.utils.functional import lazy +from django.utils.translation import gettext_lazy as _ + +from templated_email import send_templated_mail + +try: + from twilio.rest import Client as TwilioClient +except ImportError: + TwilioClient = None + +from .core_helpers import celery_optional, lazy_config + + +def send_templated_sms( + template_name: str, from_number: str, recipient_list: Sequence[str], context: dict +) -> None: + """ Render a plan-text template and send via SMS to all recipients. """ + + template = get_template(template_name) + text = template.render(context) + + client = TwilioClient(settings.TWILIO_SID, settings.TWILIO_TOKEN) + for recipient in recipient_list: + client.messages.create(body=text, to=recipient, from_=from_number) + + +def _send_notification_email(notification: "Notification", template: str = "notification") -> None: + context = { + "notification": notification, + "notification_user": notification.recipient.adressing_name, + } + send_templated_mail( + template_name=template, + from_email=lazy_config("MAIL_OUT"), + recipient_list=[notification.recipient.email], + context=context, + ) + + +def _send_notification_sms( + notification: "Notification", template: str = "sms/notification.txt" +) -> None: + context = { + "notification": notification, + "notification_user": notification.recipient.adressing_name, + } + send_templated_sms( + template_name=template, + from_number=settings.TWILIO_CALLER_ID, + recipient_list=[notification.recipient.mobile_number.as_e164], + context=context, + ) + + +# Mapping of channel id to name and two functions: +# - Check for availability +# - Send notification through it +_CHANNELS_MAP = { + "email": (_("E-Mail"), lambda: lazy_config("MAIL_OUT"), _send_notification_email), + "sms": (_("SMS"), lambda: getattr(settings, "TWILIO_SID", None), _send_notification_sms), +} + + +@celery_optional +def send_notification(notification: Union[int, "Notification"], resend: bool = False) -> None: + """ Send a notification through enabled channels. + + If resend is passed as True, the notification is sent even if it was + previously marked as sent. + """ + + channels = lazy_config("NOTIFICATION_CHANNELS") + + if isinstance(notification, int): + Notification = apps.get_model("core", "Notification") + notification = Notification.objects.get(pk=notification) + + if resend or not notification.sent: + for channel in channels: + name, check, send = _CHANNELS_MAP[channel] + if check(): + send(notification) + + +def get_notification_choices() -> list: + """ Return all available channels for notifications. + + This gathers the channels that are technically available as per the + system configuration. Which ones are available to users is defined + by the administrator (by selecting a subset of these choices). + """ + + choices = [] + for channel, (name, check, send) in _CHANNELS_MAP.items(): + if check(): + choices.append((channel, name)) + return choices + + +get_notification_choices_lazy = lazy(get_notification_choices, tuple) diff --git a/aleksis/core/views.py b/aleksis/core/views.py index e53318af912be80e42976a576c4033f6e4e19b36..09b23bb787bcd5c3bf184423431ba3193acfec21 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -20,6 +20,8 @@ from .models import Activity, Group, Notification, Person, School from .tables import GroupsTable, PersonsTable from .util import messages, network +from aleksis.apps.dashboardfeeds.views import get_widgets + @person_required def index(request: HttpRequest) -> HttpResponse: @@ -33,6 +35,8 @@ def index(request: HttpRequest) -> HttpResponse: context["notifications"] = notifications context["unread_notifications"] = unread_notifications + context["widgets"] = get_widgets(request) + return render(request, "core/index.html", context) diff --git a/backup_my_work_to_change_branches_I_did__-_model_for_feeds_-_show_feeds_on_dashboard_backu.patch b/backup_my_work_to_change_branches_I_did__-_model_for_feeds_-_show_feeds_on_dashboard_backu.patch new file mode 100644 index 0000000000000000000000000000000000000000..af3fe49c9f4af6ed5da4af4ef38da528c9bfa580 --- /dev/null +++ b/backup_my_work_to_change_branches_I_did__-_model_for_feeds_-_show_feeds_on_dashboard_backu.patch @@ -0,0 +1,3040 @@ +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/.gitignore +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/.gitignore (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/.gitignore (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) +@@ -0,0 +1,74 @@ ++# Byte-compiled / optimized / DLL files ++*$py.class ++*.py[cod] ++__pycache__/ ++ ++# Distribution / packaging ++*.egg ++*.egg-info/ ++.Python ++.eggs/ ++.installed.cfg ++build/ ++develop-eggs/ ++dist/ ++downloads/ ++eggs/ ++lib/ ++lib64/ ++parts/ ++sdist/ ++var/ ++wheels/ ++ ++# Installer logs ++pip-delete-this-directory.txt ++pip-log.txt ++ ++# Translations ++*.mo ++*.pot ++ ++# Django stuff: ++*.log ++local_settings.py ++ ++# pyenv ++.python-version ++ ++# Environments ++.env ++.venv ++ENV/ ++env/ ++venv/ ++ ++# Editors ++*~ ++DEADJOE ++\#*# ++ ++# IntelliJ ++.idea ++.idea/ ++ ++# Database ++db.sqlite3 ++ ++# Sphinx ++docs/_build/ ++ ++# TeX ++*.aux ++ ++# Generated files ++aleksis/node_modules/ ++aleksis/static/ ++ ++.coverage ++.mypy_cache/ ++.tox/ ++htmlcov/ ++maintenance_mode_state.txt ++media/ ++package-lock.json +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/CHANGELOG.rst +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/CHANGELOG.rst (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/CHANGELOG.rst (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) +@@ -0,0 +1,22 @@ ++Changelog ++========= ++ ++`1.0`_ ++------ ++ ++New features ++~~~~~~~~~~~~ ++ ++* Initial release ++ ++Bugfixes ++~~~~~~~~ ++ ++* None ++ ++Minor changes ++~~~~~~~~~~~~~ ++ ++* None ++ ++_`1.0`: https://edugit.org/Teckids/AlekSIS/AlekSIS-App-DashboardFeeds/-/tags/1.0 +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/LICENCE.rst +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/LICENCE.rst (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/LICENCE.rst (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) +@@ -0,0 +1,318 @@ ++====================================== ++ EUROPEAN UNION PUBLIC LICENCE v. 1.2 ++====================================== ++-------------------------------------- ++ EUPL © the European Union 2007, 2016 ++-------------------------------------- ++ ++This European Union Public Licence (the ‘EUPL’) applies to the Work ++(as defined below) which is provided under the terms of this Licence. ++Any use of the Work, other than as authorised under this Licence is ++prohibited (to the extent such use is covered by a right of the ++copyright holder of the Work). ++ ++The Work is provided under the terms of this Licence when the Licensor ++(as defined below) has placed the following notice immediately following ++the copyright notice for the Work: ++ ++ Licensed under the EUPL ++ ++or has expressed by any other means his willingness to license under ++the EUPL. ++ ++1. Definitions ++============== ++ ++In this Licence, the following terms have the following meaning: ++ ++* ‘The Licence’: this Licence. ++* ‘The Original Work’: the work or software distributed or communicated ++ by the Licensor under this Licence, available as Source Code and also ++ as Executable Code as the case may be. ++* ‘Derivative Works’: the works or software that could be created by the ++ Licensee, based upon the Original Work or modifications thereof. This ++ Licence does not define the extent of modification or dependence on ++ the Original Work required in order to classify a work as a Derivative ++ Work; this extent is determined by copyright law applicable in the ++ country mentioned in Article 15. ++* ‘The Work’: the Original Work or its Derivative Works. ++* ‘The Source Code’: the human-readable form of the Work which is the ++ most convenient for people to study and modify. ++* ‘The Executable Code’: any code which has generally been compiled and ++ which is meant to be interpreted by a computer as a program. ++* ‘The Licensor’: the natural or legal person that distributes or ++ communicates the Work under the Licence. ++* ‘Contributor(s)’: any natural or legal person who modifies the Work ++ under the Licence, or otherwise contributes to the creation of a ++ Derivative Work. ++* ‘The Licensee’ or ‘You’: any natural or legal person who makes any ++ usage of the Work under the terms of the Licence. ++* ‘Distribution’ or ‘Communication’: any act of selling, giving, ++ lending, renting, distributing, communicating, transmitting, or ++ otherwise making available, online or offline, copies of the Work or ++ providing access to its essential functionalities at the disposal of ++ any other natural or legal person. ++ ++2. Scope of the rights granted by the Licence ++============================================= ++ ++The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, ++sublicensable licence to do the following, for the duration of copyright ++vested in the Original Work: ++ ++* use the Work in any circumstance and for all usage, ++* reproduce the Work, ++* modify the Work, and make Derivative Works based upon the Work, ++* communicate to the public, including the right to make available or ++ display the Work or copies thereof to the public and perform publicly, ++ as the case may be, the Work, ++* distribute the Work or copies thereof, ++* lend and rent the Work or copies thereof, ++* sublicense rights in the Work or copies thereof. ++ ++Those rights can be exercised on any media, supports and formats, ++whether now known or later invented, as far as the applicable law ++permits so. ++ ++In the countries where moral rights apply, the Licensor waives his right ++to exercise his moral right to the extent allowed by law in order to ++make effective the licence of the economic rights here above listed. ++ ++The Licensor grants to the Licensee royalty-free, non-exclusive usage ++rights to any patents held by the Licensor, to the extent necessary to ++make use of the rights granted on the Work under this Licence. ++ ++3. Communication of the Source Code ++=================================== ++ ++The Licensor may provide the Work either in its Source Code form, or as ++Executable Code. If the Work is provided as Executable Code, the ++Licensor provides in addition a machine-readable copy of the Source Code ++of the Work along with each copy of the Work that the Licensor ++distributes or indicates, in a notice following the copyright notice ++attached to the Work, a repository where the Source Code is easily and ++freely accessible for as long as the Licensor continues to distribute or ++communicate the Work. ++ ++4. Limitations on copyright ++=========================== ++ ++Nothing in this Licence is intended to deprive the Licensee of the ++benefits from any exception or limitation to the exclusive rights of the ++rights owners in the Work, of the exhaustion of those rights or of other ++applicable limitations thereto. ++ ++5. Obligations of the Licensee ++============================== ++ ++The grant of the rights mentioned above is subject to some restrictions ++and obligations imposed on the Licensee. Those obligations are the ++following: ++ ++*Attribution right*: The Licensee shall keep intact all copyright, ++patent or trademarks notices and all notices that refer to the Licence ++and to the disclaimer of warranties. The Licensee must include a copy ++of such notices and a copy of the Licence with every copy of the Work ++he/she distributes or communicates. The Licensee must cause any ++Derivative Work to carry prominent notices stating that the Work has ++been modified and the date of modification. ++ ++*Copyleft clause*: If the Licensee distributes or communicates copies ++of the Original Works or Derivative Works, this Distribution or ++Communication will be done under the terms of this Licence or of a ++later version of this Licence unless the Original Work is expressly ++distributed only under this version of the Licence — for example by ++communicating ‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) ++cannot offer or impose any additional terms or conditions on the Work ++or Derivative Work that alter or restrict the terms of the Licence. ++ ++*Compatibility clause*: If the Licensee Distributes or Communicates ++Derivative Works or copies thereof based upon both the Work and another ++work licensed under a Compatible Licence, this Distribution or ++Communication can be done under the terms of this Compatible Licence. ++For the sake of this clause, ‘Compatible Licence’ refers to the licences ++listed in the appendix attached to this Licence. Should the Licensee’s ++obligations under the Compatible Licence conflict with his/her ++obligations under this Licence, the obligations of the Compatible ++Licence shall prevail. ++ ++*Provision of Source Code*: When distributing or communicating copies ++of the Work, the Licensee will provide a machine-readable copy of the ++Source Code or indicate a repository where this Source will be easily ++and freely available for as long as the Licensee continues to distribute ++or communicate the Work. Legal Protection: This Licence does not grant ++permission to use the trade names, trademarks, service marks, or names ++of the Licensor, except as required for reasonable and customary use ++in describing the origin of the Work and reproducing the content of ++the copyright notice. ++ ++6. Chain of Authorship ++====================== ++ ++The original Licensor warrants that the copyright in the Original Work ++granted hereunder is owned by him/her or licensed to him/her and that ++he/she has the power and authority to grant the Licence. ++ ++Each Contributor warrants that the copyright in the modifications he/she ++brings to the Work are owned by him/her or licensed to him/her and that ++he/she has the power and authority to grant the Licence. ++ ++Each time You accept the Licence, the original Licensor and subsequent ++Contributors grant You a licence to their contributions to the Work, ++under the terms of this Licence. ++ ++7. Disclaimer of Warranty ++========================= ++ ++The Work is a work in progress, which is continuously improved by ++numerous Contributors. It is not a finished work and may therefore ++contain defects or ‘bugs’ inherent to this type of development. For ++the above reason, the Work is provided under the Licence on an ‘as is’ ++basis and without warranties of any kind concerning the Work, including ++without limitation merchantability, fitness for a particular purpose, ++absence of defects or errors, accuracy, non-infringement of intellectual ++property rights other than copyright as stated in Article 6 of this ++Licence. ++ ++This disclaimer of warranty is an essential part of the Licence and a ++condition for the grant of any rights to the Work. ++ ++8. Disclaimer of Liability ++========================== ++ ++Except in the cases of wilful misconduct or damages directly caused to ++natural persons, the Licensor will in no event be liable for any direct ++or indirect, material or moral, damages of any kind, arising out of the ++Licence or of the use of the Work, including without limitation, damages ++for loss of goodwill, work stoppage, computer failure or malfunction, ++loss of data or any commercial damage, even if the Licensor has been ++advised of the possibility of such damage. However, the Licensor will be ++liable under statutory product liability laws as far such laws apply to ++the Work. ++ ++9. Additional agreements ++======================== ++ ++While distributing the Work, You may choose to conclude an additional ++agreement, defining obligations or services consistent with this ++Licence. However, if accepting obligations, You may act only on your own ++behalf and on your sole responsibility, not on behalf of the original ++Licensor or any other Contributor, and only if You agree to indemnify, ++defend, and hold each Contributor harmless for any liability incurred ++by, or claims asserted against such Contributor by the fact You have ++accepted any warranty or additional liability. ++ ++10. Acceptance of the Licence ++============================= ++ ++The provisions of this Licence can be accepted by clicking on an icon ++‘I agree’ placed under the bottom of a window displaying the text of ++this Licence or by affirming consent in any other similar way, in ++accordance with the rules of applicable law. Clicking on that icon ++indicates your clear and irrevocable acceptance of this Licence and ++all of its terms and conditions. ++ ++Similarly, you irrevocably accept this Licence and all of its terms ++and conditions by exercising any rights granted to You by Article 2 ++of this Licence, such as the use of the Work, the creation by You of ++a Derivative Work or the Distribution or Communication by You of the ++Work or copies thereof. ++ ++11. Information to the public ++============================= ++ ++In case of any Distribution or Communication of the Work by means of ++electronic communication by You (for example, by offering to download ++the Work from a remote location) the distribution channel or media (for ++example, a website) must at least provide to the public the information ++requested by the applicable law regarding the Licensor, the Licence and ++the way it may be accessible, concluded, stored and reproduced by the ++Licensee. ++ ++12. Termination of the Licence ++============================== ++ ++The Licence and the rights granted hereunder will terminate ++automatically upon any breach by the Licensee of the terms of the ++Licence. ++ ++Such a termination will not terminate the licences of any person who ++has received the Work from the Licensee under the Licence, provided ++such persons remain in full compliance with the Licence. ++ ++13. Miscellaneous ++================= ++ ++Without prejudice of Article 9 above, the Licence represents the ++complete agreement between the Parties as to the Work. ++ ++If any provision of the Licence is invalid or unenforceable under ++applicable law, this will not affect the validity or enforceability of ++the Licence as a whole. Such provision will be construed or reformed so ++as necessary to make it valid and enforceable. ++ ++The European Commission may publish other linguistic versions or new ++versions of this Licence or updated versions of the Appendix, so far ++this is required and reasonable, without reducing the scope of the ++rights granted by the Licence. ++ ++New versions of the Licence will be published with a unique ++version number. ++ ++All linguistic versions of this Licence, approved by the European ++Commission, have identical value. Parties can take advantage of the ++linguistic version of their choice. ++ ++14. Jurisdiction ++================ ++ ++Without prejudice to specific agreement between parties, ++ ++* any litigation resulting from the interpretation of this License, ++ arising between the European Union institutions, bodies, offices or ++ agencies, as a Licensor, and any Licensee, will be subject to the ++ jurisdiction of the Court of Justice of the European Union, as laid ++ down in article 272 of the Treaty on the Functioning of the European ++ Union, ++* any litigation arising between other parties and resulting from the ++ interpretation of this License, will be subject to the exclusive ++ jurisdiction of the competent court where the Licensor resides or ++ conducts its primary business. ++ ++15. Applicable Law ++================== ++ ++Without prejudice to specific agreement between parties, ++ ++* this Licence shall be governed by the law of the European Union Member ++ State where the Licensor has his seat, resides or has his registered ++ office, ++* this licence shall be governed by Belgian law if the Licensor has no ++ seat, residence or registered office inside a European Union Member ++ State. ++ ++Appendix ++======== ++ ++‘Compatible Licences’ according to Article 5 EUPL are: ++ ++* GNU General Public License (GPL) v. 2, v. 3 ++* GNU Affero General Public License (AGPL) v. 3 ++* Open Software License (OSL) v. 2.1, v. 3.0 ++* Eclipse Public License (EPL) v. 1.0 ++* CeCILL v. 2.0, v. 2.1 ++* Mozilla Public Licence (MPL) v. 2 ++* GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 ++* Creative Commons Attribution-ShareAlike v. 3.0 Unported ++ (CC BY-SA 3.0) for works other than software ++* European Union Public Licence (EUPL) v. 1.1, v. 1.2 ++* Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) ++ or Strong Reciprocity (LiLiQ-R+) ++ ++The European Commission may update this Appendix to later versions of ++the above licences without producing a new version of the EUPL, as long ++as they provide the rights granted in Article 2 of this Licence and ++protect the covered Source Code from exclusive appropriation. ++ ++All other changes or additions to this Appendix require the production ++of a new EUPL version. +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/MANIFEST.in +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/MANIFEST.in (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/MANIFEST.in (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) +@@ -0,0 +1,4 @@ ++include LICENCE.rst ++recursive-include aleksis/apps/*/static * ++recursive-include aleksis/apps/*/templates * ++recursive-include aleksis/apps/*/migrations * +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/README.rst +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/README.rst (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/README.rst (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) +@@ -0,0 +1,29 @@ ++AlekSIS (School Information System) — App Dashboard Feeds (Include feeds from external resources as widgets on dashboard) ++================================================================================================== ++ ++AlekSIS ++------- ++ ++This is an application for use with the `AlekSIS`_ platform. ++ ++Features ++-------- ++ ++The author of this app did not describe it yet. ++ ++Licence ++------- ++ ++:: ++ ++ Copyright © 2020 Julian Leucker <leuckerj@gmail.com> ++ ++ Licenced under the EUPL, version 1.2 or later ++ ++Please see the LICENCE.rst file accompanying this distribution for the ++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://edugit.org/AlekSIS/AlekSIS ++.. _European Union Public Licence: https://eupl.eu/ +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/__init__.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/__init__.py (revision f994fda2bf88bc49cd4658a3915ac8626c678c7c) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/__init__.py (revision f994fda2bf88bc49cd4658a3915ac8626c678c7c) +@@ -0,0 +1,8 @@ ++import pkg_resources ++ ++try: ++ __version__ = pkg_resources.get_distribution("AlekSIS-App-DashboardFeeds").version ++except Exception: ++ __version__ = "unknown" ++ ++default_app_config = "aleksis.apps.dashboardfeeds.apps.DefaultConfig" +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/apps.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/apps.py (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/apps.py (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) +@@ -0,0 +1,6 @@ ++from aleksis.core.util.apps import AppConfig ++ ++ ++class DefaultConfig(AppConfig): ++ name = "aleksis.apps.dashboardfeeds" ++ verbose_name = "AlekSIS — Dashboard Feeds (Include feeds from external resources as widgets on dashboard)" +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/menus.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/menus.py (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/menus.py (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) +@@ -0,0 +1,17 @@ ++from django.utils.translation import ugettext_lazy as _ ++ ++MENUS = { ++ "NAV_MENU_CORE": [ ++ { ++ "name": _("Dashboard Feeds"), ++ "url": "empty", ++ "root": True, ++ "validators": [ ++ "menu_generator.validators.is_authenticated", ++ "aleksis.core.util.core_helpers.has_person", ++ ], ++ "submenu": [ ++ ], ++ } ++ ] ++} +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/migrations/__init__.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/migrations/__init__.py (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/migrations/__init__.py (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) +@@ -0,0 +1,0 @@ +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/templates/dashboardfeeds/empty.html +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/templates/dashboardfeeds/empty.html (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/templates/dashboardfeeds/empty.html (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) +@@ -0,0 +1,8 @@ ++{% extends 'core/base.html' %} ++{% load i18n %} ++ ++{% block content %} ++ <p class="flow-text"> ++ {% blocktrans %}Dashboard Feeds (Include feeds from external resources as widgets on dashboard){% endblocktrans %} ++ </p> ++{% endblock %} +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/urls.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/urls.py (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/urls.py (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) +@@ -0,0 +1,7 @@ ++from django.urls import path ++ ++from . import views ++ ++urlpatterns = [ ++ path("empty", views.empty, name="empty"), ++] +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/views.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/views.py (revision f320d5a89055ce5e0f9e7c97a072a15443dc25d2) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/views.py (revision f320d5a89055ce5e0f9e7c97a072a15443dc25d2) +@@ -0,0 +1,26 @@ ++from django.contrib.auth.decorators import login_required ++from django.http import Http404, HttpRequest, HttpResponse ++from django.shortcuts import render ++from django.template.loader import render_to_string ++ ++from .models import DashboardWidget ++ ++ ++@login_required ++def empty(request: HttpRequest) -> HttpResponse: ++ context = {} ++ ++ return render(request, "dashboardfeeds/empty.html", context) ++ ++ ++def get_widgets(request): ++ widgets = DashboardWidget.objects.all() ++ ++ widgets_to_return = [] ++ ++ for widget in widgets: ++ feed = widget.widget.get_feed() ++ ++ widgets_to_return.append(render_to_string(widget.widget.template, feed, request)) ++ ++ return widgets_to_return +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/pyproject.toml +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/pyproject.toml (revision f994fda2bf88bc49cd4658a3915ac8626c678c7c) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/pyproject.toml (revision f994fda2bf88bc49cd4658a3915ac8626c678c7c) +@@ -0,0 +1,28 @@ ++[tool.poetry] ++name = "AlekSIS-App-DashboardFeeds" ++version = "1.0" ++packages = [ ++ { include = "aleksis" } ++] ++readme = "README.rst" ++ ++description = "AlekSIS (School Information System) — App Dashboard Feeds (Include feeds from external resources as widgets on dashboard)" ++authors = ["Julian Leucker <leuckerj@gmail.com>"] ++license = "EUPL-1.2" ++homepage = "https://aleksis.edugit.io/" ++repository = "https://edugit.org/Teckids/AlekSIS/AlekSIS-App-DashboardFeeds" ++documentation = "https://aleksis.edugit.io/AlekSIS/docs/html/" ++classifiers = [ ++ "Environment :: Web Environment", ++ "Intended Audience :: Education", ++ "Topic :: Education" ++] ++ ++[tool.poetry.dependencies] ++python = "^3.7" ++AlekSIS = { path = "../../.." } ++feedparser = "^5.2.1" ++ ++[build-system] ++requires = ["poetry>=1.0"] ++build-backend = "poetry.masonry.api" +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/renovate.json +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/renovate.json (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/renovate.json (revision 4d3235196dca472084aa6fcdccc21ae218ac3a07) +@@ -0,0 +1,1 @@ ++{} +Index: AlekSIS/aleksis/core/util/network.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/aleksis/core/util/network.py (revision 94e06304177bf52c979b14a6fbbba85b052dcaf2) ++++ AlekSIS/aleksis/core/util/network.py (revision 9648eaaf23a9e45ba5ab180b973d79d8f2141efb) +@@ -5,8 +5,8 @@ + from ics import Calendar + from requests import RequestException + +-from dashboard import settings +-from dashboard.caches import LATEST_ARTICLE_CACHE, CURRENT_EVENTS_CACHE ++# from dashboard import settings ++# from dashboard.caches import LATEST_ARTICLE_CACHE, CURRENT_EVENTS_CACHE + + WP_DOMAIN: str = "https://katharineum-zu-luebeck.de" + +@@ -18,7 +18,8 @@ + author_whitelist: list = None, + author_blacklist: list = None, + category_whitelist: list = None, +- category_blacklist: list = None ++ category_blacklist: list = None, ++ filter_vs_composer: bool = False, + ): + """ + This function returns the newest articles/posts of a WordPress site. +@@ -29,6 +30,7 @@ + :param author_blacklist: If the author's id (an integer) is in this list, the article won't be shown + :param category_whitelist: If this list is filled, only articles which are in one of this categories will be shown + :param category_blacklist: If the category's id (an integer) is in this list, the article won't be shown ++ :param filter_vs_composer: Remove unnecessary Visual Composer Tags + :return: a list of the newest posts/articles + """ + # Make mutable default arguments unmutable +@@ -68,7 +70,7 @@ + image_url: str = "" + + # Replace VS composer tags if activated +- if settings.latest_article_settings.replace_vs_composer_stuff: ++ if filter_vs_composer: + excerpt = VS_COMPOSER_REGEX.sub("", post["excerpt"]["rendered"]) + else: + excerpt = post["excerpt"]["rendered"] +@@ -81,13 +83,13 @@ + "image_url": image_url, + } + ) +- if len(posts) >= limit and limit >= 0: ++ if len(posts) >= limit >= 0: + break + + return posts + + +-@LATEST_ARTICLE_CACHE.decorator ++# @LATEST_ARTICLE_CACHE.decorator + def get_newest_article_from_news(domain=WP_DOMAIN): + newest_articles: list = get_newest_articles(domain=domain, limit=1, category_whitelist=[1, 27]) + if len(newest_articles) > 0: +@@ -149,7 +151,7 @@ + return events + + +-@CURRENT_EVENTS_CACHE.decorator ++# @CURRENT_EVENTS_CACHE.decorator + def get_current_events_with_cal(limit: int = 5) -> list: + # Get URL + calendar_url: str = settings.current_events_settings.calendar_url +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/admin.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/admin.py (revision f994fda2bf88bc49cd4658a3915ac8626c678c7c) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/admin.py (revision f994fda2bf88bc49cd4658a3915ac8626c678c7c) +@@ -0,0 +1,6 @@ ++from django.contrib import admin ++ ++from .models import DashboardWidget, RSSFeedWidget ++ ++admin.site.register(DashboardWidget) ++admin.site.register(RSSFeedWidget) +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/migrations/0001_initial.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/migrations/0001_initial.py (revision f994fda2bf88bc49cd4658a3915ac8626c678c7c) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/migrations/0001_initial.py (revision f994fda2bf88bc49cd4658a3915ac8626c678c7c) +@@ -0,0 +1,39 @@ ++# Generated by Django 3.0.2 on 2020-01-13 14:35 ++ ++from django.db import migrations, models ++import django.db.models.deletion ++ ++ ++class Migration(migrations.Migration): ++ ++ initial = True ++ ++ dependencies = [ ++ ] ++ ++ operations = [ ++ migrations.CreateModel( ++ name='DashboardWidget', ++ fields=[ ++ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ++ ('title', models.CharField(max_length=150, verbose_name='Widget Title')), ++ ('active', models.BooleanField(blank=True, verbose_name='Activate Widget')), ++ ], ++ options={ ++ 'verbose_name': 'Dashboard Widget', ++ 'verbose_name_plural': 'Dashboard Widgets', ++ }, ++ ), ++ migrations.CreateModel( ++ name='RSSFeedWidget', ++ fields=[ ++ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ++ ('url', models.URLField(verbose_name='RSS Url')), ++ ('widget', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='widget', to='dashboardfeeds.DashboardWidget', verbose_name='widget')), ++ ], ++ options={ ++ 'verbose_name': 'RSS Widget', ++ 'verbose_name_plural': 'RSS Widgets', ++ }, ++ ), ++ ] +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/models.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/models.py (revision f320d5a89055ce5e0f9e7c97a072a15443dc25d2) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/models.py (revision f320d5a89055ce5e0f9e7c97a072a15443dc25d2) +@@ -0,0 +1,74 @@ ++from typing import Optional ++ ++from django.contrib.auth import get_user_model ++from django.contrib.auth.models import User ++from django.db import models ++from django.utils import timezone ++from django.utils.translation import ugettext_lazy as _ ++import feedparser ++ ++from constance import config ++ ++from aleksis.core.util import network ++ ++ ++class DashboardWidget(models.Model): ++ title = models.CharField(max_length=150, verbose_name=_("Widget Title")) ++ active = models.BooleanField(blank=True, verbose_name=_("Activate Widget")) ++ base_url = models.URLField(verbose_name=_("Base URL"), ++ help_text=_("index url of the news website (as link for users)")) ++ ++ def __str__(self): ++ return self.title ++ ++ class Meta: ++ verbose_name = _("Dashboard Widget") ++ verbose_name_plural = _("Dashboard Widgets") ++ ++ ++# class WordpressFeedWidget(models.Model): ++# widget = models.OneToOneField(DashboardWidget, related_name="widget", verbose_name=_("widget"), ++# on_delete=models.CASCADE) ++# url = models.URLField(verbose_name=_("Wordpress Url")) ++# filter_vs_composer = models.BooleanField(verbose_name=_("Filter out Visual Composer Tags?"), blank=True, default=True) ++# ++# # author_whitelist: list = None, ++# # author_blacklist: list = None, ++# # category_whitelist: list = None, ++# # category_blacklist: list = None, ++# ++# # https://wordpress.org/support/article/wordpress-feeds/#finding-your-feed-url ++# ++# def get_feed(self): ++# feed = { ++# "title": self.widget.title, ++# "active": self.widget.active, ++# "url": self.url, ++# "results": network.get_newest_articles(domain=self.url, ), ++# } ++# ++# class Meta: ++# verbose_name = _("Wordpress Widget") ++# verbose_name_plural = _("Wordpress Widgets") ++ ++ ++class RSSFeedWidget(models.Model): ++ template = "dashboardfeeds/rss.html" ++ ++ widget = models.OneToOneField(DashboardWidget, related_name="widget", verbose_name=_("widget"), ++ on_delete=models.CASCADE) ++ url = models.URLField(verbose_name=_("RSS Url")) ++ ++ def get_feed(self): ++ feed = { ++ "title": self.widget.title, ++ "active": self.widget.active, ++ "url": self.url, ++ "base_url": self.widget.base_url, ++ "result": feedparser.parse(self.url)["entries"][0], ++ } ++ return feed ++ ++ class Meta: ++ verbose_name = _("RSS Widget") ++ verbose_name_plural = _("RSS Widgets") +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/poetry.lock +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/poetry.lock (revision f320d5a89055ce5e0f9e7c97a072a15443dc25d2) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/poetry.lock (revision f320d5a89055ce5e0f9e7c97a072a15443dc25d2) +@@ -0,0 +1,1271 @@ ++[[package]] ++category = "main" ++description = "" ++develop = true ++name = "aleksis" ++optional = false ++python-versions = "^3.7" ++version = "1.0a4.dev0" ++ ++[package.dependencies] ++Django = "^3.0" ++Pillow = "^7.0" ++colour = "^0.1.5" ++django-any-js = "^1.0" ++django-bootstrap4 = "^1.0" ++django-ckeditor = "^5.8.0" ++django-debug-toolbar = "^2.0" ++django-easy-audit = "^1.2rc1" ++django-filter = "^2.2.0" ++django-hattori = "^0.2" ++django-image-cropping = "^1.2" ++django-impersonate = "^1.4" ++django-ipware = "^2.1" ++django-maintenance-mode = "^0.14.0" ++django-material = "^1.6.0" ++django-menu-generator = "^1.0.4" ++django-middleware-global-request = "^0.1.2" ++django-pwa = "^1.0.6" ++django-sass-processor = "^0.8" ++django-settings-context-processor = "^0.2" ++django-tables2 = "^2.1" ++django-templated-email = "^2.3.0" ++django-yarnpkg = "^6.0" ++django_select2 = "^7.1" ++django_widget_tweaks = "^1.4.5" ++easy-thumbnails = "^2.6" ++html2text = "^2019.9.26" ++libsass = "^0.19.2" ++psycopg2 = "^2.8" ++python-memcached = "^1.59" ++requests = "^2.22" ++ ++[package.dependencies.django-constance] ++extras = ["database"] ++version = "rev 590fa02eb30e377da0eda5cc3a84254b839176a7" ++ ++[package.dependencies.django-phonenumber-field] ++extras = ["phonenumbers"] ++version = ">=3.0, <5.0" ++ ++[package.dependencies.django-two-factor-auth] ++extras = ["YubiKey", "phonenumbers", "Call", "SMS"] ++version = "^1.10.0" ++ ++[package.dependencies.dynaconf] ++extras = ["yaml", "toml", "ini"] ++version = "^2.0" ++ ++[package.extras] ++ldap = ["django-auth-ldap (^2.0)"] ++ ++[package.source] ++reference = "" ++type = "directory" ++url = "../../.." ++ ++[[package]] ++category = "main" ++description = "ASGI specs, helper code, and adapters" ++name = "asgiref" ++optional = false ++python-versions = "*" ++version = "3.2.3" ++ ++[package.extras] ++tests = ["pytest (>=4.3.0,<4.4.0)", "pytest-asyncio (>=0.10.0,<0.11.0)"] ++ ++[[package]] ++category = "main" ++description = "Internationalization utilities" ++name = "babel" ++optional = false ++python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" ++version = "2.8.0" ++ ++[package.dependencies] ++pytz = ">=2015.7" ++ ++[[package]] ++category = "main" ++description = "Screen-scraping library" ++name = "beautifulsoup4" ++optional = false ++python-versions = "*" ++version = "4.8.2" ++ ++[package.dependencies] ++soupsieve = ">=1.2" ++ ++[package.extras] ++html5lib = ["html5lib"] ++lxml = ["lxml"] ++ ++[[package]] ++category = "main" ++description = "Python package for providing Mozilla's CA Bundle." ++name = "certifi" ++optional = false ++python-versions = "*" ++version = "2019.11.28" ++ ++[[package]] ++category = "main" ++description = "Universal encoding detector for Python 2 and 3" ++name = "chardet" ++optional = false ++python-versions = "*" ++version = "3.0.4" ++ ++[[package]] ++category = "main" ++description = "Composable command line interface toolkit" ++name = "click" ++optional = false ++python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" ++version = "7.0" ++ ++[[package]] ++category = "main" ++description = "Cross-platform colored terminal text." ++marker = "platform_system == \"Windows\"" ++name = "colorama" ++optional = false ++python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" ++version = "0.4.3" ++ ++[[package]] ++category = "main" ++description = "converts and manipulates various color representation (HSL, RVB, web, X11, ...)" ++name = "colour" ++optional = false ++python-versions = "*" ++version = "0.1.5" ++ ++[package.extras] ++test = ["nose"] ++ ++[[package]] ++category = "main" ++description = "Config file reading, writing and validation." ++name = "configobj" ++optional = false ++python-versions = "*" ++version = "5.0.6" ++ ++[package.dependencies] ++six = "*" ++ ++[[package]] ++category = "main" ++description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." ++name = "django" ++optional = false ++python-versions = ">=3.6" ++version = "3.0.2" ++ ++[package.dependencies] ++asgiref = ">=3.2,<4.0" ++pytz = "*" ++sqlparse = ">=0.2.2" ++ ++[package.extras] ++argon2 = ["argon2-cffi (>=16.1.0)"] ++bcrypt = ["bcrypt"] ++ ++[[package]] ++category = "main" ++description = "Include JavaScript libraries with readable template tags" ++name = "django-any-js" ++optional = false ++python-versions = "*" ++version = "1.0.3.post0" ++ ++[package.dependencies] ++Django = ">=1.11" ++ ++[[package]] ++category = "main" ++description = "A helper class for handling configuration defaults of packaged apps gracefully." ++name = "django-appconf" ++optional = false ++python-versions = "*" ++version = "1.0.3" ++ ++[package.dependencies] ++django = "*" ++six = "*" ++ ++[[package]] ++category = "main" ++description = "Bootstrap support for Django projects" ++name = "django-bootstrap4" ++optional = false ++python-versions = "*" ++version = "1.1.1" ++ ++[package.dependencies] ++beautifulsoup4 = "*" ++ ++[[package]] ++category = "main" ++description = "Bulk update using one query over Django ORM." ++name = "django-bulk-update" ++optional = false ++python-versions = "*" ++version = "2.2.0" ++ ++[package.dependencies] ++Django = ">=1.8" ++ ++[[package]] ++category = "main" ++description = "Django admin CKEditor integration." ++name = "django-ckeditor" ++optional = false ++python-versions = "*" ++version = "5.8.0" ++ ++[package.dependencies] ++django-js-asset = ">=1.2.2" ++ ++[[package]] ++category = "main" ++description = "Django live settings with pluggable backends, including Redis." ++name = "django-constance" ++optional = false ++python-versions = "*" ++version = "2.5.0" ++ ++[package.dependencies] ++[package.dependencies.django-picklefield] ++optional = true ++version = "*" ++ ++[package.extras] ++database = ["django-picklefield"] ++redis = ["redis"] ++ ++[package.source] ++reference = "590fa02eb30e377da0eda5cc3a84254b839176a7" ++type = "git" ++url = "https://github.com/jazzband/django-constance" ++[[package]] ++category = "main" ++description = "A configurable set of panels that display various debug information about the current request/response." ++name = "django-debug-toolbar" ++optional = false ++python-versions = ">=3.5" ++version = "2.1" ++ ++[package.dependencies] ++Django = ">=1.11" ++sqlparse = ">=0.2.0" ++ ++[[package]] ++category = "main" ++description = "Yet another Django audit log app, hopefully the simplest one." ++name = "django-easy-audit" ++optional = false ++python-versions = "*" ++version = "1.2rc1" ++ ++[package.dependencies] ++beautifulsoup4 = "*" ++ ++[[package]] ++category = "main" ++description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." ++name = "django-filter" ++optional = false ++python-versions = ">=3.4" ++version = "2.2.0" ++ ++[package.dependencies] ++Django = ">=1.11" ++ ++[[package]] ++category = "main" ++description = "A set of high-level abstractions for Django forms" ++name = "django-formtools" ++optional = false ++python-versions = "*" ++version = "2.2" ++ ++[package.dependencies] ++Django = ">=1.11" ++ ++[[package]] ++category = "main" ++description = "Command to anonymize sensitive data." ++name = "django-hattori" ++optional = false ++python-versions = "*" ++version = "0.2.1" ++ ++[package.dependencies] ++Django = ">=1.8" ++Faker = ">=0.8.13" ++django-bulk-update = ">=2.2.0" ++six = "*" ++tqdm = ">=4.23.4" ++ ++[[package]] ++category = "main" ++description = "A reusable app for cropping images easily and non-destructively in Django" ++name = "django-image-cropping" ++optional = false ++python-versions = "*" ++version = "1.3.0" ++ ++[package.dependencies] ++django-appconf = ">=1.0.2" ++ ++[[package]] ++category = "main" ++description = "Django app to allow superusers to impersonate other users." ++name = "django-impersonate" ++optional = false ++python-versions = "*" ++version = "1.4.1" ++ ++[[package]] ++category = "main" ++description = "A Django utility application that returns client's real IP address" ++name = "django-ipware" ++optional = false ++python-versions = "*" ++version = "2.1.0" ++ ++[[package]] ++category = "main" ++description = "script tag with additional attributes for django.forms.Media" ++name = "django-js-asset" ++optional = false ++python-versions = "*" ++version = "1.2.2" ++ ++[[package]] ++category = "main" ++description = "django-maintenance-mode shows a 503 error page when maintenance-mode is on." ++name = "django-maintenance-mode" ++optional = false ++python-versions = "*" ++version = "0.14.0" ++ ++[[package]] ++category = "main" ++description = "Material design for django forms and admin" ++name = "django-material" ++optional = false ++python-versions = "*" ++version = "1.6.0" ++ ++[package.dependencies] ++six = "*" ++ ++[[package]] ++category = "main" ++description = "A straightforward menu generator for Django" ++name = "django-menu-generator" ++optional = false ++python-versions = "*" ++version = "1.0.4" ++ ++[[package]] ++category = "main" ++description = "Django middleware that keep request instance for every thread." ++name = "django-middleware-global-request" ++optional = false ++python-versions = "*" ++version = "0.1.2" ++ ++[package.dependencies] ++django = "*" ++ ++[[package]] ++category = "main" ++description = "A pluggable framework for adding two-factor authentication to Django using one-time passwords." ++name = "django-otp" ++optional = false ++python-versions = "*" ++version = "0.7.5" ++ ++[package.dependencies] ++django = ">=1.11" ++six = ">=1.10.0" ++ ++[package.extras] ++qrcode = ["qrcode"] ++ ++[[package]] ++category = "main" ++description = "A django-otp plugin that verifies YubiKey OTP tokens." ++name = "django-otp-yubikey" ++optional = false ++python-versions = "*" ++version = "0.5.2" ++ ++[package.dependencies] ++YubiOTP = ">=0.2.2" ++django-otp = ">=0.5.0" ++six = ">=1.10.0" ++ ++[[package]] ++category = "main" ++description = "An international phone number field for django models." ++name = "django-phonenumber-field" ++optional = false ++python-versions = ">=3.5" ++version = "3.0.1" ++ ++[package.dependencies] ++Django = ">=1.11.3" ++babel = "*" ++ ++[package.extras] ++phonenumbers = ["phonenumbers (>=7.0.2)"] ++phonenumberslite = ["phonenumberslite (>=7.0.2)"] ++ ++[[package]] ++category = "main" ++description = "Pickled object field for Django" ++name = "django-picklefield" ++optional = false ++python-versions = "*" ++version = "2.0" ++ ++[package.dependencies] ++Django = ">=1.11" ++ ++[package.extras] ++tests = ["tox"] ++ ++[[package]] ++category = "main" ++description = "A Django app to include a manifest.json and Service Worker instance to enable progressive web app behavior" ++name = "django-pwa" ++optional = false ++python-versions = "*" ++version = "1.0.6" ++ ++[package.dependencies] ++django = ">=1.8" ++ ++[[package]] ++category = "main" ++description = "Render a particular block from a template to a string." ++name = "django-render-block" ++optional = false ++python-versions = "*" ++version = "0.6" ++ ++[package.dependencies] ++django = ">=1.11" ++ ++[[package]] ++category = "main" ++description = "SASS processor to compile SCSS files into *.css, while rendering, or offline." ++name = "django-sass-processor" ++optional = false ++python-versions = "*" ++version = "0.8" ++ ++[package.extras] ++dev = ["libsass (>=0.13)"] ++management-command = ["django-compressor (>=2.4)"] ++ ++[[package]] ++category = "main" ++description = "Select2 option fields for Django" ++name = "django-select2" ++optional = false ++python-versions = "*" ++version = "7.2.0" ++ ++[package.dependencies] ++django = ">=2.2" ++django-appconf = ">=0.6.0" ++ ++[[package]] ++category = "main" ++description = "Makes specified django settings visible in template rendering context." ++name = "django-settings-context-processor" ++optional = false ++python-versions = "*" ++version = "0.2" ++ ++[[package]] ++category = "main" ++description = "Table/data-grid framework for Django" ++name = "django-tables2" ++optional = false ++python-versions = "*" ++version = "2.2.1" ++ ++[package.dependencies] ++Django = ">=1.11" ++ ++[package.extras] ++tablib = ["tablib"] ++ ++[[package]] ++category = "main" ++description = "A Django oriented templated / transaction email abstraction" ++name = "django-templated-email" ++optional = false ++python-versions = "*" ++version = "2.3.0" ++ ++[package.dependencies] ++django-render-block = ">=0.5" ++six = ">=1" ++ ++[[package]] ++category = "main" ++description = "Complete Two-Factor Authentication for Django" ++name = "django-two-factor-auth" ++optional = false ++python-versions = "*" ++version = "1.10.0" ++ ++[package.dependencies] ++Django = ">=1.11" ++django-formtools = "*" ++django-otp = ">=0.6.0,<0.99" ++django-phonenumber-field = ">=1.1.0,<3.99" ++qrcode = ">=4.0.0,<6.99" ++ ++[package.dependencies.django-otp-yubikey] ++optional = true ++version = "*" ++ ++[package.dependencies.phonenumbers] ++optional = true ++version = ">=7.0.9,<8.99" ++ ++[package.dependencies.twilio] ++optional = true ++version = ">=6.0" ++ ++[package.extras] ++Call = ["twilio (>=6.0)"] ++SMS = ["twilio (>=6.0)"] ++YubiKey = ["django-otp-yubikey"] ++phonenumbers = ["phonenumbers (>=7.0.9,<8.99)"] ++phonenumberslite = ["phonenumberslite (>=7.0.9,<8.99)"] ++ ++[[package]] ++category = "main" ++description = "Tweak the form field rendering in templates, not in python-level form definitions." ++name = "django-widget-tweaks" ++optional = false ++python-versions = "*" ++version = "1.4.5" ++ ++[[package]] ++category = "main" ++description = "Integrate django with yarnpkg" ++name = "django-yarnpkg" ++optional = false ++python-versions = "*" ++version = "6.0.1" ++ ++[package.dependencies] ++django = "*" ++six = "*" ++ ++[[package]] ++category = "main" ++description = "The dynamic configurator for your Python Project" ++name = "dynaconf" ++optional = false ++python-versions = "*" ++version = "2.2.2" ++ ++[package.dependencies] ++click = "<=7.0" ++python-box = "<4.0.0" ++python-dotenv = "<=0.10.3" ++ ++[[package.dependencies.toml]] ++version = "<=0.10.0" ++ ++[[package.dependencies.toml]] ++optional = true ++version = "*" ++ ++[package.dependencies.PyYAML] ++optional = true ++version = "*" ++ ++[package.dependencies.configobj] ++optional = true ++version = "*" ++ ++[package.extras] ++all = ["redis", "pyyaml", "configobj", "hvac"] ++configobj = ["configobj"] ++ini = ["configobj"] ++redis = ["redis"] ++toml = ["toml"] ++vault = ["hvac"] ++yaml = ["pyyaml"] ++ ++[[package]] ++category = "main" ++description = "Easy thumbnails for Django" ++name = "easy-thumbnails" ++optional = false ++python-versions = ">=3.5" ++version = "2.7" ++ ++[package.dependencies] ++django = ">=1.11,<4.0" ++pillow = "*" ++ ++[[package]] ++category = "main" ++description = "Faker is a Python package that generates fake data for you." ++name = "faker" ++optional = false ++python-versions = ">=3.4" ++version = "4.0.0" ++ ++[package.dependencies] ++python-dateutil = ">=2.4" ++text-unidecode = "1.3" ++ ++[[package]] ++category = "main" ++description = "Universal feed parser, handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds" ++name = "feedparser" ++optional = false ++python-versions = "*" ++version = "5.2.1" ++ ++[[package]] ++category = "main" ++description = "Turn HTML into equivalent Markdown-structured text." ++name = "html2text" ++optional = false ++python-versions = ">=3.5" ++version = "2019.9.26" ++ ++[[package]] ++category = "main" ++description = "Internationalized Domain Names in Applications (IDNA)" ++name = "idna" ++optional = false ++python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" ++version = "2.8" ++ ++[[package]] ++category = "main" ++description = "Sass for Python: A straightforward binding of libsass for Python." ++name = "libsass" ++optional = false ++python-versions = "*" ++version = "0.19.4" ++ ++[package.dependencies] ++six = "*" ++ ++[[package]] ++category = "main" ++description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." ++name = "phonenumbers" ++optional = false ++python-versions = "*" ++version = "8.11.1" ++ ++[[package]] ++category = "main" ++description = "Python Imaging Library (Fork)" ++name = "pillow" ++optional = false ++python-versions = ">=3.5" ++version = "7.0.0" ++ ++[[package]] ++category = "main" ++description = "psycopg2 - Python-PostgreSQL Database Adapter" ++name = "psycopg2" ++optional = false ++python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" ++version = "2.8.4" ++ ++[[package]] ++category = "main" ++description = "Cryptographic library for Python" ++name = "pycryptodome" ++optional = false ++python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" ++version = "3.9.4" ++ ++[[package]] ++category = "main" ++description = "JSON Web Token implementation in Python" ++name = "pyjwt" ++optional = false ++python-versions = "*" ++version = "1.7.1" ++ ++[package.extras] ++crypto = ["cryptography (>=1.4)"] ++flake8 = ["flake8", "flake8-import-order", "pep8-naming"] ++test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner (>=4.2,<5.0.0)"] ++ ++[[package]] ++category = "main" ++description = "Advanced Python dictionaries with dot notation access" ++name = "python-box" ++optional = false ++python-versions = "*" ++version = "3.4.6" ++ ++[package.extras] ++testing = ["pytest", "coverage (>=3.6)", "pytest-cov"] ++ ++[[package]] ++category = "main" ++description = "Extensions to the standard Python datetime module" ++name = "python-dateutil" ++optional = false ++python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" ++version = "2.8.1" ++ ++[package.dependencies] ++six = ">=1.5" ++ ++[[package]] ++category = "main" ++description = "Add .env support to your django/flask apps in development and deployments" ++name = "python-dotenv" ++optional = false ++python-versions = "*" ++version = "0.10.3" ++ ++[package.extras] ++cli = ["click (>=5.0)"] ++ ++[[package]] ++category = "main" ++description = "Pure python memcached client" ++name = "python-memcached" ++optional = false ++python-versions = "*" ++version = "1.59" ++ ++[package.dependencies] ++six = ">=1.4.0" ++ ++[[package]] ++category = "main" ++description = "World timezone definitions, modern and historical" ++name = "pytz" ++optional = false ++python-versions = "*" ++version = "2019.3" ++ ++[[package]] ++category = "main" ++description = "YAML parser and emitter for Python" ++name = "pyyaml" ++optional = false ++python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" ++version = "5.3" ++ ++[[package]] ++category = "main" ++description = "QR Code image generator" ++name = "qrcode" ++optional = false ++python-versions = "*" ++version = "6.1" ++ ++[package.dependencies] ++colorama = "*" ++six = "*" ++ ++[package.extras] ++dev = ["tox", "pytest", "mock"] ++maintainer = ["zest.releaser"] ++pil = ["pillow"] ++test = ["pytest", "pytest-cov", "mock"] ++ ++[[package]] ++category = "main" ++description = "Python HTTP for Humans." ++name = "requests" ++optional = false ++python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" ++version = "2.22.0" ++ ++[package.dependencies] ++certifi = ">=2017.4.17" ++chardet = ">=3.0.2,<3.1.0" ++idna = ">=2.5,<2.9" ++urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" ++ ++[package.extras] ++security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] ++socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] ++ ++[[package]] ++category = "main" ++description = "Python 2 and 3 compatibility utilities" ++name = "six" ++optional = false ++python-versions = ">=2.6, !=3.0.*, !=3.1.*" ++version = "1.13.0" ++ ++[[package]] ++category = "main" ++description = "A modern CSS selector implementation for Beautiful Soup." ++name = "soupsieve" ++optional = false ++python-versions = "*" ++version = "1.9.5" ++ ++[[package]] ++category = "main" ++description = "Non-validating SQL parser" ++name = "sqlparse" ++optional = false ++python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" ++version = "0.3.0" ++ ++[[package]] ++category = "main" ++description = "The most basic Text::Unidecode port" ++name = "text-unidecode" ++optional = false ++python-versions = "*" ++version = "1.3" ++ ++[[package]] ++category = "main" ++description = "Python Library for Tom's Obvious, Minimal Language" ++name = "toml" ++optional = false ++python-versions = "*" ++version = "0.10.0" ++ ++[[package]] ++category = "main" ++description = "Fast, Extensible Progress Meter" ++name = "tqdm" ++optional = false ++python-versions = ">=2.6, !=3.0.*, !=3.1.*" ++version = "4.41.1" ++ ++[package.extras] ++dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown"] ++ ++[[package]] ++category = "main" ++description = "Twilio API client and TwiML generator" ++name = "twilio" ++optional = false ++python-versions = "*" ++version = "6.35.2" ++ ++[package.dependencies] ++PyJWT = ">=1.4.2" ++pytz = "*" ++six = "*" ++ ++[package.dependencies.requests] ++python = ">=3.0" ++version = ">=2.0.0" ++ ++[[package]] ++category = "main" ++description = "HTTP library with thread-safe connection pooling, file post, and more." ++name = "urllib3" ++optional = false ++python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" ++version = "1.25.7" ++ ++[package.extras] ++brotli = ["brotlipy (>=0.6.0)"] ++secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] ++socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] ++ ++[[package]] ++category = "main" ++description = "A library for verifying YubiKey OTP tokens, both locally and through a Yubico web service." ++name = "yubiotp" ++optional = false ++python-versions = "*" ++version = "0.2.2.post1" ++ ++[package.dependencies] ++pycryptodome = "*" ++six = "*" ++ ++[metadata] ++content-hash = "2b1e8c5d8a499199e0ea131bc709f41131b4db1eaaa66d818d4024d8d9bc72c3" ++python-versions = "^3.7" ++ ++[metadata.files] ++aleksis = [] ++asgiref = [ ++ {file = "asgiref-3.2.3-py2.py3-none-any.whl", hash = "sha256:ea448f92fc35a0ef4b1508f53a04c4670255a3f33d22a81c8fc9c872036adbe5"}, ++ {file = "asgiref-3.2.3.tar.gz", hash = "sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0"}, ++] ++babel = [ ++ {file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"}, ++ {file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"}, ++] ++beautifulsoup4 = [ ++ {file = "beautifulsoup4-4.8.2-py2-none-any.whl", hash = "sha256:e1505eeed31b0f4ce2dbb3bc8eb256c04cc2b3b72af7d551a4ab6efd5cbe5dae"}, ++ {file = "beautifulsoup4-4.8.2-py3-none-any.whl", hash = "sha256:9fbb4d6e48ecd30bcacc5b63b94088192dcda178513b2ae3c394229f8911b887"}, ++ {file = "beautifulsoup4-4.8.2.tar.gz", hash = "sha256:05fd825eb01c290877657a56df4c6e4c311b3965bda790c613a3d6fb01a5462a"}, ++] ++certifi = [ ++ {file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"}, ++ {file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"}, ++] ++chardet = [ ++ {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, ++ {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, ++] ++click = [ ++ {file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"}, ++ {file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"}, ++] ++colorama = [ ++ {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, ++ {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, ++] ++colour = [ ++ {file = "colour-0.1.5-py2.py3-none-any.whl", hash = "sha256:33f6db9d564fadc16e59921a56999b79571160ce09916303d35346dddc17978c"}, ++ {file = "colour-0.1.5.tar.gz", hash = "sha256:af20120fefd2afede8b001fbef2ea9da70ad7d49fafdb6489025dae8745c3aee"}, ++] ++configobj = [ ++ {file = "configobj-5.0.6.tar.gz", hash = "sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902"}, ++] ++django = [ ++ {file = "Django-3.0.2-py3-none-any.whl", hash = "sha256:4f2c913303be4f874015993420bf0bd8fd2097a9c88e6b49c6a92f9bdd3fb13a"}, ++ {file = "Django-3.0.2.tar.gz", hash = "sha256:8c3575f81e11390893860d97e1e0154c47512f180ea55bd84ce8fa69ba8051ca"}, ++] ++django-any-js = [ ++ {file = "django-any-js-1.0.3.post0.tar.gz", hash = "sha256:1da88b44b861b0f54f6b8ea0eb4c7c4fa1a5772e9a4320532cd4e0871a4e23f7"}, ++] ++django-appconf = [ ++ {file = "django-appconf-1.0.3.tar.gz", hash = "sha256:35f13ca4d567f132b960e2cd4c832c2d03cb6543452d34e29b7ba10371ba80e3"}, ++ {file = "django_appconf-1.0.3-py2.py3-none-any.whl", hash = "sha256:c98a7af40062e996b921f5962a1c4f3f0c979fa7885f7be4710cceb90ebe13a6"}, ++] ++django-bootstrap4 = [ ++ {file = "django-bootstrap4-1.1.1.tar.gz", hash = "sha256:39f97cbce85eb66f6d76be2029bae171bd3863d0c6932b1c2dae7f299c569b90"}, ++ {file = "django_bootstrap4-1.1.1-py3-none-any.whl", hash = "sha256:0fcd84f8414a58b43df0b331c00c8b2f1786ae28f75f419b4d33b06fca43e0d1"}, ++] ++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-ckeditor = [ ++ {file = "django-ckeditor-5.8.0.tar.gz", hash = "sha256:46fc9c7346ea36183dc0cea350f98704f8b04c4722b7fe4fb18baf8ae20423fb"}, ++ {file = "django_ckeditor-5.8.0-py2.py3-none-any.whl", hash = "sha256:a59bab13f4481318f8a048b1b0aef5c7da768a6352dcfb9ba0e77d91fbb9462a"}, ++] ++django-constance = [] ++django-debug-toolbar = [ ++ {file = "django-debug-toolbar-2.1.tar.gz", hash = "sha256:24c157bc6c0e1648e0a6587511ecb1b007a00a354ce716950bff2de12693e7a8"}, ++ {file = "django_debug_toolbar-2.1-py3-none-any.whl", hash = "sha256:77cfba1d6e91b9bc3d36dc7dc74a9bb80be351948db5f880f2562a0cbf20b6c5"}, ++] ++django-easy-audit = [ ++ {file = "django-easy-audit-1.2rc1.tar.gz", hash = "sha256:80f82fa4006290dcd6589a345e75de1c780de49d38218050eedd9048c54b647d"}, ++ {file = "django_easy_audit-1.2rc1-py3-none-any.whl", hash = "sha256:fb9c5ec3e90f0900302448d3648acc11da6d6b3d35d13d77eab917ab8c813d77"}, ++] ++django-filter = [ ++ {file = "django-filter-2.2.0.tar.gz", hash = "sha256:c3deb57f0dd7ff94d7dce52a047516822013e2b441bed472b722a317658cfd14"}, ++ {file = "django_filter-2.2.0-py3-none-any.whl", hash = "sha256:558c727bce3ffa89c4a7a0b13bc8976745d63e5fd576b3a9a851650ef11c401b"}, ++] ++django-formtools = [ ++ {file = "django-formtools-2.2.tar.gz", hash = "sha256:c5272c03c1cd51b2375abf7397a199a3148a9fbbf2f100e186467a84025d13b2"}, ++ {file = "django_formtools-2.2-py2.py3-none-any.whl", hash = "sha256:304fa777b8ef9e0693ce7833f885cb89ba46b0e46fc23b01176900a93f46742f"}, ++] ++django-hattori = [ ++ {file = "django-hattori-0.2.1.tar.gz", hash = "sha256:6953d40881317252f19f62c4e7fe8058924b852c7498bc42beb7bc4d268c252c"}, ++ {file = "django_hattori-0.2.1-py2.py3-none-any.whl", hash = "sha256:e529ed7af8fc34a0169c797c477672b687a205a56f3f5206f90c260acb83b7ac"}, ++] ++django-image-cropping = [ ++ {file = "django-image-cropping-1.3.0.tar.gz", hash = "sha256:5c102d87bc66de025517ad06e485c100f73313ebf725e7482a728944276f6463"}, ++] ++django-impersonate = [ ++ {file = "django-impersonate-1.4.1.tar.gz", hash = "sha256:63b62d06f93b0318698c68f7314c78473914c262d4164eb66ad860bb83e04771"}, ++] ++django-ipware = [ ++ {file = "django-ipware-2.1.0.tar.gz", hash = "sha256:a7c7a8fd019dbdc9c357e6e582f65034e897572fc79a7e467674efa8aef9d00b"}, ++] ++django-js-asset = [ ++ {file = "django-js-asset-1.2.2.tar.gz", hash = "sha256:c163ae80d2e0b22d8fb598047cd0dcef31f81830e127cfecae278ad574167260"}, ++ {file = "django_js_asset-1.2.2-py2.py3-none-any.whl", hash = "sha256:8ec12017f26eec524cab436c64ae73033368a372970af4cf42d9354fcb166bdd"}, ++] ++django-maintenance-mode = [ ++ {file = "django-maintenance-mode-0.14.0.tar.gz", hash = "sha256:f3fef1760fdcda5e0bf6c2966aadc77eea6f328580a9c751920daba927281a68"}, ++ {file = "django_maintenance_mode-0.14.0-py2-none-any.whl", hash = "sha256:b4cc24a469ed10897826a28f05d64e6166a58d130e4940ac124ce198cd4cc778"}, ++] ++django-material = [ ++ {file = "django-material-1.6.0.tar.gz", hash = "sha256:767ab6ad51f906bf773f927e853c2bff6b4ebdd1bd2bf45dbd4ef3e31657c3d5"}, ++ {file = "django_material-1.6.0-py2.py3-none-any.whl", hash = "sha256:6a30e42f0ceefef1ff325bda0017fa6f6a7fa534b15b8fcc48eb96de4b6adc8e"}, ++] ++django-menu-generator = [ ++ {file = "django-menu-generator-1.0.4.tar.gz", hash = "sha256:ce71a5055c16933c8aff64fb36c21e5cf8b6d505733aceed1252f8b99369a378"}, ++] ++django-middleware-global-request = [ ++ {file = "django-middleware-global-request-0.1.2.tar.gz", hash = "sha256:f6490759bc9f7dbde4001709554e29ca715daf847f2222914b4e47117dca9313"}, ++] ++django-otp = [ ++ {file = "django-otp-0.7.5.tar.gz", hash = "sha256:1f16c2b93fe484706ff16ac6f5e64ecc73dd240318c333e0560384ba548d3837"}, ++ {file = "django_otp-0.7.5-py2.py3-none-any.whl", hash = "sha256:cd4975539be478417033561e9832a1a69a583189f680e92a649f412c661f90aa"}, ++] ++django-otp-yubikey = [ ++ {file = "django-otp-yubikey-0.5.2.tar.gz", hash = "sha256:f0b1881562fb42ee9f12c28d284cbdb90d1f0383f2d53a595373b080a19bc261"}, ++ {file = "django_otp_yubikey-0.5.2-py2.py3-none-any.whl", hash = "sha256:26b12c763b37e99b95b8b8a54d06d8d54c3774eb26133a452f54558033de732b"}, ++] ++django-phonenumber-field = [ ++ {file = "django-phonenumber-field-3.0.1.tar.gz", hash = "sha256:794ebbc3068a7af75aa72a80cb0cec67e714ff8409a965968040f1fd210b2d97"}, ++ {file = "django_phonenumber_field-3.0.1-py3-none-any.whl", hash = "sha256:1ab19f723928582fed412bd9844221fa4ff466276d8526b8b4a9913ee1487c5e"}, ++] ++django-picklefield = [ ++ {file = "django-picklefield-2.0.tar.gz", hash = "sha256:f1733a8db1b6046c0d7d738e785f9875aa3c198215de11993463a9339aa4ea24"}, ++ {file = "django_picklefield-2.0-py2.py3-none-any.whl", hash = "sha256:9052f2dcf4882c683ce87b4356f29b4d014c0dad645b6906baf9f09571f52bc8"}, ++] ++django-pwa = [ ++ {file = "django-pwa-1.0.6.tar.gz", hash = "sha256:b3f1ad0c5241fae4c7505423540de4db93077d7c88416ff6d2af545ffe209f34"}, ++ {file = "django_pwa-1.0.6-py3-none-any.whl", hash = "sha256:9306105fcb637ae16fea6527be4b147d45fd53db85efb1d4f61dfea6bf793e56"}, ++] ++django-render-block = [ ++ {file = "django_render_block-0.6-py2.py3-none-any.whl", hash = "sha256:95c7dc9610378a10e0c4a10d8364ec7307210889afccd6a67a6aaa0fd599bd4d"}, ++] ++django-sass-processor = [ ++ {file = "django-sass-processor-0.8.tar.gz", hash = "sha256:e039551994feaaba6fcf880412b25a772dd313162a34cbb4289814988cfae340"}, ++] ++django-select2 = [ ++ {file = "django-select2-7.2.0.tar.gz", hash = "sha256:4c531cb7e9eb4152c7e5f8ab83be386f46978b3d80e91e55ad1fb46382222a0b"}, ++ {file = "django_select2-7.2.0-py2.py3-none-any.whl", hash = "sha256:d17bb0e64503a7e52ba405f73a187664906cefda5f1c33971c67ab0b3891e91c"}, ++] ++django-settings-context-processor = [ ++ {file = "django-settings-context-processor-0.2.tar.gz", hash = "sha256:d37c853d69a3069f5abbf94c7f4f6fc0fac38bbd0524190cd5a250ba800e496a"}, ++] ++django-tables2 = [ ++ {file = "django-tables2-2.2.1.tar.gz", hash = "sha256:0d9b17f5c030ba1b5fcaeb206d8397bf58f1fdfc6beaf56e7874841b8647aa94"}, ++ {file = "django_tables2-2.2.1-py2.py3-none-any.whl", hash = "sha256:6afa0496695e15b332e98537265d09fe01a55b28c75a85323d8e6b0dc2350280"}, ++] ++django-templated-email = [ ++ {file = "django-templated-email-2.3.0.tar.gz", hash = "sha256:536c4e5ae099eabfb9aab36087d4d7799948c654e73da55a744213d086d5bb33"}, ++] ++django-two-factor-auth = [ ++ {file = "django-two-factor-auth-1.10.0.tar.gz", hash = "sha256:3c3af3cd747462be18e7494c4068a2bdc606d7a2d2b2914f8d4590fc80995a71"}, ++ {file = "django_two_factor_auth-1.10.0-py2.py3-none-any.whl", hash = "sha256:0945260fa84e4522d8fa951c35e401616579fd8564938441614399dc588a1c1f"}, ++] ++django-widget-tweaks = [ ++ {file = "django-widget-tweaks-1.4.5.tar.gz", hash = "sha256:f2e2c9c9be1ccc59061e248dcc2144f4906d594abe1a563902f4bdf6aa14e432"}, ++ {file = "django_widget_tweaks-1.4.5-py2.py3-none-any.whl", hash = "sha256:65c960f3d75008a285e4b10f4d21f9eae4160fd77a0f6097ad545185f8648bd6"}, ++] ++django-yarnpkg = [ ++ {file = "django-yarnpkg-6.0.1.tar.gz", hash = "sha256:aa059347b246c6f242401581d2c129bdcb45aa726be59fe2f288762a9843348a"}, ++] ++dynaconf = [ ++ {file = "dynaconf-2.2.2-py2.py3-none-any.whl", hash = "sha256:3cfc1ad7efae08b9cf91d81043a3230175e74e01157d875ce5ec6709bf4e6b9d"}, ++ {file = "dynaconf-2.2.2.tar.gz", hash = "sha256:4bac78b432e090d8ed66f1c23fb32e03ca91a590bf0a51ac36137e0e45ac31ca"}, ++] ++easy-thumbnails = [ ++ {file = "easy-thumbnails-2.7.tar.gz", hash = "sha256:e4e7a0dd4001f56bfd4058428f2c91eafe27d33ef3b8b33ac4e013b159b9ff91"}, ++] ++faker = [ ++ {file = "Faker-4.0.0-py3-none-any.whl", hash = "sha256:047d4d1791bfb3756264da670d99df13d799bb36e7d88774b1585a82d05dbaec"}, ++ {file = "Faker-4.0.0.tar.gz", hash = "sha256:1b1a58961683b30c574520d0c739c4443e0ef6a185c04382e8cc888273dbebed"}, ++] ++feedparser = [ ++ {file = "feedparser-5.2.1.tar.bz2", hash = "sha256:ce875495c90ebd74b179855449040003a1beb40cd13d5f037a0654251e260b02"}, ++ {file = "feedparser-5.2.1.tar.gz", hash = "sha256:bd030652c2d08532c034c27fcd7c85868e7fa3cb2b17f230a44a6bbc92519bf9"}, ++ {file = "feedparser-5.2.1.zip", hash = "sha256:cd2485472e41471632ed3029d44033ee420ad0b57111db95c240c9160a85831c"}, ++] ++html2text = [ ++ {file = "html2text-2019.9.26-py3-none-any.whl", hash = "sha256:55ce85704f244fc18890c5ded89fa22ff7333e41e9f3cad04d51f48d62ad8834"}, ++ {file = "html2text-2019.9.26.tar.gz", hash = "sha256:6f56057c5c2993b5cc5b347cb099bdf6d095828fef1b53ef4e2a2bf2a1be9b4f"}, ++] ++idna = [ ++ {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, ++ {file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"}, ++] ++libsass = [ ++ {file = "libsass-0.19.4-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:74acd9adf506142699dfa292f0e569fdccbd9e7cf619e8226f7117de73566e32"}, ++ {file = "libsass-0.19.4-cp27-cp27m-win32.whl", hash = "sha256:50778d4be269a021ba2bf42b5b8f6ff3704ab96a82175a052680bddf3ba7cc9f"}, ++ {file = "libsass-0.19.4-cp27-cp27m-win_amd64.whl", hash = "sha256:4dcfd561fb100250b89496e1362b96f2cc804f689a59731eb0f94f9a9e144f4a"}, ++ {file = "libsass-0.19.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:845a9573b25c141164972d498855f4ad29367c09e6d76fad12955ad0e1c83013"}, ++ {file = "libsass-0.19.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:81a013a4c2a614927fd1ef7a386eddabbba695cbb02defe8f31cf495106e974c"}, ++ {file = "libsass-0.19.4-cp35-cp35m-win32.whl", hash = "sha256:fcb7ab4dc81889e5fc99cafbc2017bc76996f9992fc6b175f7a80edac61d71df"}, ++ {file = "libsass-0.19.4-cp35-cp35m-win_amd64.whl", hash = "sha256:fc5f8336750f76f1bfae82f7e9e89ae71438d26fc4597e3ab4c05ca8fcd41d8a"}, ++ {file = "libsass-0.19.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:9b59afa0d755089c4165516400a39a289b796b5612eeef5736ab7a1ebf96a67c"}, ++ {file = "libsass-0.19.4-cp36-cp36m-win32.whl", hash = "sha256:c93df526eeef90b1ea4799c1d33b6cd5aea3e9f4633738fb95c1287c13e6b404"}, ++ {file = "libsass-0.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:0fd8b4337b3b101c6e6afda9112cc0dc4bacb9133b59d75d65968c7317aa3272"}, ++ {file = "libsass-0.19.4-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:003a65b4facb4c5dbace53fb0f70f61c5aae056a04b4d112a198c3c9674b31f2"}, ++ {file = "libsass-0.19.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:338e9ae066bf1fde874e335324d5355c52d2081d978b4f74fc59536564b35b08"}, ++ {file = "libsass-0.19.4-cp37-cp37m-win32.whl", hash = "sha256:e318f06f06847ff49b1f8d086ac9ebce1e63404f7ea329adab92f4f16ba0e00e"}, ++ {file = "libsass-0.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a7e685466448c9b1bf98243339793978f654a1151eb5c975f09b83c7a226f4c1"}, ++ {file = "libsass-0.19.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6a51393d75f6e3c812785b0fa0b7d67c54258c28011921f204643b55f7355ec0"}, ++ {file = "libsass-0.19.4.tar.gz", hash = "sha256:8b5b6d1a7c4ea1d954e0982b04474cc076286493f6af2d0a13c2e950fbe0be95"}, ++] ++phonenumbers = [ ++ {file = "phonenumbers-8.11.1-py2.py3-none-any.whl", hash = "sha256:aaa19bc1f2c7efbf7a94be33558e0c5b71620377c9271692d3e314c558962460"}, ++ {file = "phonenumbers-8.11.1.tar.gz", hash = "sha256:239507184ee5b1b83557005af1d5fcce70f83ae18f5dff45b94a67226db10d63"}, ++] ++pillow = [ ++ {file = "Pillow-7.0.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:5f3546ceb08089cedb9e8ff7e3f6a7042bb5b37c2a95d392fb027c3e53a2da00"}, ++ {file = "Pillow-7.0.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:9d2ba4ed13af381233e2d810ff3bab84ef9f18430a9b336ab69eaf3cd24299ff"}, ++ {file = "Pillow-7.0.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ff3797f2f16bf9d17d53257612da84dd0758db33935777149b3334c01ff68865"}, ++ {file = "Pillow-7.0.0-cp35-cp35m-win32.whl", hash = "sha256:c18f70dc27cc5d236f10e7834236aff60aadc71346a5bc1f4f83a4b3abee6386"}, ++ {file = "Pillow-7.0.0-cp35-cp35m-win_amd64.whl", hash = "sha256:875358310ed7abd5320f21dd97351d62de4929b0426cdb1eaa904b64ac36b435"}, ++ {file = "Pillow-7.0.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:ab76e5580b0ed647a8d8d2d2daee170e8e9f8aad225ede314f684e297e3643c2"}, ++ {file = "Pillow-7.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a62ec5e13e227399be73303ff301f2865bf68657d15ea50b038d25fc41097317"}, ++ {file = "Pillow-7.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8ac6ce7ff3892e5deaab7abaec763538ffd011f74dc1801d93d3c5fc541feee2"}, ++ {file = "Pillow-7.0.0-cp36-cp36m-win32.whl", hash = "sha256:91b710e3353aea6fc758cdb7136d9bbdcb26b53cefe43e2cba953ac3ee1d3313"}, ++ {file = "Pillow-7.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:bf598d2e37cf8edb1a2f26ed3fb255191f5232badea4003c16301cb94ac5bdd0"}, ++ {file = "Pillow-7.0.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:5bfef0b1cdde9f33881c913af14e43db69815c7e8df429ceda4c70a5e529210f"}, ++ {file = "Pillow-7.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:dc058b7833184970d1248135b8b0ab702e6daa833be14035179f2acb78ff5636"}, ++ {file = "Pillow-7.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c5ed816632204a2fc9486d784d8e0d0ae754347aba99c811458d69fcdfd2a2f9"}, ++ {file = "Pillow-7.0.0-cp37-cp37m-win32.whl", hash = "sha256:54ebae163e8412aff0b9df1e88adab65788f5f5b58e625dc5c7f51eaf14a6837"}, ++ {file = "Pillow-7.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:87269cc6ce1e3dee11f23fa515e4249ae678dbbe2704598a51cee76c52e19cda"}, ++ {file = "Pillow-7.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0a628977ac2e01ca96aaae247ec2bd38e729631ddf2221b4b715446fd45505be"}, ++ {file = "Pillow-7.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:62a889aeb0a79e50ecf5af272e9e3c164148f4bd9636cc6bcfa182a52c8b0533"}, ++ {file = "Pillow-7.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bf4003aa538af3f4205c5fac56eacaa67a6dd81e454ffd9e9f055fff9f1bc614"}, ++ {file = "Pillow-7.0.0-cp38-cp38-win32.whl", hash = "sha256:7406f5a9b2fd966e79e6abdaf700585a4522e98d6559ce37fc52e5c955fade0a"}, ++ {file = "Pillow-7.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:5f7ae9126d16194f114435ebb79cc536b5682002a4fa57fa7bb2cbcde65f2f4d"}, ++ {file = "Pillow-7.0.0-pp373-pypy36_pp73-win32.whl", hash = "sha256:8453f914f4e5a3d828281a6628cf517832abfa13ff50679a4848926dac7c0358"}, ++ {file = "Pillow-7.0.0.tar.gz", hash = "sha256:4d9ed9a64095e031435af120d3c910148067087541131e82b3e8db302f4c8946"}, ++] ++psycopg2 = [ ++ {file = "psycopg2-2.8.4-cp27-cp27m-win32.whl", hash = "sha256:72772181d9bad1fa349792a1e7384dde56742c14af2b9986013eb94a240f005b"}, ++ {file = "psycopg2-2.8.4-cp27-cp27m-win_amd64.whl", hash = "sha256:893c11064b347b24ecdd277a094413e1954f8a4e8cdaf7ffbe7ca3db87c103f0"}, ++ {file = "psycopg2-2.8.4-cp34-cp34m-win32.whl", hash = "sha256:9ab75e0b2820880ae24b7136c4d230383e07db014456a476d096591172569c38"}, ++ {file = "psycopg2-2.8.4-cp34-cp34m-win_amd64.whl", hash = "sha256:b0845e3bdd4aa18dc2f9b6fb78fbd3d9d371ad167fd6d1b7ad01c0a6cdad4fc6"}, ++ {file = "psycopg2-2.8.4-cp35-cp35m-win32.whl", hash = "sha256:ef6df7e14698e79c59c7ee7cf94cd62e5b869db369ed4b1b8f7b729ea825712a"}, ++ {file = "psycopg2-2.8.4-cp35-cp35m-win_amd64.whl", hash = "sha256:965c4c93e33e6984d8031f74e51227bd755376a9df6993774fd5b6fb3288b1f4"}, ++ {file = "psycopg2-2.8.4-cp36-cp36m-win32.whl", hash = "sha256:ed686e5926929887e2c7ae0a700e32c6129abb798b4ad2b846e933de21508151"}, ++ {file = "psycopg2-2.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:dca2d7203f0dfce8ea4b3efd668f8ea65cd2b35112638e488a4c12594015f67b"}, ++ {file = "psycopg2-2.8.4-cp37-cp37m-win32.whl", hash = "sha256:8396be6e5ff844282d4d49b81631772f80dabae5658d432202faf101f5283b7c"}, ++ {file = "psycopg2-2.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:47fc642bf6f427805daf52d6e52619fe0637648fe27017062d898f3bf891419d"}, ++ {file = "psycopg2-2.8.4-cp38-cp38-win32.whl", hash = "sha256:4212ca404c4445dc5746c0d68db27d2cbfb87b523fe233dc84ecd24062e35677"}, ++ {file = "psycopg2-2.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:92a07dfd4d7c325dd177548c4134052d4842222833576c8391aab6f74038fc3f"}, ++ {file = "psycopg2-2.8.4.tar.gz", hash = "sha256:f898e5cc0a662a9e12bde6f931263a1bbd350cfb18e1d5336a12927851825bb6"}, ++] ++pycryptodome = [ ++ {file = "pycryptodome-3.9.4-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:6c2720696b10ae356040e888bde1239b8957fe18885ccf5e7b4e8dec882f0856"}, ++ {file = "pycryptodome-3.9.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:5c485ed6e9718ebcaa81138fa70ace9c563d202b56a8cee119b4085b023931f5"}, ++ {file = "pycryptodome-3.9.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:56fdd0e425f1b8fd3a00b6d96351f86226674974814c50534864d0124d48871f"}, ++ {file = "pycryptodome-3.9.4-cp27-cp27m-win32.whl", hash = "sha256:2de33ed0a95855735d5a0fc0c39603314df9e78ee8bbf0baa9692fb46b3b8bbb"}, ++ {file = "pycryptodome-3.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:eec0689509389f19875f66ae8dedd59f982240cdab31b9f78a8dc266011df93a"}, ++ {file = "pycryptodome-3.9.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:68fab8455efcbfe87c5d75015476f9b606227ffe244d57bfd66269451706e899"}, ++ {file = "pycryptodome-3.9.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4b9533d4166ca07abdd49ce9d516666b1df944997fe135d4b21ac376aa624aff"}, ++ {file = "pycryptodome-3.9.4-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:d3fe3f33ad52bf0c19ee6344b695ba44ffbfa16f3c29ca61116b48d97bd970fb"}, ++ {file = "pycryptodome-3.9.4-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:319e568baf86620b419d53063b18c216abf924875966efdfe06891b987196a45"}, ++ {file = "pycryptodome-3.9.4-cp34-cp34m-win32.whl", hash = "sha256:042ae873baadd0c33b4d699a5c5b976ade3233a979d972f98ca82314632d868c"}, ++ {file = "pycryptodome-3.9.4-cp34-cp34m-win_amd64.whl", hash = "sha256:a30f501bbb32e01a49ef9e09ca1260e5ab49bf33a257080ec553e08997acc487"}, ++ {file = "pycryptodome-3.9.4-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:b55c60c321ac91945c60a40ac9896ac7a3d432bb3e8c14006dfd82ad5871c331"}, ++ {file = "pycryptodome-3.9.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:9d9945ac8375d5d8e60bd2a2e1df5882eaa315522eedf3ca868b1546dfa34eba"}, ++ {file = "pycryptodome-3.9.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:4372ec7518727172e1605c0843cdc5375d4771e447b8148c787b860260aae151"}, ++ {file = "pycryptodome-3.9.4-cp35-cp35m-win32.whl", hash = "sha256:0502876279772b1384b660ccc91563d04490d562799d8e2e06b411e2d81128a9"}, ++ {file = "pycryptodome-3.9.4-cp35-cp35m-win_amd64.whl", hash = "sha256:72166c2ac520a5dbd2d90208b9c279161ec0861662a621892bd52fb6ca13ab91"}, ++ {file = "pycryptodome-3.9.4-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:b4af098f2a50f8d048ab12cabb59456585c0acf43d90ee79782d2d6d0ed59dba"}, ++ {file = "pycryptodome-3.9.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:8a799bea3c6617736e914a2e77c409f52893d382f619f088f8a80e2e21f573c1"}, ++ {file = "pycryptodome-3.9.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:7c52308ac5b834331b2f107a490b2c27de024a229b61df4cdc5c131d563dfe98"}, ++ {file = "pycryptodome-3.9.4-cp36-cp36m-win32.whl", hash = "sha256:63c103a22cbe9752f6ea9f1a0de129995bad91c4d03a66c67cffcf6ee0c9f1e1"}, ++ {file = "pycryptodome-3.9.4-cp36-cp36m-win_amd64.whl", hash = "sha256:54456cf85130e01674d21fb1ab89ffccacb138a8ade88d72fa2b0ac898d2798b"}, ++ {file = "pycryptodome-3.9.4-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:aec4d42deb836b8fb3ba32f2ba1ef0d33dd3dc9d430b1479ee7a914490d15b5e"}, ++ {file = "pycryptodome-3.9.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:896e9b6fd0762aa07b203c993fbbee7a1f1a4674c6886afd7bfa86f3d1be98a8"}, ++ {file = "pycryptodome-3.9.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:57b1b707363490c495ad0eeb38bd1b0e1697c497af25fad78d3a1ebf0477fd5b"}, ++ {file = "pycryptodome-3.9.4-cp37-cp37m-win32.whl", hash = "sha256:87d8d85b4792ca5e730fb7a519fbc3ed976c59dcf79c5204589c59afd56b9926"}, ++ {file = "pycryptodome-3.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:e3a79a30d15d9c7c284a7734036ee8abdb5ca3a6f5774d293cdc9e1358c1dc10"}, ++ {file = "pycryptodome-3.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:48821950ffb9c836858d8fa09d7840b6df52eadd387a3c5acece55cb387743f9"}, ++ {file = "pycryptodome-3.9.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cbfd97f9e060f0d30245cd29fa267a9a84de9da97559366fca0a3f7655acc63f"}, ++ {file = "pycryptodome-3.9.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9ef966c727de942de3e41aa8462c4b7b4bca70f19af5a3f99e31376589c11aac"}, ++ {file = "pycryptodome-3.9.4-cp38-cp38-win32.whl", hash = "sha256:a8ca2450394d3699c9f15ef25e8de9a24b401933716a1e39d37fa01f5fe3c58b"}, ++ {file = "pycryptodome-3.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:c53348358408d94869059e16fba5ff3bef8c52c25b18421472aba272b9bb450f"}, ++ {file = "pycryptodome-3.9.4.tar.gz", hash = "sha256:a168e73879619b467072509a223282a02c8047d932a48b74fbd498f27224aa04"}, ++] ++pyjwt = [ ++ {file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"}, ++ {file = "PyJWT-1.7.1.tar.gz", hash = "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"}, ++] ++python-box = [ ++ {file = "python-box-3.4.6.tar.gz", hash = "sha256:694a7555e3ff9fbbce734bbaef3aad92b8e4ed0659d3ed04d56b6a0a0eff26a9"}, ++ {file = "python_box-3.4.6-py2.py3-none-any.whl", hash = "sha256:a71d3dc9dbaa34c8597d3517c89a8041bd62fa875f23c0f3dad55e1958e3ce10"}, ++] ++python-dateutil = [ ++ {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, ++ {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, ++] ++python-dotenv = [ ++ {file = "python-dotenv-0.10.3.tar.gz", hash = "sha256:f157d71d5fec9d4bd5f51c82746b6344dffa680ee85217c123f4a0c8117c4544"}, ++ {file = "python_dotenv-0.10.3-py2.py3-none-any.whl", hash = "sha256:debd928b49dbc2bf68040566f55cdb3252458036464806f4094487244e2a4093"}, ++] ++python-memcached = [ ++ {file = "python-memcached-1.59.tar.gz", hash = "sha256:a2e28637be13ee0bf1a8b6843e7490f9456fd3f2a4cb60471733c7b5d5557e4f"}, ++ {file = "python_memcached-1.59-py2.py3-none-any.whl", hash = "sha256:4dac64916871bd3550263323fc2ce18e1e439080a2d5670c594cf3118d99b594"}, ++] ++pytz = [ ++ {file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"}, ++ {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"}, ++] ++pyyaml = [ ++ {file = "PyYAML-5.3-cp27-cp27m-win32.whl", hash = "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d"}, ++ {file = "PyYAML-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6"}, ++ {file = "PyYAML-5.3-cp35-cp35m-win32.whl", hash = "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e"}, ++ {file = "PyYAML-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689"}, ++ {file = "PyYAML-5.3-cp36-cp36m-win32.whl", hash = "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994"}, ++ {file = "PyYAML-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e"}, ++ {file = "PyYAML-5.3-cp37-cp37m-win32.whl", hash = "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5"}, ++ {file = "PyYAML-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf"}, ++ {file = "PyYAML-5.3-cp38-cp38-win32.whl", hash = "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811"}, ++ {file = "PyYAML-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20"}, ++ {file = "PyYAML-5.3.tar.gz", hash = "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"}, ++] ++qrcode = [ ++ {file = "qrcode-6.1-py2.py3-none-any.whl", hash = "sha256:3996ee560fc39532910603704c82980ff6d4d5d629f9c3f25f34174ce8606cf5"}, ++ {file = "qrcode-6.1.tar.gz", hash = "sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369"}, ++] ++requests = [ ++ {file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"}, ++ {file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"}, ++] ++six = [ ++ {file = "six-1.13.0-py2.py3-none-any.whl", hash = "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd"}, ++ {file = "six-1.13.0.tar.gz", hash = "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"}, ++] ++soupsieve = [ ++ {file = "soupsieve-1.9.5-py2.py3-none-any.whl", hash = "sha256:bdb0d917b03a1369ce964056fc195cfdff8819c40de04695a80bc813c3cfa1f5"}, ++ {file = "soupsieve-1.9.5.tar.gz", hash = "sha256:e2c1c5dee4a1c36bcb790e0fabd5492d874b8ebd4617622c4f6a731701060dda"}, ++] ++sqlparse = [ ++ {file = "sqlparse-0.3.0-py2.py3-none-any.whl", hash = "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177"}, ++ {file = "sqlparse-0.3.0.tar.gz", hash = "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873"}, ++] ++text-unidecode = [ ++ {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, ++ {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, ++] ++toml = [ ++ {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, ++ {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, ++ {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, ++] ++tqdm = [ ++ {file = "tqdm-4.41.1-py2.py3-none-any.whl", hash = "sha256:efab950cf7cc1e4d8ee50b2bb9c8e4a89f8307b49e0b2c9cfef3ec4ca26655eb"}, ++ {file = "tqdm-4.41.1.tar.gz", hash = "sha256:4789ccbb6fc122b5a6a85d512e4e41fc5acad77216533a6f2b8ce51e0f265c23"}, ++] ++twilio = [ ++ {file = "twilio-6.35.2.tar.gz", hash = "sha256:a086443642c0e1f13c8f8f087b426ca81ec883efbe496d8279180a49bb9287bc"}, ++] ++urllib3 = [ ++ {file = "urllib3-1.25.7-py2.py3-none-any.whl", hash = "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293"}, ++ {file = "urllib3-1.25.7.tar.gz", hash = "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"}, ++] ++yubiotp = [ ++ {file = "YubiOTP-0.2.2.post1-py2.py3-none-any.whl", hash = "sha256:7e281801b24678f4bda855ce8ab975a7688a912f5a6cb22b6c2b16263a93cbd2"}, ++ {file = "YubiOTP-0.2.2.post1.tar.gz", hash = "sha256:de83b1560226e38b5923f6ab919f962c8c2abb7c722104cb45b2b6db2ac86e40"}, ++] +Index: AlekSIS/aleksis/core/templates/core/index.html +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/aleksis/core/templates/core/index.html (revision 9648eaaf23a9e45ba5ab180b973d79d8f2141efb) ++++ AlekSIS/aleksis/core/templates/core/index.html (revision e7ac0ec18d65e778e7d90e2731c22e8e1f447145) +@@ -22,6 +22,14 @@ + </div> + {% endfor %} + ++ <div class="row"> ++ {% for widget in widgets %} ++ <div class="col s12 m12 l6 xl4"> ++ {{ widget }} ++ </div> ++ {% endfor %} ++ </div> ++ + <div class="row"> + <div class="col s12 m6"> + <h5>{% blocktrans %}Last activities{% endblocktrans %}</h5> +Index: AlekSIS/aleksis/core/views.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/aleksis/core/views.py (revision 9648eaaf23a9e45ba5ab180b973d79d8f2141efb) ++++ AlekSIS/aleksis/core/views.py (revision e7ac0ec18d65e778e7d90e2731c22e8e1f447145) +@@ -20,6 +20,8 @@ + from .tables import GroupsTable, PersonsTable + from .util import messages + ++from aleksis.apps.dashboardfeeds.views import get_widgets ++ + + @person_required + def index(request: HttpRequest) -> HttpResponse: +@@ -33,6 +35,8 @@ + context["notifications"] = notifications + context["unread_notifications"] = unread_notifications + ++ context["widgets"] = get_widgets(request) ++ + return render(request, "core/index.html", context) + + +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/migrations/0002_dashboardwidget_base_url.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/migrations/0002_dashboardwidget_base_url.py (revision f320d5a89055ce5e0f9e7c97a072a15443dc25d2) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/migrations/0002_dashboardwidget_base_url.py (revision f320d5a89055ce5e0f9e7c97a072a15443dc25d2) +@@ -0,0 +1,19 @@ ++# Generated by Django 3.0.2 on 2020-01-13 21:07 ++ ++from django.db import migrations, models ++ ++ ++class Migration(migrations.Migration): ++ ++ dependencies = [ ++ ('dashboardfeeds', '0001_initial'), ++ ] ++ ++ operations = [ ++ migrations.AddField( ++ model_name='dashboardwidget', ++ name='base_url', ++ field=models.URLField(default='example.com', help_text='index url of the news website (as link for users)', verbose_name='Base URL'), ++ preserve_default=False, ++ ), ++ ] +Index: AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/templates/dashboardfeeds/rss.html +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/templates/dashboardfeeds/rss.html (revision f320d5a89055ce5e0f9e7c97a072a15443dc25d2) ++++ AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/templates/dashboardfeeds/rss.html (revision f320d5a89055ce5e0f9e7c97a072a15443dc25d2) +@@ -0,0 +1,18 @@ ++{% load i18n static %} ++ ++<div class="card"> ++ <div class="card-image"> ++ <span class="badge-image z-depth-2">{{ title }}</span> ++ {# <img src="{{ result.enclosures.0.href }}" alt="{{ title }} – {% trans "title image" %}">#} ++ <img src="{{ result.enclosures.0.href }}" alt="{{ title }} – {% trans "title image" %}" ++ onerror="this.src='{% static "dashboardfeeds/image_not_found.png" %}'; "/> ++ </div> ++ {# THIS DOSEN'T WOKR! #} ++ <div class="card-content"><span class="card-title">{{ result.title }}</span> ++ {{ result.summary|safe }} ++ </div> ++ <div class="card-action"> ++ <a href="{{ result.link }}" target="_blank">Mehr lesen</a> ++ </div> ++</div><a class="btn hundred-percent primary-color" href="{{ base_url }}" target="_blank">Weitere ++ Artikel<i class="material-icons right">arrow_forward</i></a> +diff --git AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/locale/.keepdir AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/locale/.keepdir +new file mode 100644 +index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 +GIT binary patch +literal 0 +Hc$@<O00001 + +diff --git AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/static/.keepdir AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/static/.keepdir +new file mode 100644 +index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 +GIT binary patch +literal 0 +Hc$@<O00001 + +diff --git AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/static/dashboardfeeds/image_not_found.png AlekSIS/apps/official/AlekSIS-App-DashboardFeeds/aleksis/apps/dashboardfeeds/static/dashboardfeeds/image_not_found.png +new file mode 100644 +index 0000000000000000000000000000000000000000..8ad0620f8b0954ae3676ddc706e112de2042d1e4 +GIT binary patch +literal 51251 +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 +XPGy)sPP_eOG=6vC>~<`)KaltzO8-p~ + diff --git a/dev.sh b/dev.sh index 51884395620140fb107680066825d668c61bf55c..a9bae65132dd9c9d48ac8a38f67053e4af476886 100755 --- a/dev.sh +++ b/dev.sh @@ -29,6 +29,7 @@ case "$1" in for d in aleksis/core apps/official/*/aleksis/apps/*; do echo; echo "Entering $d." poetry run sh -c "cd $d; $manage_py makemessages --no-wrap -i static $locales" + poetry run sh -c "cd $d; $manage_py makemessages --no-wrap -d djangojs $locales" done exit ;; diff --git a/poetry.lock b/poetry.lock index 7786453239bf59ce4ec4036ff3c268856eadd108..b556a0d95660861c9662d33e3c3437e3e486aea8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,6 +6,17 @@ optional = false python-versions = "*" version = "0.7.12" +[[package]] +category = "main" +description = "Low-level AMQP client for Python (fork of amqplib)." +name = "amqp" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.5.2" + +[package.dependencies] +vine = ">=1.1.3,<5.0.0a1" + [[package]] category = "dev" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." @@ -100,6 +111,14 @@ soupsieve = ">=1.2" html5lib = ["html5lib"] lxml = ["lxml"] +[[package]] +category = "main" +description = "Python multiprocessing fork with improvements and bugfixes" +name = "billiard" +optional = true +python-versions = "*" +version = "3.6.1.0" + [[package]] category = "dev" description = "The uncompromising code formatter." @@ -120,6 +139,73 @@ typed-ast = ">=1.4.0" [package.extras] d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] +[[package]] +category = "main" +description = "Utilities for working with calendar weeks in Python and Django" +name = "calendarweek" +optional = false +python-versions = ">=3.7,<4.0" +version = "0.4.4" + +[package.extras] +django = ["Django (>=2.2,<4.0)"] + +[[package]] +category = "main" +description = "Distributed Task Queue." +name = "celery" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*," +version = "4.4.0" + +[package.dependencies] +billiard = ">=3.6.1,<4.0" +kombu = ">=4.6.7,<4.7" +pytz = ">0.0-dev" +vine = "1.3.0" + +[package.dependencies.Django] +optional = true +version = ">=1.11" + +[package.dependencies.redis] +optional = true +version = ">=3.2.0" + +[package.extras] +arangodb = ["pyArango (>=1.3.2)"] +auth = ["cryptography"] +azureblockblob = ["azure-storage (0.36.0)", "azure-common (1.1.5)", "azure-storage-common (1.1.0)"] +brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] +cassandra = ["cassandra-driver"] +consul = ["python-consul"] +cosmosdbsql = ["pydocumentdb (2.3.2)"] +couchbase = ["couchbase", "couchbase-cffi"] +couchdb = ["pycouchdb"] +django = ["Django (>=1.11)"] +dynamodb = ["boto3 (>=1.9.178)"] +elasticsearch = ["elasticsearch"] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent"] +librabbitmq = ["librabbitmq (>=1.5.0)"] +lzma = ["backports.lzma"] +memcache = ["pylibmc"] +mongodb = ["pymongo (>=3.3.0)"] +msgpack = ["msgpack"] +pymemcache = ["python-memcached"] +pyro = ["pyro4"] +redis = ["redis (>=3.2.0)"] +riak = ["riak (>=2.0)"] +s3 = ["boto3 (>=1.9.125)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +solar = ["ephem"] +sqlalchemy = ["sqlalchemy"] +sqs = ["boto3 (>=1.9.125)", "pycurl"] +tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=1.3.1)"] +zstd = ["zstandard"] + [[package]] category = "main" description = "Python package for providing Mozilla's CA Bundle." @@ -172,16 +258,13 @@ optional = false python-versions = "*" version = "5.0.6" -[package.dependencies] -six = "*" - [[package]] category = "dev" description = "Code coverage measurement for Python" name = "coverage" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "5.0.2" +version = "5.0.3" [package.extras] toml = ["toml"] @@ -268,6 +351,53 @@ version = "2.2.0" [package.dependencies] Django = ">=1.8" +[[package]] +category = "main" +description = "Database-backed Periodic Tasks." +name = "django-celery-beat" +optional = true +python-versions = "*" +version = "1.5.0" + +[package.dependencies] +django-timezone-field = ">=2.0" +python-crontab = ">=2.3.4" + +[[package]] +category = "main" +description = "An async Django email backend using celery" +name = "django-celery-email" +optional = true +python-versions = "*" +version = "3.0.0" + +[package.dependencies] +celery = ">=4.0" +django = ">=2.2" +django-appconf = "*" + +[[package]] +category = "main" +description = "Celery result backends for Django." +name = "django-celery-results" +optional = true +python-versions = "*" +version = "1.1.2" + +[package.dependencies] +celery = ">=4.3,<5.0" + +[[package]] +category = "main" +description = "Django admin CKEditor integration." +name = "django-ckeditor" +optional = false +python-versions = "*" +version = "5.8.0" + +[package.dependencies] +django-js-asset = ">=1.2.2" + [[package]] category = "main" description = "Django live settings with pluggable backends, including Redis." @@ -372,6 +502,37 @@ optional = false python-versions = "*" version = "2.1.0" +[[package]] +category = "main" +description = "script tag with additional attributes for django.forms.Media" +name = "django-js-asset" +optional = false +python-versions = "*" +version = "1.2.2" + +[[package]] +category = "main" +description = "Javascript url handling for Django that doesn't hurt." +name = "django-js-reverse" +optional = false +python-versions = "*" +version = "0.9.1" + +[package.dependencies] +Django = ">=1.5" + +[[package]] +category = "main" +description = "Expose JSONField data as a virtual django model fields." +name = "django-jsonstore" +optional = false +python-versions = "*" +version = "0.4.1" + +[package.dependencies] +Django = ">=1.11" +six = "*" + [[package]] category = "main" description = "django-maintenance-mode shows a 503 error page when maintenance-mode is on." @@ -479,6 +640,17 @@ version = "1.0.6" [package.dependencies] django = ">=1.8" +[[package]] +category = "main" +description = "Render a particular block from a template to a string." +name = "django-render-block" +optional = false +python-versions = "*" +version = "0.6" + +[package.dependencies] +django = ">=1.11" + [[package]] category = "main" description = "SASS processor to compile SCSS files into *.css, while rendering, or offline." @@ -538,6 +710,30 @@ Django = ">=1.11" [package.extras] tablib = ["tablib"] +[[package]] +category = "main" +description = "A Django oriented templated / transaction email abstraction" +name = "django-templated-email" +optional = false +python-versions = "*" +version = "2.3.0" + +[package.dependencies] +django-render-block = ">=0.5" +six = ">=1" + +[[package]] +category = "main" +description = "A Django app providing database and form fields for pytz timezone objects." +name = "django-timezone-field" +optional = true +python-versions = ">=3.5" +version = "4.0" + +[package.dependencies] +django = ">=2.2" +pytz = "*" + [[package]] category = "main" description = "Complete Two-Factor Authentication for Django" @@ -597,8 +793,8 @@ category = "dev" description = "Docutils -- Python Documentation Utilities" name = "docutils" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "0.15.2" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.16" [[package]] category = "dev" @@ -678,12 +874,11 @@ category = "main" description = "Faker is a Python package that generates fake data for you." name = "faker" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "3.0.0" +python-versions = ">=3.4" +version = "4.0.0" [package.dependencies] python-dateutil = ">=2.4" -six = ">=1.10" text-unidecode = "1.3" [[package]] @@ -862,6 +1057,14 @@ python-dateutil = "*" six = ">1.5" tatsu = ">4.2" +[[package]] +category = "main" +description = "Turn HTML into equivalent Markdown-structured text." +name = "html2text" +optional = false +python-versions = ">=3.5" +version = "2020.1.16" + [[package]] category = "main" description = "Internationalized Domain Names in Applications (IDNA)" @@ -879,13 +1082,13 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "1.2.0" [[package]] -category = "dev" +category = "main" description = "Read metadata from Python packages" marker = "python_version < \"3.8\"" name = "importlib-metadata" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.3.0" +version = "1.4.0" [package.dependencies] zipp = ">=0.5" @@ -922,6 +1125,37 @@ MarkupSafe = ">=0.23" [package.extras] i18n = ["Babel (>=0.8)"] +[[package]] +category = "main" +description = "Messaging library for Python." +name = "kombu" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "4.6.7" + +[package.dependencies] +amqp = ">=2.5.2,<2.6" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.18" + +[package.extras] +azureservicebus = ["azure-servicebus (>=0.21.1)"] +azurestoragequeues = ["azure-storage-queue"] +consul = ["python-consul (>=0.6.0)"] +librabbitmq = ["librabbitmq (>=1.5.2)"] +mongodb = ["pymongo (>=3.3.0)"] +msgpack = ["msgpack"] +pyro = ["pyro4"] +qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] +redis = ["redis (>=3.3.11)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +sqlalchemy = ["sqlalchemy"] +sqs = ["boto3 (>=1.4.4)", "pycurl (7.43.0.2)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=1.3.1)"] + [[package]] category = "main" description = "Sass for Python: A straightforward binding of libsass for Python." @@ -950,12 +1184,12 @@ python-versions = "*" version = "0.6.1" [[package]] -category = "dev" +category = "main" description = "More routines for operating on iterables, beyond itertools" name = "more-itertools" optional = false python-versions = ">=3.5" -version = "8.0.2" +version = "8.1.0" [[package]] category = "dev" @@ -1026,7 +1260,7 @@ description = "Python version of Google's common library for parsing, formatting name = "phonenumbers" optional = false python-versions = "*" -version = "8.11.1" +version = "8.11.2" [[package]] category = "main" @@ -1157,7 +1391,7 @@ description = "pytest: simple powerful testing with Python" name = "pytest" optional = false python-versions = ">=3.5" -version = "5.3.2" +version = "5.3.3" [package.dependencies] atomicwrites = ">=1.0" @@ -1174,6 +1408,7 @@ python = "<3.8" version = ">=0.12" [package.extras] +checkqa-mypy = ["mypy (v0.761)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] @@ -1197,7 +1432,7 @@ description = "A Django plugin for pytest." name = "pytest-django" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.7.0" +version = "3.8.0" [package.dependencies] pytest = ">=3.6" @@ -1242,6 +1477,21 @@ version = "3.4.6" [package.extras] testing = ["pytest", "coverage (>=3.6)", "pytest-cov"] +[[package]] +category = "main" +description = "Python Crontab API" +name = "python-crontab" +optional = true +python-versions = "*" +version = "2.4.0" + +[package.dependencies] +python-dateutil = "*" + +[package.extras] +cron-description = ["cron-descriptor"] +cron-schedule = ["croniter"] + [[package]] category = "main" description = "Extensions to the standard Python datetime module" @@ -1300,7 +1550,7 @@ category = "main" description = "YAML parser and emitter for Python" name = "pyyaml" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "*" version = "5.3" [[package]] @@ -1321,6 +1571,17 @@ maintainer = ["zest.releaser"] pil = ["pillow"] test = ["pytest", "pytest-cov", "mock"] +[[package]] +category = "main" +description = "Python client for Redis key-value store" +name = "redis" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.3.11" + +[package.extras] +hiredis = ["hiredis (>=0.1.3)"] + [[package]] category = "dev" description = "Alternative regular expression module, to replace re." @@ -1397,8 +1658,8 @@ category = "main" description = "Python 2 and 3 compatibility utilities" name = "six" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "1.13.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.14.0" [[package]] category = "dev" @@ -1672,7 +1933,7 @@ description = "a fork of Python 2 and 3 ast modules with type comment support" name = "typed-ast" optional = false python-versions = "*" -version = "1.4.0" +version = "1.4.1" [[package]] category = "dev" @@ -1695,6 +1956,14 @@ brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] +[[package]] +category = "main" +description = "Promises, promises, promises." +name = "vine" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.3.0" + [[package]] category = "dev" description = "Measures number of Terminal column cells of wide-character codes" @@ -1716,13 +1985,13 @@ pycryptodome = "*" six = "*" [[package]] -category = "dev" +category = "main" description = "Backport of pathlib-compatible object wrapper for zip files" marker = "python_version < \"3.8\"" name = "zipp" optional = false python-versions = ">=2.7" -version = "0.6.0" +version = "1.0.0" [package.dependencies] more-itertools = "*" @@ -1732,10 +2001,11 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["pathlib2", "contextlib2", "unittest2"] [extras] +celery = ["Celery", "django-celery-results", "django-celery-beat", "django-celery-email"] ldap = ["django-auth-ldap"] [metadata] -content-hash = "750a94f30372594e475cded87f35529d305587771ebdced3f6ec1e88eef4f299" +content-hash = "61847d3ffe7092e41f9ad04eadf9056fc09b6682cc945dce4544020a6b2f1712" python-versions = "^3.7" [metadata.files] @@ -1743,6 +2013,10 @@ alabaster = [ {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, ] +amqp = [ + {file = "amqp-2.5.2-py2.py3-none-any.whl", hash = "sha256:6e649ca13a7df3faacdc8bbb280aa9a6602d22fd9d545336077e573a1f4ff3b8"}, + {file = "amqp-2.5.2.tar.gz", hash = "sha256:77f1aef9410698d20eaeac5b73a87817365f457a507d82edf292e12cbb83b08d"}, +] appdirs = [ {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, @@ -1776,10 +2050,22 @@ beautifulsoup4 = [ {file = "beautifulsoup4-4.8.2-py3-none-any.whl", hash = "sha256:9fbb4d6e48ecd30bcacc5b63b94088192dcda178513b2ae3c394229f8911b887"}, {file = "beautifulsoup4-4.8.2.tar.gz", hash = "sha256:05fd825eb01c290877657a56df4c6e4c311b3965bda790c613a3d6fb01a5462a"}, ] +billiard = [ + {file = "billiard-3.6.1.0-py3-none-any.whl", hash = "sha256:01afcb4e7c4fd6480940cfbd4d9edc19d7a7509d6ada533984d0d0f49901ec82"}, + {file = "billiard-3.6.1.0.tar.gz", hash = "sha256:b8809c74f648dfe69b973c8e660bcec00603758c9db8ba89d7719f88d5f01f26"}, +] black = [ {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, ] +calendarweek = [ + {file = "calendarweek-0.4.4-py3-none-any.whl", hash = "sha256:6510a42015558f140ed6677e79efbb45d8bf87ccded069db4026283eb639a256"}, + {file = "calendarweek-0.4.4.tar.gz", hash = "sha256:02f092ec54ebe162dc9f3614de6efbf3d7fb35115e8ca5d62e99d65c342f5732"}, +] +celery = [ + {file = "celery-4.4.0-py2.py3-none-any.whl", hash = "sha256:7c544f37a84a5eadc44cab1aa8c9580dff94636bb81978cdf9bf8012d9ea7d8f"}, + {file = "celery-4.4.0.tar.gz", hash = "sha256:d3363bb5df72d74420986a435449f3c3979285941dff57d5d97ecba352a0e3e2"}, +] certifi = [ {file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"}, {file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"}, @@ -1804,37 +2090,37 @@ configobj = [ {file = "configobj-5.0.6.tar.gz", hash = "sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902"}, ] coverage = [ - {file = "coverage-5.0.2-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:511ec0c00840e12fb4e852e4db58fa6a01ca4da72f36a9766fae344c3d502033"}, - {file = "coverage-5.0.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:d22b4297e7e4225ccf01f1aa55e7a96412ea0796b532dd614c3fcbafa341128e"}, - {file = "coverage-5.0.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:593853aa1ac6dcc6405324d877544c596c9d948ef20d2e9512a0f5d2d3202356"}, - {file = "coverage-5.0.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:e65a5aa1670db6263f19fdc03daee1d7dbbadb5cb67fd0a1f16033659db13c1d"}, - {file = "coverage-5.0.2-cp27-cp27m-win32.whl", hash = "sha256:d4a2b578a7a70e0c71f662705262f87a456f1e6c1e40ada7ea699abaf070a76d"}, - {file = "coverage-5.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:28f7f73b34a05e23758e860a89a7f649b85c6749e252eff60ebb05532d180e86"}, - {file = "coverage-5.0.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7d1cc7acc9ce55179616cf72154f9e648136ea55987edf84addbcd9886ffeba2"}, - {file = "coverage-5.0.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:2d0cb9b1fe6ad0d915d45ad3d87f03a38e979093a98597e755930db1f897afae"}, - {file = "coverage-5.0.2-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:bfe102659e2ec13b86c7f3b1db6c9a4e7beea4255058d006351339e6b342d5d2"}, - {file = "coverage-5.0.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:23688ff75adfa8bfa2a67254d889f9bdf9302c27241d746e17547c42c732d3f4"}, - {file = "coverage-5.0.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:1bf7ba2af1d373a1750888724f84cffdfc697738f29a353c98195f98fc011509"}, - {file = "coverage-5.0.2-cp35-cp35m-win32.whl", hash = "sha256:569f9ee3025682afda6e9b0f5bb14897c0db03f1a1dc088b083dd36e743f92bb"}, - {file = "coverage-5.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:cf908840896f7aa62d0ec693beb53264b154f972eb8226fb864ac38975590c4f"}, - {file = "coverage-5.0.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:eaad65bd20955131bcdb3967a4dea66b4e4d4ca488efed7c00d91ee0173387e8"}, - {file = "coverage-5.0.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:225e79a5d485bc1642cb7ba02281419c633c216cdc6b26c26494ba959f09e69f"}, - {file = "coverage-5.0.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bd82b684bb498c60ef47bb1541a50e6d006dde8579934dcbdbc61d67d1ea70d9"}, - {file = "coverage-5.0.2-cp36-cp36m-win32.whl", hash = "sha256:7ca3db38a61f3655a2613ee2c190d63639215a7a736d3c64cc7bbdb002ce6310"}, - {file = "coverage-5.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:47874b4711c5aeb295c31b228a758ce3d096be83dc37bd56da48ed99efb8813b"}, - {file = "coverage-5.0.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:955ec084f549128fa2702f0b2dc696392001d986b71acd8fd47424f28289a9c3"}, - {file = "coverage-5.0.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1f4ee8e2e4243971618bc16fcc4478317405205f135e95226c2496e2a3b8dbbf"}, - {file = "coverage-5.0.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f45fba420b94165c17896861bb0e8b27fb7abdcedfeb154895d8553df90b7b00"}, - {file = "coverage-5.0.2-cp37-cp37m-win32.whl", hash = "sha256:cca38ded59105f7705ef6ffe1e960b8db6c7d8279c1e71654a4775ab4454ca15"}, - {file = "coverage-5.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:cb2b74c123f65e8166f7e1265829a6c8ed755c3cd16d7f50e75a83456a5f3fd7"}, - {file = "coverage-5.0.2-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:53e7438fef0c97bc248f88ba1edd10268cd94d5609970aaf87abbe493691af87"}, - {file = "coverage-5.0.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c1e4e39e43057396a5e9d069bfbb6ffeee892e40c5d2effbd8cd71f34ee66c4d"}, - {file = "coverage-5.0.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5b0a07158360d22492f9abd02a0f2ee7981b33f0646bf796598b7673f6bbab14"}, - {file = "coverage-5.0.2-cp38-cp38m-win32.whl", hash = "sha256:88b51153657612aea68fa684a5b88037597925260392b7bb4509d4f9b0bdd889"}, - {file = "coverage-5.0.2-cp38-cp38m-win_amd64.whl", hash = "sha256:189aac76d6e0d7af15572c51892e7326ee451c076c5a50a9d266406cd6c49708"}, - {file = "coverage-5.0.2-cp39-cp39m-win32.whl", hash = "sha256:d095a7b473f8a95f7efe821f92058c8a2ecfb18f8db6677ae3819e15dc11aaae"}, - {file = "coverage-5.0.2-cp39-cp39m-win_amd64.whl", hash = "sha256:ddeb42a3d5419434742bf4cc71c9eaa22df3b76808e23a82bd0b0bd360f1a9f1"}, - {file = "coverage-5.0.2.tar.gz", hash = "sha256:b251c7092cbb6d789d62dc9c9e7c4fb448c9138b51285c36aeb72462cad3600e"}, + {file = "coverage-5.0.3-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f"}, + {file = "coverage-5.0.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc"}, + {file = "coverage-5.0.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a"}, + {file = "coverage-5.0.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52"}, + {file = "coverage-5.0.3-cp27-cp27m-win32.whl", hash = "sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c"}, + {file = "coverage-5.0.3-cp27-cp27m-win_amd64.whl", hash = "sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73"}, + {file = "coverage-5.0.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68"}, + {file = "coverage-5.0.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691"}, + {file = "coverage-5.0.3-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301"}, + {file = "coverage-5.0.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf"}, + {file = "coverage-5.0.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3"}, + {file = "coverage-5.0.3-cp35-cp35m-win32.whl", hash = "sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0"}, + {file = "coverage-5.0.3-cp35-cp35m-win_amd64.whl", hash = "sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0"}, + {file = "coverage-5.0.3-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2"}, + {file = "coverage-5.0.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894"}, + {file = "coverage-5.0.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf"}, + {file = "coverage-5.0.3-cp36-cp36m-win32.whl", hash = "sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477"}, + {file = "coverage-5.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc"}, + {file = "coverage-5.0.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8"}, + {file = "coverage-5.0.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987"}, + {file = "coverage-5.0.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea"}, + {file = "coverage-5.0.3-cp37-cp37m-win32.whl", hash = "sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc"}, + {file = "coverage-5.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e"}, + {file = "coverage-5.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb"}, + {file = "coverage-5.0.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37"}, + {file = "coverage-5.0.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d"}, + {file = "coverage-5.0.3-cp38-cp38m-win32.whl", hash = "sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954"}, + {file = "coverage-5.0.3-cp38-cp38m-win_amd64.whl", hash = "sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e"}, + {file = "coverage-5.0.3-cp39-cp39m-win32.whl", hash = "sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40"}, + {file = "coverage-5.0.3-cp39-cp39m-win_amd64.whl", hash = "sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af"}, + {file = "coverage-5.0.3.tar.gz", hash = "sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef"}, ] dj-database-url = [ {file = "dj-database-url-0.5.0.tar.gz", hash = "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163"}, @@ -1863,6 +2149,22 @@ 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-celery-beat = [ + {file = "django-celery-beat-1.5.0.tar.gz", hash = "sha256:659b39232c454ac27022bf679939bce0471fd482f3ee9276f5199716cb4afad9"}, + {file = "django_celery_beat-1.5.0-py2.py3-none-any.whl", hash = "sha256:61c92d4b600a9f24406ee0b8d01a9b192253e15d047e3325e1d81e2cacf7aba6"}, +] +django-celery-email = [ + {file = "django-celery-email-3.0.0.tar.gz", hash = "sha256:5546cbba80952cc3b8a0ffa4206ce90a4a996a7ffd1c385a2bdb65903ca18ece"}, + {file = "django_celery_email-3.0.0-py2.py3-none-any.whl", hash = "sha256:0f72da39cb2ea83c69440566e87f27cd72f68f247f98ce99fb29889fcf329406"}, +] +django-celery-results = [ + {file = "django_celery_results-1.1.2-py2.py3-none-any.whl", hash = "sha256:932277e9382528f74778b30cf90e17941cba577b7d73cee09ed55e4972972c32"}, + {file = "django_celery_results-1.1.2.tar.gz", hash = "sha256:e735dc3e705a0e21afc3b6fa2918ec388258145fcbaad3727c493c5707d25034"}, +] +django-ckeditor = [ + {file = "django-ckeditor-5.8.0.tar.gz", hash = "sha256:46fc9c7346ea36183dc0cea350f98704f8b04c4722b7fe4fb18baf8ae20423fb"}, + {file = "django_ckeditor-5.8.0-py2.py3-none-any.whl", hash = "sha256:a59bab13f4481318f8a048b1b0aef5c7da768a6352dcfb9ba0e77d91fbb9462a"}, +] django-constance = [] django-debug-toolbar = [ {file = "django-debug-toolbar-2.1.tar.gz", hash = "sha256:24c157bc6c0e1648e0a6587511ecb1b007a00a354ce716950bff2de12693e7a8"}, @@ -1893,6 +2195,17 @@ django-impersonate = [ django-ipware = [ {file = "django-ipware-2.1.0.tar.gz", hash = "sha256:a7c7a8fd019dbdc9c357e6e582f65034e897572fc79a7e467674efa8aef9d00b"}, ] +django-js-asset = [ + {file = "django-js-asset-1.2.2.tar.gz", hash = "sha256:c163ae80d2e0b22d8fb598047cd0dcef31f81830e127cfecae278ad574167260"}, + {file = "django_js_asset-1.2.2-py2.py3-none-any.whl", hash = "sha256:8ec12017f26eec524cab436c64ae73033368a372970af4cf42d9354fcb166bdd"}, +] +django-js-reverse = [ + {file = "django-js-reverse-0.9.1.tar.gz", hash = "sha256:2a392d169f44e30b883c30dfcfd917a14167ce8fe196c99d2385b31c90d77aa0"}, + {file = "django_js_reverse-0.9.1-py2.py3-none-any.whl", hash = "sha256:8134c2ab6307c945edfa90671ca65e85d6c1754d48566bdd6464be259cc80c30"}, +] +django-jsonstore = [ + {file = "django-jsonstore-0.4.1.tar.gz", hash = "sha256:d6e42152af3f924e4657c99e80144ba9a6410799256f6134b5a4e9fa4282ec10"}, +] django-maintenance-mode = [ {file = "django-maintenance-mode-0.14.0.tar.gz", hash = "sha256:f3fef1760fdcda5e0bf6c2966aadc77eea6f328580a9c751920daba927281a68"}, {file = "django_maintenance_mode-0.14.0-py2-none-any.whl", hash = "sha256:b4cc24a469ed10897826a28f05d64e6166a58d130e4940ac124ce198cd4cc778"}, @@ -1927,6 +2240,9 @@ django-pwa = [ {file = "django-pwa-1.0.6.tar.gz", hash = "sha256:b3f1ad0c5241fae4c7505423540de4db93077d7c88416ff6d2af545ffe209f34"}, {file = "django_pwa-1.0.6-py3-none-any.whl", hash = "sha256:9306105fcb637ae16fea6527be4b147d45fd53db85efb1d4f61dfea6bf793e56"}, ] +django-render-block = [ + {file = "django_render_block-0.6-py2.py3-none-any.whl", hash = "sha256:95c7dc9610378a10e0c4a10d8364ec7307210889afccd6a67a6aaa0fd599bd4d"}, +] django-sass-processor = [ {file = "django-sass-processor-0.8.tar.gz", hash = "sha256:e039551994feaaba6fcf880412b25a772dd313162a34cbb4289814988cfae340"}, ] @@ -1945,6 +2261,13 @@ django-tables2 = [ {file = "django-tables2-2.2.1.tar.gz", hash = "sha256:0d9b17f5c030ba1b5fcaeb206d8397bf58f1fdfc6beaf56e7874841b8647aa94"}, {file = "django_tables2-2.2.1-py2.py3-none-any.whl", hash = "sha256:6afa0496695e15b332e98537265d09fe01a55b28c75a85323d8e6b0dc2350280"}, ] +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"}, +] django-two-factor-auth = [ {file = "django-two-factor-auth-1.10.0.tar.gz", hash = "sha256:3c3af3cd747462be18e7494c4068a2bdc606d7a2d2b2914f8d4590fc80995a71"}, {file = "django_two_factor_auth-1.10.0-py2.py3-none-any.whl", hash = "sha256:0945260fa84e4522d8fa951c35e401616579fd8564938441614399dc588a1c1f"}, @@ -1957,9 +2280,8 @@ django-yarnpkg = [ {file = "django-yarnpkg-6.0.1.tar.gz", hash = "sha256:aa059347b246c6f242401581d2c129bdcb45aa726be59fe2f288762a9843348a"}, ] docutils = [ - {file = "docutils-0.15.2-py2-none-any.whl", hash = "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827"}, - {file = "docutils-0.15.2-py3-none-any.whl", hash = "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0"}, - {file = "docutils-0.15.2.tar.gz", hash = "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"}, + {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, + {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, ] dparse = [ {file = "dparse-0.4.1-py2-none-any.whl", hash = "sha256:cef95156fa0adedaf042cd42f9990974bec76f25dfeca4dc01f381a243d5aa5b"}, @@ -1977,8 +2299,8 @@ entrypoints = [ {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, ] faker = [ - {file = "Faker-3.0.0-py2.py3-none-any.whl", hash = "sha256:202ad3b2ec16ae7c51c02904fb838831f8d2899e61bf18db1e91a5a582feab11"}, - {file = "Faker-3.0.0.tar.gz", hash = "sha256:92c84a10bec81217d9cb554ee12b3838c8986ce0b5d45f72f769da22e4bb5432"}, + {file = "Faker-4.0.0-py3-none-any.whl", hash = "sha256:047d4d1791bfb3756264da670d99df13d799bb36e7d88774b1585a82d05dbaec"}, + {file = "Faker-4.0.0.tar.gz", hash = "sha256:1b1a58961683b30c574520d0c739c4443e0ef6a185c04382e8cc888273dbebed"}, ] flake8 = [ {file = "flake8-3.7.9-py2.py3-none-any.whl", hash = "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"}, @@ -2031,6 +2353,9 @@ gitpython = [ ics = [ {file = "ics-0.6-py2.py3-none-any.whl", hash = "sha256:12cf34aed0dafa1bf99d79ca58e99949d6721511b856386e118015fe5f5d6e3a"}, {file = "ics-0.6-py3.7.egg", hash = "sha256:daa457478dbaba3ce7ab5f7b3a411e72d7a2771c0781c21013cc6c9f27b2a050"}, +html2text = [ + {file = "html2text-2020.1.16-py3-none-any.whl", hash = "sha256:c7c629882da0cf377d66f073329ccf34a12ed2adf0169b9285ae4e63ef54c82b"}, + {file = "html2text-2020.1.16.tar.gz", hash = "sha256:e296318e16b059ddb97f7a8a1d6a5c1d7af4544049a01e261731d2d5cc277bbb"}, ] idna = [ {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, @@ -2041,8 +2366,8 @@ imagesize = [ {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-1.3.0-py2.py3-none-any.whl", hash = "sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"}, - {file = "importlib_metadata-1.3.0.tar.gz", hash = "sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45"}, + {file = "importlib_metadata-1.4.0-py2.py3-none-any.whl", hash = "sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359"}, + {file = "importlib_metadata-1.4.0.tar.gz", hash = "sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"}, ] isort = [ {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, @@ -2052,6 +2377,10 @@ jinja2 = [ {file = "Jinja2-2.10.3-py2.py3-none-any.whl", hash = "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f"}, {file = "Jinja2-2.10.3.tar.gz", hash = "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"}, ] +kombu = [ + {file = "kombu-4.6.7-py2.py3-none-any.whl", hash = "sha256:2a9e7adff14d046c9996752b2c48b6d9185d0b992106d5160e1a179907a5d4ac"}, + {file = "kombu-4.6.7.tar.gz", hash = "sha256:67b32ccb6fea030f8799f8fd50dd08e03a4b99464ebc4952d71d8747b1a52ad1"}, +] libsass = [ {file = "libsass-0.19.4-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:74acd9adf506142699dfa292f0e569fdccbd9e7cf619e8226f7117de73566e32"}, {file = "libsass-0.19.4-cp27-cp27m-win32.whl", hash = "sha256:50778d4be269a021ba2bf42b5b8f6ff3704ab96a82175a052680bddf3ba7cc9f"}, @@ -2105,8 +2434,8 @@ mccabe = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] more-itertools = [ - {file = "more-itertools-8.0.2.tar.gz", hash = "sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d"}, - {file = "more_itertools-8.0.2-py3-none-any.whl", hash = "sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"}, + {file = "more-itertools-8.1.0.tar.gz", hash = "sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288"}, + {file = "more_itertools-8.1.0-py3-none-any.whl", hash = "sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39"}, ] mypy = [ {file = "mypy-0.761-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6"}, @@ -2145,8 +2474,8 @@ pg8000 = [ {file = "pg8000-1.13.2.tar.gz", hash = "sha256:eebcb4176a7e407987e525a07454882f611985e0becb2b73f76efb93bbdc0aab"}, ] phonenumbers = [ - {file = "phonenumbers-8.11.1-py2.py3-none-any.whl", hash = "sha256:aaa19bc1f2c7efbf7a94be33558e0c5b71620377c9271692d3e314c558962460"}, - {file = "phonenumbers-8.11.1.tar.gz", hash = "sha256:239507184ee5b1b83557005af1d5fcce70f83ae18f5dff45b94a67226db10d63"}, + {file = "phonenumbers-8.11.2-py2.py3-none-any.whl", hash = "sha256:796ba25c3064727ca0b8edf7a8ef5ef247c6da37aee498562e6e0ed46970a57f"}, + {file = "phonenumbers-8.11.2.tar.gz", hash = "sha256:a22d3b14c7f18af9be7c4ade92285035f621c6a17b75352dc9c2e5d146aee348"}, ] pillow = [ {file = "Pillow-7.0.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:5f3546ceb08089cedb9e8ff7e3f6a7042bb5b37c2a95d392fb027c3e53a2da00"}, @@ -2284,16 +2613,16 @@ pyparsing = [ {file = "pyparsing-2.4.6.tar.gz", hash = "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f"}, ] pytest = [ - {file = "pytest-5.3.2-py3-none-any.whl", hash = "sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4"}, - {file = "pytest-5.3.2.tar.gz", hash = "sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa"}, + {file = "pytest-5.3.3-py3-none-any.whl", hash = "sha256:9f8d44f4722b3d06b41afaeb8d177cfbe0700f8351b1fc755dd27eedaa3eb9e0"}, + {file = "pytest-5.3.3.tar.gz", hash = "sha256:f5d3d0e07333119fe7d4af4ce122362dc4053cdd34a71d2766290cf5369c64ad"}, ] pytest-cov = [ {file = "pytest-cov-2.8.1.tar.gz", hash = "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b"}, {file = "pytest_cov-2.8.1-py2.py3-none-any.whl", hash = "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"}, ] pytest-django = [ - {file = "pytest-django-3.7.0.tar.gz", hash = "sha256:17592f06d51c2ef4b7a0fb24aa32c8b6998506a03c8439606cb96db160106659"}, - {file = "pytest_django-3.7.0-py2.py3-none-any.whl", hash = "sha256:ef3d15b35ed7e46293475e6f282e71a53bcd8c6f87bdc5d5e7ad0f4d49352127"}, + {file = "pytest-django-3.8.0.tar.gz", hash = "sha256:489b904f695f9fb880ce591cf5a4979880afb467763b1f180c07574554bdfd26"}, + {file = "pytest_django-3.8.0-py2.py3-none-any.whl", hash = "sha256:456fa6854d04ee625d6bbb8b38ca2259e7040a6f93333bfe8bc8159b7e987203"}, ] pytest-django-testing-postgresql = [ {file = "pytest-django-testing-postgresql-0.1.post0.tar.gz", hash = "sha256:78b0c58930084cb4393407b2e5a2a3b8734c627b841ecef7d62d39bbfb8e8a45"}, @@ -2307,6 +2636,9 @@ python-box = [ {file = "python-box-3.4.6.tar.gz", hash = "sha256:694a7555e3ff9fbbce734bbaef3aad92b8e4ed0659d3ed04d56b6a0a0eff26a9"}, {file = "python_box-3.4.6-py2.py3-none-any.whl", hash = "sha256:a71d3dc9dbaa34c8597d3517c89a8041bd62fa875f23c0f3dad55e1958e3ce10"}, ] +python-crontab = [ + {file = "python-crontab-2.4.0.tar.gz", hash = "sha256:3ac1608ff76032e6fc6e16b5fbf83b51557e0e066bf78e9f88571571e7bd7ae6"}, +] python-dateutil = [ {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, @@ -2343,6 +2675,10 @@ qrcode = [ {file = "qrcode-6.1-py2.py3-none-any.whl", hash = "sha256:3996ee560fc39532910603704c82980ff6d4d5d629f9c3f25f34174ce8606cf5"}, {file = "qrcode-6.1.tar.gz", hash = "sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369"}, ] +redis = [ + {file = "redis-3.3.11-py2.py3-none-any.whl", hash = "sha256:3613daad9ce5951e426f460deddd5caf469e08a3af633e9578fc77d362becf62"}, + {file = "redis-3.3.11.tar.gz", hash = "sha256:8d0fc278d3f5e1249967cba2eb4a5632d19e45ce5c09442b8422d15ee2c22cc2"}, +] regex = [ {file = "regex-2020.1.8-cp27-cp27m-win32.whl", hash = "sha256:4e8f02d3d72ca94efc8396f8036c0d3bcc812aefc28ec70f35bb888c74a25161"}, {file = "regex-2020.1.8-cp27-cp27m-win_amd64.whl", hash = "sha256:e6c02171d62ed6972ca8631f6f34fa3281d51db8b326ee397b9c83093a6b7242"}, @@ -2386,8 +2722,8 @@ selenium = [ {file = "selenium-3.141.0.tar.gz", hash = "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d"}, ] six = [ - {file = "six-1.13.0-py2.py3-none-any.whl", hash = "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd"}, - {file = "six-1.13.0.tar.gz", hash = "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"}, + {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, + {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, ] smmap2 = [ {file = "smmap2-2.0.5-py2.py3-none-any.whl", hash = "sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde"}, @@ -2481,26 +2817,27 @@ twilio = [ {file = "twilio-6.35.2.tar.gz", hash = "sha256:a086443642c0e1f13c8f8f087b426ca81ec883efbe496d8279180a49bb9287bc"}, ] typed-ast = [ - {file = "typed_ast-1.4.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e"}, - {file = "typed_ast-1.4.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b"}, - {file = "typed_ast-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4"}, - {file = "typed_ast-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"}, - {file = "typed_ast-1.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631"}, - {file = "typed_ast-1.4.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233"}, - {file = "typed_ast-1.4.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1"}, - {file = "typed_ast-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a"}, - {file = "typed_ast-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c"}, - {file = "typed_ast-1.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a"}, - {file = "typed_ast-1.4.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e"}, - {file = "typed_ast-1.4.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d"}, - {file = "typed_ast-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36"}, - {file = "typed_ast-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0"}, - {file = "typed_ast-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66"}, - {file = "typed_ast-1.4.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2"}, - {file = "typed_ast-1.4.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47"}, - {file = "typed_ast-1.4.0-cp38-cp38-win32.whl", hash = "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161"}, - {file = "typed_ast-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e"}, - {file = "typed_ast-1.4.0.tar.gz", hash = "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, + {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, + {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, + {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, + {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, ] typing-extensions = [ {file = "typing_extensions-3.7.4.1-py2-none-any.whl", hash = "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d"}, @@ -2511,6 +2848,10 @@ urllib3 = [ {file = "urllib3-1.25.7-py2.py3-none-any.whl", hash = "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293"}, {file = "urllib3-1.25.7.tar.gz", hash = "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"}, ] +vine = [ + {file = "vine-1.3.0-py2.py3-none-any.whl", hash = "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"}, + {file = "vine-1.3.0.tar.gz", hash = "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87"}, +] wcwidth = [ {file = "wcwidth-0.1.8-py2.py3-none-any.whl", hash = "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603"}, ] @@ -2519,6 +2860,6 @@ yubiotp = [ {file = "YubiOTP-0.2.2.post1.tar.gz", hash = "sha256:de83b1560226e38b5923f6ab919f962c8c2abb7c722104cb45b2b6db2ac86e40"}, ] zipp = [ - {file = "zipp-0.6.0-py2.py3-none-any.whl", hash = "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"}, - {file = "zipp-0.6.0.tar.gz", hash = "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e"}, + {file = "zipp-1.0.0-py2.py3-none-any.whl", hash = "sha256:8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656"}, + {file = "zipp-1.0.0.tar.gz", hash = "sha256:d38fbe01bbf7a3593a32bc35a9c4453c32bc42b98c377f9bff7e9f8da157786c"}, ] diff --git a/pyproject.toml b/pyproject.toml index 9fdcf0e24af955adcb2bac8f7321e086c1daaca9..1c3bdb77b59e270f075470b6b0dd82525d126073 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,10 +53,20 @@ django-pwa = "^1.0.6" django-constance = {git = "https://github.com/jazzband/django-constance", rev = "590fa02eb30e377da0eda5cc3a84254b839176a7", extras = ["database"]} django_widget_tweaks = "^1.4.5" django-filter = "^2.2.0" -ics = "^0.6" +django-templated-email = "^2.3.0" +html2text = "^2020.0.0" +django-ckeditor = "^5.8.0" +django-js-reverse = "^0.9.1" +calendarweek = "^0.4.3" +Celery = {version="^4.4.0", optional=true, extras=["django", "redis"]} +django-celery-results = {version="^1.1.2", optional=true} +django-celery-beat = {version="^1.5.0", optional=true} +django-celery-email = {version="^3.0.0", optional=true} +django-jsonstore = "^0.4.1" [tool.poetry.extras] ldap = ["django-auth-ldap"] +celery = ["Celery", "django-celery-results", "django-celery-beat", "django-celery-email"] [tool.poetry.dev-dependencies] sphinx = "^2.1"