diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py index 0037842bc71590eb521aa67f25b5f4cc6b50fb92..aeda609326941428e483e05cabd6e5412b2832f7 100644 --- a/aleksis/core/apps.py +++ b/aleksis/core/apps.py @@ -2,6 +2,7 @@ from typing import Any, List, Optional, Tuple import django.apps from django.http import HttpRequest +from django.utils.module_loading import autodiscover_modules from dynamic_preferences.registries import preference_models @@ -36,6 +37,9 @@ class CoreConfig(AppConfig): def ready(self): super().ready() + # Autodiscover various modules defined by AlekSIS + autodiscover_modules("form_extensions", "model_extensions", "checks") + sitepreferencemodel = self.get_model("SitePreferenceModel") personpreferencemodel = self.get_model("PersonPreferenceModel") grouppreferencemodel = self.get_model("GroupPreferenceModel") diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py index abd45aabdee3f9d933376a0fc034f59218a8523f..208cbff93f76b7d44451550b73d3b5659ba4aa59 100644 --- a/aleksis/core/forms.py +++ b/aleksis/core/forms.py @@ -9,8 +9,8 @@ from django_select2.forms import ModelSelect2MultipleWidget, Select2Widget from dynamic_preferences.forms import PreferenceForm from material import Fieldset, Layout, Row -from .mixins import ExtensibleForm -from .models import AdditionalField, Announcement, Group, GroupType, Person +from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm +from .models import AdditionalField, Announcement, Group, GroupType, Person, SchoolTerm from .registries import ( group_preferences_registry, person_preferences_registry, @@ -121,10 +121,11 @@ class EditPersonForm(ExtensibleForm): return PersonAccountForm.clean(self) -class EditGroupForm(ExtensibleForm): +class EditGroupForm(SchoolTermRelatedExtensibleForm): """Form to edit an existing group in the frontend.""" layout = Layout( + Fieldset(_("School term"), "school_term"), Fieldset(_("Common data"), "name", "short_name", "group_type"), Fieldset(_("Persons"), "members", "owners", "parent_groups"), Fieldset(_("Additional data"), "additional_fields"), @@ -170,7 +171,7 @@ class AnnouncementForm(ExtensibleForm): persons = forms.ModelMultipleChoiceField( Person.objects.all(), label=_("Persons"), required=False ) - groups = forms.ModelMultipleChoiceField(Group.objects.all(), label=_("Groups"), required=False) + groups = forms.ModelMultipleChoiceField(queryset=None, label=_("Groups"), required=False) layout = Layout( Fieldset( @@ -205,6 +206,8 @@ class AnnouncementForm(ExtensibleForm): super().__init__(*args, **kwargs) + self.fields["groups"].queryset = Group.objects.for_current_school_term_or_all() + def clean(self): data = super().clean() @@ -298,3 +301,13 @@ class EditGroupTypeForm(forms.ModelForm): class Meta: model = GroupType exclude = [] + + +class SchoolTermForm(ExtensibleForm): + """Form for managing school years.""" + + layout = Layout("name", Row("date_start", "date_end")) + + class Meta: + model = SchoolTerm + exclude = [] diff --git a/aleksis/core/managers.py b/aleksis/core/managers.py new file mode 100644 index 0000000000000000000000000000000000000000..a0871078015df8e117a09e23a593b384f7b25dc2 --- /dev/null +++ b/aleksis/core/managers.py @@ -0,0 +1,81 @@ +from datetime import date +from typing import Union + +from django.contrib.sites.managers import CurrentSiteManager as _CurrentSiteManager +from django.db.models import QuerySet + +from calendarweek import CalendarWeek + + +class CurrentSiteManagerWithoutMigrations(_CurrentSiteManager): + """CurrentSiteManager for auto-generating managers just by query sets.""" + + use_in_migrations = False + + +class DateRangeQuerySetMixin: + """QuerySet with custom query methods for models with date ranges. + + Filterable fields: date_start, date_end + """ + + def within_dates(self, start: date, end: date): + """Filter for all objects within a date range.""" + return self.filter(date_start__lte=end, date_end__gte=start) + + def in_week(self, wanted_week: CalendarWeek): + """Filter for all objects within a calendar week.""" + return self.within_dates(wanted_week[0], wanted_week[6]) + + def on_day(self, day: date): + """Filter for all objects on a certain day.""" + return self.within_dates(day, day) + + +class SchoolTermQuerySet(QuerySet, DateRangeQuerySetMixin): + """Custom query set for school terms.""" + + +class SchoolTermRelatedQuerySet(QuerySet): + """Custom query set for all models related to school terms.""" + + def within_dates(self, start: date, end: date) -> "SchoolTermRelatedQuerySet": + """Filter for all objects within a date range.""" + return self.filter(school_term__date_start__lte=end, school_term__date_end__gte=start) + + def in_week(self, wanted_week: CalendarWeek) -> "SchoolTermRelatedQuerySet": + """Filter for all objects within a calendar week.""" + return self.within_dates(wanted_week[0], wanted_week[6]) + + def on_day(self, day: date) -> "SchoolTermRelatedQuerySet": + """Filter for all objects on a certain day.""" + return self.within_dates(day, day) + + def for_school_term(self, school_term: "SchoolTerm") -> "SchoolTermRelatedQuerySet": + return self.filter(school_term=school_term) + + def for_current_school_term_or_all(self) -> "SchoolTermRelatedQuerySet": + """Get all objects related to current school term. + + If there is no current school term, it will return all objects. + """ + from aleksis.core.models import SchoolTerm + + current_school_term = SchoolTerm.current + if current_school_term: + return self.for_school_term(current_school_term) + else: + return self + + def for_current_school_term_or_none(self) -> Union["SchoolTermRelatedQuerySet", None]: + """Get all objects related to current school term. + + If there is no current school term, it will return `None`. + """ + from aleksis.core.models import SchoolTerm + + current_school_term = SchoolTerm.current + if current_school_term: + return self.for_school_term(current_school_term) + else: + return None diff --git a/aleksis/core/menus.py b/aleksis/core/menus.py index c1b19bb3970eac18d756ec0eae6bdaf8ae285d07..2e71c357384c1bc7e4ffa4ff1485f4edc4ffc7de 100644 --- a/aleksis/core/menus.py +++ b/aleksis/core/menus.py @@ -82,6 +82,17 @@ MENUS = { ), ], }, + { + "name": _("School terms"), + "url": "school_terms", + "icon": "date_range", + "validators": [ + ( + "aleksis.core.util.predicates.permission_validator", + "core.view_schoolterm", + ), + ], + }, { "name": _("Data management"), "url": "data_management", diff --git a/aleksis/core/migrations/0002_school_term.py b/aleksis/core/migrations/0002_school_term.py new file mode 100644 index 0000000000000000000000000000000000000000..456f78ebf7e16f32b92b954d36ad39f5d323e866 --- /dev/null +++ b/aleksis/core/migrations/0002_school_term.py @@ -0,0 +1,64 @@ +# Generated by Django 3.0.7 on 2020-06-13 14:34 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('sites', '0002_alter_domain_unique'), + ('core', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='SchoolTerm', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)), + ('name', models.CharField(max_length=255, unique=True, verbose_name='Name')), + ('date_start', models.DateField(verbose_name='Start date')), + ('date_end', models.DateField(verbose_name='End date')), + ], + options={ + 'verbose_name': 'School term', + 'verbose_name_plural': 'School terms', + }, + ), + migrations.AlterModelManagers( + name='group', + managers=[ + ], + ), + migrations.AlterField( + model_name='group', + name='name', + field=models.CharField(max_length=255, verbose_name='Long name'), + ), + migrations.AlterField( + model_name='group', + name='short_name', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Short name'), + ), + migrations.AddField( + model_name='group', + name='school_term', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, + related_name='+', to='core.SchoolTerm', verbose_name='Linked school term'), + ), + migrations.AddConstraint( + model_name='group', + constraint=models.UniqueConstraint(fields=('school_term', 'name'), name='unique_school_term_name'), + ), + migrations.AddConstraint( + model_name='group', + constraint=models.UniqueConstraint(fields=('school_term', 'short_name'), name='unique_school_term_short_name'), + ), + migrations.AddField( + model_name='schoolterm', + name='site', + field=models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site'), + ), + ] diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py index 9b15f27f15f28d3509ffacfc1e7458a8ced08b04..531181df5aabfe919cbc7ea8007ce92105a27984 100644 --- a/aleksis/core/mixins.py +++ b/aleksis/core/mixins.py @@ -4,13 +4,19 @@ from datetime import datetime from typing import Any, Callable, List, Optional, Tuple, Union from django.conf import settings +from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.contrib.sites.managers import CurrentSiteManager from django.contrib.sites.models import Site from django.db import models from django.db.models import QuerySet +from django.forms.forms import BaseForm from django.forms.models import ModelForm, ModelFormMetaclass +from django.http import HttpResponse from django.utils.functional import lazy +from django.utils.translation import gettext as _ +from django.views.generic import CreateView, UpdateView +from django.views.generic.edit import ModelFormMixin import reversion from easyaudit.models import CRUDEvent @@ -19,6 +25,8 @@ from jsonstore.fields import IntegerField, JSONField, JSONFieldMixin from material.base import Layout, LayoutNode from rules.contrib.admin import ObjectPermissionsModelAdmin +from aleksis.core.managers import CurrentSiteManagerWithoutMigrations, SchoolTermRelatedQuerySet + class _ExtensibleModelBase(models.base.ModelBase): """Ensure predefined behaviour on model creation. @@ -35,6 +43,8 @@ class _ExtensibleModelBase(models.base.ModelBase): # Register all non-abstract models with django-reversion mcls = reversion.register(mcls) + mcls.extra_permissions = [] + return mcls @@ -92,6 +102,8 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase): objects = CurrentSiteManager() objects_all_sites = models.Manager() + extra_permissions = [] + def get_absolute_url(self) -> str: """Get the URL o a view representing this model instance.""" pass @@ -164,17 +176,17 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase): @classmethod def property_(cls, func: Callable[[], Any], name: Optional[str] = None) -> None: """Add the passed callable as a property.""" - cls._safe_add(property(func), func.__name__) + cls._safe_add(property(func), name or func.__name__) @classmethod def method(cls, func: Callable[[], Any], name: Optional[str] = None) -> None: """Add the passed callable as a method.""" - cls._safe_add(func, func.__name__) + cls._safe_add(func, name or func.__name__) @classmethod def class_method(cls, func: Callable[[], Any], name: Optional[str] = None) -> None: """Add the passed callable as a classmethod.""" - cls._safe_add(classmethod(func), func.__name__) + cls._safe_add(classmethod(func), name or func.__name__) @classmethod def field(cls, **kwargs) -> None: @@ -282,6 +294,11 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase): """Collect all fields that can be synced on a model.""" return lazy(cls.syncable_fields_choices, tuple) + @classmethod + def add_permission(cls, name: str, verbose_name: str): + """Dynamically add a new permission to a model.""" + cls.extra_permissions.append((name, verbose_name)) + class Meta: abstract = True @@ -343,3 +360,55 @@ class BaseModelAdmin(GuardedModelAdmin, ObjectPermissionsModelAdmin): """A base class for ModelAdmin combining django-guardian and rules.""" pass + + +class SuccessMessageMixin(ModelFormMixin): + success_message: Optional[str] = None + + def form_valid(self, form: BaseForm) -> HttpResponse: + if self.success_message: + messages.success(self.request, self.success_message) + return super().form_valid(form) + + +class AdvancedCreateView(CreateView, SuccessMessageMixin): + pass + + +class AdvancedEditView(UpdateView, SuccessMessageMixin): + pass + + +class SchoolTermRelatedExtensibleModel(ExtensibleModel): + """Add relation to school term.""" + + objects = CurrentSiteManagerWithoutMigrations.from_queryset(SchoolTermRelatedQuerySet)() + + school_term = models.ForeignKey( + "core.SchoolTerm", + on_delete=models.CASCADE, + related_name="+", + verbose_name=_("Linked school term"), + blank=True, + null=True, + ) + + class Meta: + abstract = True + + +class SchoolTermRelatedExtensibleForm(ExtensibleForm): + """Extensible form for school term related data. + + .. warning:: + This doesn't automatically include the field `school_term` in `fields` or `layout`, + it just sets an initial value. + """ + + def __init__(self, *args, **kwargs): + from aleksis.core.models import SchoolTerm # noqa + + if "instance" not in kwargs: + kwargs["initial"] = {"school_term": SchoolTerm.current} + + super().__init__(*args, **kwargs) diff --git a/aleksis/core/models.py b/aleksis/core/models.py index b5bb204c25139c463e6d53c27b7c1ddf51b75576..fa24519076a2cb1951aabe01e973d9f1380c1588 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -8,11 +8,13 @@ from django.contrib.auth.models import Group as DjangoGroup from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site +from django.core.exceptions import ValidationError from django.db import models from django.db.models import QuerySet from django.forms.widgets import Media from django.urls import reverse from django.utils import timezone +from django.utils.decorators import classproperty from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ @@ -22,7 +24,8 @@ from image_cropping import ImageCropField, ImageRatioField from phonenumber_field.modelfields import PhoneNumberField from polymorphic.models import PolymorphicModel -from .mixins import ExtensibleModel, PureDjangoModel +from .managers import CurrentSiteManagerWithoutMigrations, SchoolTermQuerySet +from .mixins import ExtensibleModel, PureDjangoModel, SchoolTermRelatedExtensibleModel from .tasks import send_notification from .util.core_helpers import get_site_preferences, now_tomorrow from .util.model_helpers import ICONS @@ -43,6 +46,53 @@ FIELD_CHOICES = ( ) +class SchoolTerm(ExtensibleModel): + """School term model. + + This is used to manage start and end times of a school term and link data to it. + """ + + objects = CurrentSiteManagerWithoutMigrations.from_queryset(SchoolTermQuerySet)() + + name = models.CharField(verbose_name=_("Name"), max_length=255, unique=True) + + date_start = models.DateField(verbose_name=_("Start date")) + date_end = models.DateField(verbose_name=_("End date")) + + @classmethod + def get_current(cls, day: Optional[date] = None): + if not day: + day = timezone.now().date() + try: + return cls.objects.on_day(day).first() + except SchoolTerm.DoesNotExist: + return None + + @classproperty + def current(cls): + return cls.get_current() + + def clean(self): + """Ensure there is only one school term at each point of time.""" + if self.date_end < self.date_start: + raise ValidationError(_("The start date must be earlier than the end date.")) + + qs = SchoolTerm.objects.within_dates(self.date_start, self.date_end) + if self.pk: + qs.exclude(pk=self.pk) + if qs.exists(): + raise ValidationError( + _("There is already a school term for this time or a part of this time.") + ) + + def __str__(self): + return self.name + + class Meta: + verbose_name = _("School term") + verbose_name_plural = _("School terms") + + class Person(ExtensibleModel): """Person model. @@ -250,12 +300,15 @@ class AdditionalField(ExtensibleModel): verbose_name=_("Type of field"), choices=FIELD_CHOICES, max_length=50 ) + def __str__(self) -> str: + return self.title + class Meta: verbose_name = _("Addtitional field for groups") verbose_name_plural = _("Addtitional fields for groups") -class Group(ExtensibleModel): +class Group(SchoolTermRelatedExtensibleModel): """Group model. Any kind of group of persons in a school, including, but not limited @@ -267,12 +320,18 @@ class Group(ExtensibleModel): verbose_name = _("Group") verbose_name_plural = _("Groups") permissions = (("assign_child_groups_to_groups", _("Can assign child groups to groups")),) + constraints = [ + models.UniqueConstraint(fields=["school_term", "name"], name="unique_school_term_name"), + models.UniqueConstraint( + fields=["school_term", "short_name"], name="unique_school_term_short_name" + ), + ] icon_ = "group" - name = models.CharField(verbose_name=_("Long name"), max_length=255, unique=True) + name = models.CharField(verbose_name=_("Long name"), max_length=255) short_name = models.CharField( - verbose_name=_("Short name"), max_length=255, unique=True, blank=True, null=True # noqa + verbose_name=_("Short name"), max_length=255, blank=True, null=True # noqa ) members = models.ManyToManyField( @@ -302,7 +361,9 @@ class Group(ExtensibleModel): null=True, blank=True, ) - additional_fields = models.ManyToManyField(AdditionalField, verbose_name=_("Additional fields")) + additional_fields = models.ManyToManyField( + AdditionalField, verbose_name=_("Additional fields"), blank=True + ) def get_absolute_url(self) -> str: return reverse("group_by_id", args=[self.id]) @@ -313,7 +374,10 @@ class Group(ExtensibleModel): return list(self.members.all()) + list(self.owners.all()) def __str__(self) -> str: - return f"{self.name} ({self.short_name})" + if self.school_term: + return f"{self.name} ({self.short_name}) ({self.school_term})" + else: + return f"{self.name} ({self.short_name})" def save(self, *args, **kwargs): super().save(*args, **kwargs) @@ -657,6 +721,9 @@ class GroupType(ExtensibleModel): name = models.CharField(verbose_name=_("Title of type"), max_length=50) description = models.CharField(verbose_name=_("Description"), max_length=500) + def __str__(self) -> str: + return self.name + class Meta: verbose_name = _("Group type") verbose_name_plural = _("Group types") diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py index 4f97eafddf3cfdb9725cf0e9e0cde05ef83d1ad0..35b00f23770f80e3a5ca821393af3bf0956833f8 100644 --- a/aleksis/core/rules.py +++ b/aleksis/core/rules.py @@ -66,6 +66,12 @@ edit_person_predicate = has_person & ( ) add_perm("core.edit_person", edit_person_predicate) +# Delete person +delete_person_predicate = has_person & ( + has_global_perm("core.delete_person") | has_object_perm("core.delete_person") +) +add_perm("core.delete_person", delete_person_predicate) + # Link persons with accounts link_persons_accounts_predicate = has_person & has_global_perm("core.link_persons_accounts") add_perm("core.link_persons_accounts", link_persons_accounts_predicate) @@ -88,6 +94,12 @@ edit_group_predicate = has_person & ( ) add_perm("core.edit_group", edit_group_predicate) +# Delete group +delete_group_predicate = has_person & ( + has_global_perm("core.delete_group") | has_object_perm("core.delete_group") +) +add_perm("core.delete_group", delete_group_predicate) + # Assign child groups to groups assign_child_groups_to_groups_predicate = has_person & has_global_perm( "core.assign_child_groups_to_groups" @@ -98,14 +110,6 @@ add_perm("core.assign_child_groups_to_groups", assign_child_groups_to_groups_pre edit_school_information_predicate = has_person & has_global_perm("core.change_school") add_perm("core.edit_school_information", edit_school_information_predicate) -# Edit school term -edit_schoolterm_predicate = has_person & has_global_perm("core.change_schoolterm") -add_perm("core.edit_schoolterm", edit_schoolterm_predicate) - -# Manage school -manage_school_predicate = edit_school_information_predicate | edit_schoolterm_predicate -add_perm("core.manage_school", manage_school_predicate) - # Manage data manage_data_predicate = has_person & has_global_perm("core.manage_data") add_perm("core.manage_data", manage_data_predicate) @@ -154,16 +158,6 @@ add_perm( ), ) -# View admin menu -view_admin_menu_predicate = has_person & ( - manage_data_predicate - | manage_school_predicate - | impersonate_predicate - | view_system_status_predicate - | view_announcements_predicate -) -add_perm("core.view_admin_menu", view_admin_menu_predicate) - # View person personal details view_personal_details_predicate = has_person & ( has_global_perm("core.view_personal_details") @@ -246,3 +240,35 @@ view_group_type_predicate = has_person & ( has_global_perm("core.view_grouptype") | has_any_object("core.view_grouptype", GroupType) ) add_perm("core.view_grouptype", view_group_type_predicate) + +# Create person +create_person_predicate = has_person & ( + has_global_perm("core.create_person") | has_object_perm("core.create_person") +) +add_perm("core.create_person", create_person_predicate) + +# Create group +create_group_predicate = has_person & ( + has_global_perm("core.create_group") | has_object_perm("core.create_group") +) +add_perm("core.create_group", create_group_predicate) + +# School years +view_school_term_predicate = has_person & has_global_perm("core.view_schoolterm") +add_perm("core.view_schoolterm", view_school_term_predicate) + +create_school_term_predicate = has_person & has_global_perm("core.add_schoolterm") +add_perm("core.create_schoolterm", create_school_term_predicate) + +edit_school_term_predicate = has_person & has_global_perm("core.change_schoolterm") +add_perm("core.edit_schoolterm", edit_school_term_predicate) + +# View admin menu +view_admin_menu_predicate = has_person & ( + manage_data_predicate + | view_school_term_predicate + | impersonate_predicate + | view_system_status_predicate + | view_announcements_predicate +) +add_perm("core.view_admin_menu", view_admin_menu_predicate) diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index 756b14f7bc39ad021f207c00d47dd791de8be551..33d146e3f30f2d7fe0843c23f6f0cbc3a2d2aca4 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -85,6 +85,11 @@ INSTALLED_APPS = [ "django_otp", "otp_yubikey", "aleksis.core", + "health_check", + "health_check.db", + "health_check.cache", + "health_check.storage", + "health_check.contrib.psutil", "dynamic_preferences", "dynamic_preferences.users.apps.UserPreferencesConfig", "impersonate", @@ -419,7 +424,12 @@ if _settings.get("twilio.sid", None): TWILIO_CALLER_ID = _settings.get("twilio.callerid") if _settings.get("celery.enabled", False): - INSTALLED_APPS += ("django_celery_beat", "django_celery_results") + INSTALLED_APPS += ( + "django_celery_beat", + "django_celery_results", + "celery_progress", + "health_check.contrib.celery", + ) CELERY_BROKER_URL = _settings.get("celery.broker", "redis://localhost") CELERY_RESULT_BACKEND = "django-db" CELERY_CACHE_BACKEND = "django-cache" @@ -656,3 +666,8 @@ else: HAYSTACK_SEARCH_RESULTS_PER_PAGE = 10 DJANGO_EASY_AUDIT_WATCH_REQUEST_EVENTS = False + +HEALTH_CHECK = { + "DISK_USAGE_MAX": _settings.get("health.disk_usage_max_percent", 90), + "MEMORY_MIN": _settings.get("health.memory_min_mb", 500), +} diff --git a/aleksis/core/static/js/main.js b/aleksis/core/static/js/main.js index dd57cbe262eb60e24646b680fa435c940008c4aa..ba4e56cd54d8909d1f74cb54bbdb3c669b4cfeab 100644 --- a/aleksis/core/static/js/main.js +++ b/aleksis/core/static/js/main.js @@ -45,6 +45,19 @@ $(document).ready( function () { // Initialize select [MAT] $('select').formSelect(); + // If JS is activated, the language form will be auto-submitted + $('.language-field select').change(function () { + + // Ugly bug fix to ensure correct value + const selectEl = $("select[name=language]"); + selectEl.val(selectEl.val()); + + $(".language-form").submit(); + }); + + // If auto-submit is activated (see above), the language submit must not be visible + $(".language-submit-p").hide(); + // Initalize print button $("#print").click(function () { window.print(); diff --git a/aleksis/core/static/js/progress.js b/aleksis/core/static/js/progress.js new file mode 100644 index 0000000000000000000000000000000000000000..4d4c0692f39e78af60bdc621544ba7a2c2b720c4 --- /dev/null +++ b/aleksis/core/static/js/progress.js @@ -0,0 +1,69 @@ +const OPTIONS = getJSONScript("progress_options"); + +const STYLE_CLASSES = { + 10: 'info', + 20: 'info', + 25: 'success', + 30: 'warning', + 40: 'error', +}; + +const ICONS = { + 10: 'info', + 20: 'info', + 25: 'check_circle', + 30: 'warning', + 40: 'error', +}; + +function setProgress(progress) { + $("#progress-bar").css("width", progress + "%"); +} + +function renderMessageBox(level, text) { + return '<div class="alert ' + STYLE_CLASSES[level] + '"><p><i class="material-icons left">' + ICONS[level] + '</i>' + text + '</p></div>'; +} + +function customProgress(progressBarElement, progressBarMessageElement, progress) { + setProgress(progress.percent); + + if (progress.hasOwnProperty("messages")) { + const messagesBox = $("#messages"); + + // Clear container + messagesBox.html("") + + // Render message boxes + $.each(progress.messages, function (i, message) { + messagesBox.append(renderMessageBox(message[0], message[1])); + }) + } +} + + +function customSuccess(progressBarElement, progressBarMessageElement) { + setProgress(100); + $("#result-alert").addClass("success"); + $("#result-icon").text("check_circle"); + $("#result-text").text(OPTIONS.success); + $("#result-box").show(); +} + +function customError(progressBarElement, progressBarMessageElement) { + setProgress(100); + $("#result-alert").addClass("error"); + $("#result-icon").text("error"); + $("#result-text").text(OPTIONS.error); + $("#result-box").show(); +} + +$(document).ready(function () { + $("#progress-bar").removeClass("indeterminate").addClass("determinate"); + + var progressUrl = Urls["celeryProgress:taskStatus"](OPTIONS.task_id); + CeleryProgressBar.initProgressBar(progressUrl, { + onProgress: customProgress, + onSuccess: customSuccess, + onError: customError, + }); +}); diff --git a/aleksis/core/static/print.css b/aleksis/core/static/print.css index 4c511a1aec0e871778890bbee305e48130d65d36..0e2389e1dfb308f2b763fae85887f8a682fd1ad7 100644 --- a/aleksis/core/static/print.css +++ b/aleksis/core/static/print.css @@ -38,7 +38,13 @@ header, main, footer { height: 0; } -.print-layout-table td { +.print-layout-table, .print-layout-td { + width: 190mm; + max-width: 190mm; + min-width: 190mm; +} + +.print-layout-td { padding: 0; } @@ -65,6 +71,18 @@ header .row, header .col { width: auto; } +.page-break { + display: block; + text-align: center; + margin: auto; + margin-top: 20px; + margin-bottom: 20px; + width: 200px; + border-top: 1px dashed; + color: darkgrey; + page-break-after: always; +} + @media print { .header-space { height: 35mm; @@ -87,4 +105,8 @@ header .row, header .col { position: fixed; bottom: 0; } + + .page-break { + border: white; + } } diff --git a/aleksis/core/static/style.scss b/aleksis/core/static/style.scss index 10a00caa35ef329b248b7be2f927cd2714ebd244..e2ea3bbd9953477469350e1c477556947bf8f0e2 100644 --- a/aleksis/core/static/style.scss +++ b/aleksis/core/static/style.scss @@ -258,6 +258,15 @@ ul.footer-ul { line-height: 36px; } +// Language form in footer + +.language-field .select-dropdown { + @extend .white-text; +} + +.language-field svg path:first-child { + fill: white; +} /* Collections */ diff --git a/aleksis/core/tables.py b/aleksis/core/tables.py index 6f7ba041457f11420cf9155ff0cc8ca5012fd9d1..f562b61a151487c5047ef2861255a00ef7a5f785 100644 --- a/aleksis/core/tables.py +++ b/aleksis/core/tables.py @@ -4,6 +4,24 @@ import django_tables2 as tables from django_tables2.utils import A +class SchoolTermTable(tables.Table): + """Table to list persons.""" + + class Meta: + attrs = {"class": "responsive-table highlight"} + + name = tables.LinkColumn("edit_school_term", args=[A("id")]) + date_start = tables.Column() + date_end = tables.Column() + edit = tables.LinkColumn( + "edit_school_term", + args=[A("id")], + text=_("Edit"), + attrs={"a": {"class": "btn-flat waves-effect waves-orange orange-text"}}, + verbose_name=_("Actions"), + ) + + class PersonsTable(tables.Table): """Table to list persons.""" @@ -22,6 +40,7 @@ class GroupsTable(tables.Table): name = tables.LinkColumn("group_by_id", args=[A("id")]) short_name = tables.LinkColumn("group_by_id", args=[A("id")]) + school_term = tables.Column() class AdditionalFieldsTable(tables.Table): diff --git a/aleksis/core/templates/403.html b/aleksis/core/templates/403.html index c84e7f57ed0fa82dead672fde7ffd2c20a8bdc81..cbc962a93da9f8e8efd91d1af1b493c254834281 100644 --- a/aleksis/core/templates/403.html +++ b/aleksis/core/templates/403.html @@ -5,17 +5,23 @@ {% block content %} <div class="container"> <div class="card red"> - <div class="card white-text"> - <i class="material-icons small">error_outline</i> - <span class="card-title">{% trans "Error" %} (403): {% blocktrans %}You are not allowed to access the requested page or - object.{% endblocktrans %}</span> + <div class="card-content white-text"> + <i class="material-icons small left">error_outline</i> + <span class="card-title"> + {% if exception %} + {{ exception }} + {% else %} + {% trans "Error" %} (403): {% blocktrans %}You are not allowed to access the requested page or + object.{% endblocktrans %} + {% endif %} + </span> <p> {% blocktrans %} If you think this is an error in AlekSIS, please contact your site administrators: {% endblocktrans %} </p> - {% include "core/admins_list.html" %} + {% include "core/partials/admins_list.html" %} </div> </div> </div> diff --git a/aleksis/core/templates/404.html b/aleksis/core/templates/404.html index 45873cde006feaf32b27999cf357d4888d70f644..33c311fcaf4c106d44c53788b44426584d014c33 100644 --- a/aleksis/core/templates/404.html +++ b/aleksis/core/templates/404.html @@ -19,7 +19,7 @@ administrators: {% endblocktrans %} </p> - {% include "core/admins_list.html" %} + {% include "core/partials/admins_list.html" %} </div> </div> </div> diff --git a/aleksis/core/templates/500.html b/aleksis/core/templates/500.html index 621b9e424054cb321072450140c60548ebec3a0d..a084d76f804f9fa1089c225ac4a16db7bf360f69 100644 --- a/aleksis/core/templates/500.html +++ b/aleksis/core/templates/500.html @@ -6,7 +6,7 @@ <div class="container"> <div class="card red"> <div class="card-content white-text"> - <div class="material-icons small">error_outline</div> + <div class="material-icons small left">error_outline</div> <span class="card-title">{% trans "Error" %} (500): {% blocktrans %}An unexpected error has occured.{% endblocktrans %}</span> <p> @@ -15,7 +15,7 @@ error. You can also contact them directly: {% endblocktrans %} </p> - {% include "core/admins_list.html" %} + {% include "core/partials/admins_list.html" %} </div> </div> </div> diff --git a/aleksis/core/templates/503.html b/aleksis/core/templates/503.html index 9ed4fcecbaa5ef620264374fef1074288c4d7785..dd65828745eb846ee1f6b934043996ffa06759e1 100644 --- a/aleksis/core/templates/503.html +++ b/aleksis/core/templates/503.html @@ -6,7 +6,7 @@ <div class="container"> <div class="card red"> <div class="card-content white-text"> - <div class="material-icons small">error_outline</div> + <div class="material-icons small left">error_outline</div> <span class="card-title">{% blocktrans %}The maintenance mode is currently enabled. Please try again later.{% endblocktrans %}</span> <p> @@ -14,7 +14,7 @@ This page is currently unavailable. If this error persists, contact your site administrators: {% endblocktrans %} </p> - {% include "core/admins_list.html" %} + {% include "core/partials/admins_list.html" %} </div> </div> </div> diff --git a/aleksis/core/templates/core/edit_additional_field.html b/aleksis/core/templates/core/additional_field/edit.html similarity index 89% rename from aleksis/core/templates/core/edit_additional_field.html rename to aleksis/core/templates/core/additional_field/edit.html index b1487eb259b44f1425c950574f7df845d4e22129..cbfbdfffb0db5932710697a3e20eb5776f8a2e8b 100644 --- a/aleksis/core/templates/core/edit_additional_field.html +++ b/aleksis/core/templates/core/additional_field/edit.html @@ -11,7 +11,7 @@ <form method="post"> {% csrf_token %} {% form form=edit_additional_field_form %}{% endform %} - {% include "core/save_button.html" %} + {% include "core/partials/save_button.html" %} </form> {% endblock %} diff --git a/aleksis/core/templates/core/additional_fields.html b/aleksis/core/templates/core/additional_field/list.html similarity index 100% rename from aleksis/core/templates/core/additional_fields.html rename to aleksis/core/templates/core/additional_field/list.html diff --git a/aleksis/core/templates/core/base.html b/aleksis/core/templates/core/base.html index e30d428f060f540cbf7d4dbbb483a1371dbddf38..07db50c7fb3a29dc169543ae03c4f26dff1ec25f 100644 --- a/aleksis/core/templates/core/base.html +++ b/aleksis/core/templates/core/base.html @@ -7,7 +7,7 @@ <!DOCTYPE html> <html lang="{{ LANGUAGE_CODE }}"> <head> - {% include "core/meta.html" %} + {% include "core/partials/meta.html" %} <title> {% block no_browser_title %} @@ -81,14 +81,14 @@ </li> {% endif %} <li class="no-padding"> - {% include "core/sidenav.html" %} + {% include "core/partials/sidenav.html" %} </li> </ul> </header> <main role="main"> - {% include 'core/no_person.html' %} + {% include 'core/partials/no_person.html' %} {% if messages %} {% for message in messages %} @@ -121,24 +121,22 @@ <div class="container"> <div class="row no-margin footer-row-large"> <div class="col l6 s12 no-pad-left height-inherit"> - <p class="white-text valign-bot"> - {% include 'core/language_form.html' %} - - </p> + <div class="white-text valign-bot"> + {% include 'core/partials/language_form.html' %} + </div> </div> <div class="col xl15 l6 offset-xl01 s12 no-pad-right"> <ul class="no-margin right"> - {% include "core/footer-menu.html" %} + {% include "core/partials/footer-menu.html" %} </ul> </div> </div> <div class="row no-margin footer-row-small"> - <span class="white-text make-it-higher"> - {% include 'core/language_form.html' %} - - </span> + <div class="white-text make-it-higher"> + {% include 'core/partials/language_form.html' %} + </div> <ul class="no-margin footer-ul"> - {% include "core/footer-menu.html" %} + {% include "core/partials/footer-menu.html" %} </ul> </div> </div> diff --git a/aleksis/core/templates/core/base_print.html b/aleksis/core/templates/core/base_print.html index 6059d664fccb38ce18a55eb9ea6d1a406a6e9ad4..a229d723ed2afa4ec8ad1e7fa0e775889c9d9990 100644 --- a/aleksis/core/templates/core/base_print.html +++ b/aleksis/core/templates/core/base_print.html @@ -5,7 +5,7 @@ <!DOCTYPE html> <html lang="{{ LANGUAGE_CODE }}"> <head> - {% include "core/meta.html" %} + {% include "core/partials/meta.html" %} <title> {% block no_browser_title %} @@ -29,7 +29,7 @@ <table class="print-layout-table"> <thead> <tr class="no-border"> - <td> + <td class="print-layout-td"> <div class="header-space"> </div> </td> </tr> @@ -37,12 +37,14 @@ <tbody> <tr class="no-border"> - <td> + <td class="print-layout-td"> <div class="content"> <header> <div id="print-header" class="row"> <div class="col s6 logo"> - <img src="{{ request.site.preferences.theme__logo.url }}" alt="Logo" id="print-logo"/> + {% static "img/aleksis-banner.svg" as aleksis_banner %} + <img src="{% firstof request.site.preferences.theme__logo.url aleksis_banner %}" alt="Logo" + id="print-logo"/> </div> <div class="col s6 right-align"> <h5>{% block page_title %}{% endblock %}</h5> @@ -55,7 +57,7 @@ <footer> <div class="left"> - {{ SCHOOL.name }} + {{ request.site.preferences.school__name }} </div> <div class="right"> @@ -69,7 +71,7 @@ <tfoot> <tr class="no-border"> - <td> + <td class="print-layout-td"> <div class="footer-space"> </div> </td> </tr> diff --git a/aleksis/core/templates/core/groups_child_groups.html b/aleksis/core/templates/core/group/child_groups.html similarity index 100% rename from aleksis/core/templates/core/groups_child_groups.html rename to aleksis/core/templates/core/group/child_groups.html diff --git a/aleksis/core/templates/core/edit_group.html b/aleksis/core/templates/core/group/edit.html similarity index 88% rename from aleksis/core/templates/core/edit_group.html rename to aleksis/core/templates/core/group/edit.html index e7b3188076125020958105ee97ab0de00bd5f766..b26a28d1efc5ee292a257220bca00754512c1b99 100644 --- a/aleksis/core/templates/core/edit_group.html +++ b/aleksis/core/templates/core/group/edit.html @@ -11,7 +11,7 @@ <form method="post"> {% csrf_token %} {% form form=edit_group_form %}{% endform %} - {% include "core/save_button.html" %} + {% include "core/partials/save_button.html" %} </form> {% endblock %} diff --git a/aleksis/core/templates/core/group_full.html b/aleksis/core/templates/core/group/full.html similarity index 59% rename from aleksis/core/templates/core/group_full.html rename to aleksis/core/templates/core/group/full.html index 0add6f1a3041b723e4a4e3d87972e07a8daebf97..ba5bba0aa1208565331d748d106b4d2e982f7918 100644 --- a/aleksis/core/templates/core/group_full.html +++ b/aleksis/core/templates/core/group/full.html @@ -13,8 +13,9 @@ {% has_perm 'core.edit_group' user group as can_change_group %} {% has_perm 'core.change_group_preferences' user group as can_change_group_preferences %} + {% has_perm 'core.delete_group' user group as can_delete_group %} - {% if can_change_group or can_change_group_preferences %} + {% if can_change_group or can_change_group_preferences or can_delete_group %} <p> {% if can_change_group %} <a href="{% url 'edit_group_by_id' group.id %}" class="btn waves-effect waves-light"> @@ -22,6 +23,14 @@ {% trans "Edit" %} </a> {% endif %} + + {% if can_delete_group %} + <a href="{% url 'delete_group_by_id' group.id %}" class="btn waves-effect waves-light red"> + <i class="material-icons left">delete</i> + {% trans "Delete" %} + </a> + {% endif %} + {% if can_change_group_preferences %} <a href="{% url "preferences_group" group.id %}" class="btn waves-effect waves-light"> <i class="material-icons left">settings</i> @@ -31,6 +40,25 @@ </p> {% endif %} + <table> + <tr> + <th> + <i class="material-icons center" title="{% trans "Group type" %}">category</i> + </th> + <td> + {{ group.group_type }} + </td> + </tr> + <tr> + <th> + <i class="material-icons center" title="{% trans "Parent groups" %}">vertical_align_top</i> + </th> + <td> + {{ group.parent_groups.all|join:", " }} + </td> + </tr> + </table> + <h5>{% blocktrans %}Owners{% endblocktrans %}</h5> {% render_table owners_table %} diff --git a/aleksis/core/templates/core/groups.html b/aleksis/core/templates/core/group/list.html similarity index 100% rename from aleksis/core/templates/core/groups.html rename to aleksis/core/templates/core/group/list.html diff --git a/aleksis/core/templates/core/edit_group_type.html b/aleksis/core/templates/core/group_type/edit.html similarity index 89% rename from aleksis/core/templates/core/edit_group_type.html rename to aleksis/core/templates/core/group_type/edit.html index c857de98b893cf243c3c108f3c76b5aa142e6037..843975b16bb6ccbd7cf8eeed8f2d5713f5320e23 100644 --- a/aleksis/core/templates/core/edit_group_type.html +++ b/aleksis/core/templates/core/group_type/edit.html @@ -11,7 +11,7 @@ <form method="post"> {% csrf_token %} {% form form=edit_group_type_form %}{% endform %} - {% include "core/save_button.html" %} + {% include "core/partials/save_button.html" %} </form> {% endblock %} diff --git a/aleksis/core/templates/core/group_types.html b/aleksis/core/templates/core/group_type/list.html similarity index 100% rename from aleksis/core/templates/core/group_types.html rename to aleksis/core/templates/core/group_type/list.html diff --git a/aleksis/core/templates/core/index.html b/aleksis/core/templates/core/index.html index ac5d19b71bc6ac4acd00d7d3c509e1c07f089dcc..683adca900d46a8ba50790829db4d3ecd0ba2716 100644 --- a/aleksis/core/templates/core/index.html +++ b/aleksis/core/templates/core/index.html @@ -27,7 +27,7 @@ </div> {% endfor %} - {% include "core/announcements.html" with announcements=announcements %} + {% include "core/partials/announcements.html" with announcements=announcements %} <div class="row" id="live_load"> {% for widget in widgets %} diff --git a/aleksis/core/templates/core/language_form.html b/aleksis/core/templates/core/language_form.html deleted file mode 100644 index ee2637becccc05bf4aa4032cd71d3cb162b6f98e..0000000000000000000000000000000000000000 --- a/aleksis/core/templates/core/language_form.html +++ /dev/null @@ -1,19 +0,0 @@ -{# -*- engine:django -*- #} - -{% load i18n %} - - -<form action="{% url 'set_language' %}" method="post"> - {% csrf_token %} - <input name="next" type="hidden" value="{{ request.get_full_path }}"> - - {% get_current_language as LANGUAGE_CODE %} - {% get_available_languages as LANGUAGES %} - {% get_language_info_list for LANGUAGES as languages %} - {% for language in languages %} - <button type="submit" value="{{ language.code }}" name="language" - class="blue-text text-lighten-4 btn-flat {% if language == LANGUAGE_CODE %} disabled {% endif %}"> - {{ language.code }} - </button> - {% endfor %} -</form> diff --git a/aleksis/core/templates/core/data_management.html b/aleksis/core/templates/core/management/data_management.html similarity index 87% rename from aleksis/core/templates/core/data_management.html rename to aleksis/core/templates/core/management/data_management.html index 520dcb6d54beb20c482158cfa538ccf5cf2dc1ab..fa8e61f5dac1d1e01e5a773b68015cc9befe8d8f 100644 --- a/aleksis/core/templates/core/data_management.html +++ b/aleksis/core/templates/core/management/data_management.html @@ -8,5 +8,5 @@ {% block content %} {% get_menu "DATA_MANAGEMENT_MENU" as menu %} - {% include "core/on_page_menu.html" %} + {% include "core/partials/on_page_menu.html" %} {% endblock %} diff --git a/aleksis/core/templates/core/about.html b/aleksis/core/templates/core/pages/about.html similarity index 100% rename from aleksis/core/templates/core/about.html rename to aleksis/core/templates/core/pages/about.html diff --git a/aleksis/core/templates/offline.html b/aleksis/core/templates/core/pages/offline.html similarity index 92% rename from aleksis/core/templates/offline.html rename to aleksis/core/templates/core/pages/offline.html index 6961e03de97ea1a51096df239ac55880729e0fe7..a6a70dc19f074e8c3f3ede50b5e9b5b80b5682f1 100644 --- a/aleksis/core/templates/offline.html +++ b/aleksis/core/templates/core/pages/offline.html @@ -13,5 +13,5 @@ administrators: {% endblocktrans %} </p> - {% include "core/admins_list.html" %} + {% include "core/partials/admins_list.html" %} {% endblock %} diff --git a/aleksis/core/templates/core/pages/progress.html b/aleksis/core/templates/core/pages/progress.html new file mode 100644 index 0000000000000000000000000000000000000000..6292fb0d243b1b2ecb72ff1cec7c3b6339409c18 --- /dev/null +++ b/aleksis/core/templates/core/pages/progress.html @@ -0,0 +1,57 @@ +{% extends "core/base.html" %} +{% load i18n static %} + +{% block browser_title %} + {{ title }} +{% endblock %} +{% block page_title %} + {{ title }} +{% endblock %} + +{% block content %} + + <div class="container"> + <div class="row"> + <div class="progress center"> + <div class="indeterminate" style="width: 0;" id="progress-bar"></div> + </div> + <h6 class="center"> + {{ progress.title }} + </h6> + </div> + <div class="row"> + <noscript> + <div class="alert warning"> + <p> + <i class="material-icons left">warning</i> + {% blocktrans %} + Without activated JavaScript the progress status can't be updated. + {% endblocktrans %} + </p> + </div> + </noscript> + + <div id="messages"></div> + + <div id="result-box" style="display: none;"> + <div class="alert" id="result-alert"> + <div> + <i class="material-icons left" id="result-icon">check_circle</i> + <p id="result-text"></p> + </div> + </div> + + {% url "index" as index_url %} + <a class="btn waves-effect waves-light" href="{{ back_url|default:index_url }}"> + <i class="material-icons left">arrow_back</i> + {% trans "Go back" %} + </a> + </div> + </div> + </div> + + {{ progress|json_script:"progress_options" }} + <script src="{% static "js/helper.js" %}"></script> + <script src="{% static "celery_progress/celery_progress.js" %}"></script> + <script src="{% static "js/progress.js" %}"></script> +{% endblock %} diff --git a/aleksis/core/templates/core/system_status.html b/aleksis/core/templates/core/pages/system_status.html similarity index 80% rename from aleksis/core/templates/core/system_status.html rename to aleksis/core/templates/core/pages/system_status.html index a62d024cf9ff5875ce61c8305b84834e47393726..89c6ed65e3982a0318838a66ef7f6716935d0af3 100644 --- a/aleksis/core/templates/core/system_status.html +++ b/aleksis/core/templates/core/pages/system_status.html @@ -63,6 +63,43 @@ </div> </div> + {# Health checks #} + <div class="card"> + <div class="card-content"> + <span class="card-title"> {% blocktrans %}System health checks{% endblocktrans %}</span> + + <table> + <thead> + <tr> + + <th colspan="2">{% trans "Service" %}</th> + <th>{% trans "Status" %}</th> + <th>{% trans "Time taken" %}</th> + </tr> + </thead> + <tbody> + {% for plugin in plugins %} + <tr> + <td> + <a class="tooltipped" data-position="top" data-tooltip="{{ plugin.pretty_status }}"> + {% if plugin.status %} + <i class="material-icons green-text" aria-hidden="true" title="{{ plugin.pretty_status }}">check</i> + {% else %} + <i class="material-icons red-text" aria-hidden="true" title="{{ plugin.pretty_status }}">warning</i> + {% endif %} + </a> + </td> + <td>{{ plugin.identifier }}</td> + <td> + {{ plugin.pretty_status }} + </td> + <td>{{ plugin.time_taken|floatformat:4 }} {% trans "seconds" %}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> {% if tasks %} <div class="card"> diff --git a/aleksis/core/templates/core/admins_list.html b/aleksis/core/templates/core/partials/admins_list.html similarity index 100% rename from aleksis/core/templates/core/admins_list.html rename to aleksis/core/templates/core/partials/admins_list.html diff --git a/aleksis/core/templates/core/announcements.html b/aleksis/core/templates/core/partials/announcements.html similarity index 100% rename from aleksis/core/templates/core/announcements.html rename to aleksis/core/templates/core/partials/announcements.html diff --git a/aleksis/core/templates/core/crud_events.html b/aleksis/core/templates/core/partials/crud_events.html similarity index 100% rename from aleksis/core/templates/core/crud_events.html rename to aleksis/core/templates/core/partials/crud_events.html diff --git a/aleksis/core/templates/core/footer-menu.html b/aleksis/core/templates/core/partials/footer-menu.html similarity index 100% rename from aleksis/core/templates/core/footer-menu.html rename to aleksis/core/templates/core/partials/footer-menu.html diff --git a/aleksis/core/templates/core/partials/language_form.html b/aleksis/core/templates/core/partials/language_form.html new file mode 100644 index 0000000000000000000000000000000000000000..36ffa81a8c6017c4fdc86dac8749b0b681e0553f --- /dev/null +++ b/aleksis/core/templates/core/partials/language_form.html @@ -0,0 +1,31 @@ +{# -*- engine:django -*- #} + +{% load i18n %} + + +<form action="{% url 'set_language' %}" method="post" class="language-form"> + {% csrf_token %} + <input name="next" type="hidden" value="{{ request.get_full_path }}"> + + {% get_current_language as LANGUAGE_CODE %} + {% get_available_languages as LANGUAGES %} + {% get_language_info_list for LANGUAGES as languages %} + + {# Select #} + <div class="input-field language-field"> + <span>{% trans "Language" %}</span> + <select name="language" id="language-select"> + {% for language in languages %} + <option value="{{ language.code }}" {% if language.code == LANGUAGE_CODE %} + selected {% endif %}>{{ language.name_local }}</option> + {% endfor %} + </select> + </div> + + {# Submit button (only visible if JS isn't activated #} + <p class="language-submit-p"> + <button type="submit" class="btn-flat waves-effect waves-light white-text"> + {% trans "Select language" %} + </button> + </p> +</form> diff --git a/aleksis/core/templates/core/meta.html b/aleksis/core/templates/core/partials/meta.html similarity index 100% rename from aleksis/core/templates/core/meta.html rename to aleksis/core/templates/core/partials/meta.html diff --git a/aleksis/core/templates/core/no_person.html b/aleksis/core/templates/core/partials/no_person.html similarity index 100% rename from aleksis/core/templates/core/no_person.html rename to aleksis/core/templates/core/partials/no_person.html diff --git a/aleksis/core/templates/core/on_page_menu.html b/aleksis/core/templates/core/partials/on_page_menu.html similarity index 100% rename from aleksis/core/templates/core/on_page_menu.html rename to aleksis/core/templates/core/partials/on_page_menu.html diff --git a/aleksis/core/templates/core/save_button.html b/aleksis/core/templates/core/partials/save_button.html similarity index 100% rename from aleksis/core/templates/core/save_button.html rename to aleksis/core/templates/core/partials/save_button.html diff --git a/aleksis/core/templates/core/sidenav.html b/aleksis/core/templates/core/partials/sidenav.html similarity index 100% rename from aleksis/core/templates/core/sidenav.html rename to aleksis/core/templates/core/partials/sidenav.html diff --git a/aleksis/core/templates/core/turnable.html b/aleksis/core/templates/core/partials/turnable.html similarity index 100% rename from aleksis/core/templates/core/turnable.html rename to aleksis/core/templates/core/partials/turnable.html diff --git a/aleksis/core/templates/core/persons_accounts.html b/aleksis/core/templates/core/person/accounts.html similarity index 100% rename from aleksis/core/templates/core/persons_accounts.html rename to aleksis/core/templates/core/person/accounts.html diff --git a/aleksis/core/templates/core/person/collection.html b/aleksis/core/templates/core/person/collection.html new file mode 100644 index 0000000000000000000000000000000000000000..c23fa2360c00977485a9d32373ae826d7fe66b6d --- /dev/null +++ b/aleksis/core/templates/core/person/collection.html @@ -0,0 +1,8 @@ +<div class="collection"> + {% for person in persons %} + <a class="collection-item" href="{% url "person_by_id" person.pk %}"> + <i class="material-icons left">person</i> + {{ person }} + </a> + {% endfor %} +</div> diff --git a/aleksis/core/templates/core/edit_person.html b/aleksis/core/templates/core/person/edit.html similarity index 89% rename from aleksis/core/templates/core/edit_person.html rename to aleksis/core/templates/core/person/edit.html index 8a5d0ca39a8fa0cbfd437519a09723ce59278c2d..8f854610e3424b9da47f142cef41b71c9e0fb097 100644 --- a/aleksis/core/templates/core/edit_person.html +++ b/aleksis/core/templates/core/person/edit.html @@ -14,7 +14,7 @@ <form method="post" enctype="multipart/form-data"> {% csrf_token %} {% form form=edit_person_form %}{% endform %} - {% include "core/save_button.html" %} + {% include "core/partials/save_button.html" %} </form> {% endblock %} diff --git a/aleksis/core/templates/core/person_full.html b/aleksis/core/templates/core/person/full.html similarity index 81% rename from aleksis/core/templates/core/person_full.html rename to aleksis/core/templates/core/person/full.html index d676fd75d87889edaa2f61f09711f9de758ee3e1..73d2a4cf663999b2648e497e25559e83df69bb24 100644 --- a/aleksis/core/templates/core/person_full.html +++ b/aleksis/core/templates/core/person/full.html @@ -12,8 +12,9 @@ {% has_perm 'core.edit_person' user person as can_change_person %} {% has_perm 'core.change_person_preferences' user person as can_change_person_preferences %} + {% has_perm 'core.delete_person' user person as can_delete_person %} - {% if can_change_person or can_change_person_preferences %} + {% if can_change_person or can_change_person_preferences or can_delete_person %} <p> {% if can_change_person %} <a href="{% url 'edit_person_by_id' person.id %}" class="btn waves-effect waves-light"> @@ -22,6 +23,13 @@ </a> {% endif %} + {% if can_delete_person %} + <a href="{% url 'delete_person_by_id' person.id %}" class="btn waves-effect waves-light red"> + <i class="material-icons left">delete</i> + {% trans "Delete" %} + </a> + {% endif %} + {% if can_change_person_preferences %} <a href="{% url "preferences_person" person.id %}" class="btn waves-effect waves-light"> <i class="material-icons left">settings</i> @@ -109,6 +117,20 @@ {% endif %} </div> + {% if person.children.all and can_view_personal_details %} + <div class="col s12 m12"> + <h5>{% trans "Children" %}</h5> + {% include "core/person/collection.html" with persons=person.children.all %} + </div> + {% endif %} + + {% if person.guardians.all and can_view_personal_details %} + <div class="col s12 m12"> + <h5>{% trans "Guardians / Parents" %}</h5> + {% include "core/person/collection.html" with persons=person.guardians.all %} + </div> + {% endif %} + {% has_perm 'core.view_person_groups' user person as can_view_groups %} {% if can_view_groups %} <h5>{% blocktrans %}Groups{% endblocktrans %}</h5> diff --git a/aleksis/core/templates/core/persons.html b/aleksis/core/templates/core/person/list.html similarity index 52% rename from aleksis/core/templates/core/persons.html rename to aleksis/core/templates/core/person/list.html index dfecfb7c52b64cbdf49e70d06d1148e82128577f..1103199e002bda1394b3028df4483c69804268d8 100644 --- a/aleksis/core/templates/core/persons.html +++ b/aleksis/core/templates/core/person/list.html @@ -2,12 +2,21 @@ {% extends "core/base.html" %} -{% load i18n %} +{% load i18n rules %} {% load render_table from django_tables2 %} {% block browser_title %}{% blocktrans %}Persons{% endblocktrans %}{% endblock %} {% block page_title %}{% blocktrans %}Persons{% endblocktrans %}{% endblock %} {% block content %} + {% has_perm 'core.create_person' user person as can_create_person %} + + {% if can_create_person %} + <a class="btn green waves-effect waves-light" href="{% url 'create_person' %}"> + <i class="material-icons left">add</i> + {% trans "Create person" %} + </a> + {% endif %} + {% render_table persons_table %} {% endblock %} diff --git a/aleksis/core/templates/core/school_term/create.html b/aleksis/core/templates/core/school_term/create.html new file mode 100644 index 0000000000000000000000000000000000000000..a3e049112caeaf84095dde68c7fd8d7a32f75602 --- /dev/null +++ b/aleksis/core/templates/core/school_term/create.html @@ -0,0 +1,17 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% block browser_title %}{% blocktrans %}Create school term{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}Create school term{% endblocktrans %}{% endblock %} + +{% block content %} + + <form method="post"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/partials/save_button.html" %} + </form> + +{% endblock %} diff --git a/aleksis/core/templates/core/school_term/edit.html b/aleksis/core/templates/core/school_term/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..aa1b1dcf5015e876d0b9aa316d25da31673f0a3f --- /dev/null +++ b/aleksis/core/templates/core/school_term/edit.html @@ -0,0 +1,17 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% block browser_title %}{% blocktrans %}Edit school term{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}Edit school term{% endblocktrans %}{% endblock %} + +{% block content %} + + <form method="post"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/partials/save_button.html" %} + </form> + +{% endblock %} diff --git a/aleksis/core/templates/core/school_term/list.html b/aleksis/core/templates/core/school_term/list.html new file mode 100644 index 0000000000000000000000000000000000000000..14aa2f0a697a3efd54b27154b431ef3f424923de --- /dev/null +++ b/aleksis/core/templates/core/school_term/list.html @@ -0,0 +1,18 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} + +{% load i18n %} +{% load render_table from django_tables2 %} + +{% block browser_title %}{% blocktrans %}School terms{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}School terms{% endblocktrans %}{% endblock %} + +{% block content %} + <a class="btn green waves-effect waves-light" href="{% url 'create_school_term' %}"> + <i class="material-icons left">add</i> + {% trans "Create school term" %} + </a> + + {% render_table table %} +{% endblock %} diff --git a/aleksis/core/templates/dynamic_preferences/form.html b/aleksis/core/templates/dynamic_preferences/form.html index d8de37802c09a14327b0d949b5935fb69660c666..da3d608285c43673bd27932d0d19cef303b1cef7 100644 --- a/aleksis/core/templates/dynamic_preferences/form.html +++ b/aleksis/core/templates/dynamic_preferences/form.html @@ -22,7 +22,7 @@ <form action="" enctype="multipart/form-data" method="post"> {% csrf_token %} {% form form=form %}{% endform %} - {% include "core/save_button.html" with caption=_("Save preferences") %} + {% include "core/partials/save_button.html" with caption=_("Save preferences") %} </form> </div> {% endblock %} diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index 07736f7b8a7f5e2655e0a26d7444cc8c6f1f2c6c..51048e5c83bdb6c540254a36754f32cdc3059591 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -9,23 +9,30 @@ from django.views.i18n import JavaScriptCatalog import calendarweek.django import debug_toolbar from django_js_reverse.views import urls_js +from health_check.urls import urlpatterns as health_urls from two_factor.urls import urlpatterns as tf_urls from . import views +from .util.core_helpers import is_celery_enabled urlpatterns = [ path("", include("pwa.urls"), name="pwa"), path("about/", views.about, name="about_aleksis"), path("admin/", admin.site.urls), path("data_management/", views.data_management, name="data_management"), - path("status/", views.system_status, name="system_status"), + path("status/", views.SystemStatus.as_view(), name="system_status"), path("", include(tf_urls)), path("accounts/logout/", auth_views.LogoutView.as_view(), name="logout"), + path("school_terms/", views.SchoolTermListView.as_view(), name="school_terms"), + path("school_terms/create/", views.SchoolTermCreateView.as_view(), name="create_school_term"), + path("school_terms/<int:pk>/", views.SchoolTermEditView.as_view(), name="edit_school_term"), path("persons", views.persons, name="persons"), path("persons/accounts", views.persons_accounts, name="persons_accounts"), path("person", views.person, name="person"), + path("person/create", views.edit_person, name="create_person"), path("person/<int:id_>", views.person, name="person_by_id"), path("person/<int:id_>/edit", views.edit_person, name="edit_person_by_id"), + path("person/<int:id_>/delete", views.delete_person, name="delete_person_by_id"), path("groups", views.groups, name="groups"), path("groups/additional_fields", views.additional_fields, name="additional_fields"), path("groups/child_groups/", views.groups_child_groups, name="groups_child_groups"), @@ -47,6 +54,7 @@ urlpatterns = [ path("group/create", views.edit_group, name="create_group"), path("group/<int:id_>", views.group, name="group_by_id"), path("group/<int:id_>/edit", views.edit_group, name="edit_group_by_id"), + path("group/<int:id_>/delete", views.delete_group, name="delete_group_by_id"), path("", views.index, name="index"), path( "notifications/mark-read/<int:id_>", @@ -143,6 +151,7 @@ urlpatterns = [ {"registry_name": "group"}, name="preferences_group", ), + path("health/", include(health_urls)), ] # Serve static files from STATIC_ROOT to make it work with runserver @@ -158,6 +167,9 @@ if hasattr(settings, "TWILIO_ACCOUNT_SID"): urlpatterns += [path("", include(tf_twilio_urls))] +if is_celery_enabled(): + urlpatterns.append(path("celery_progress/", include("celery_progress.urls"))) + # Serve javascript-common if in development if settings.DEBUG: urlpatterns.append(path("__debug__/", include(debug_toolbar.urls))) diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py index 1500537334ebf5d339955c55a480776053b01d03..72e0a2fcd0c46c7ca8bae47dd79e79b70ec9d432 100644 --- a/aleksis/core/util/apps.py +++ b/aleksis/core/util/apps.py @@ -1,4 +1,3 @@ -from importlib import import_module from typing import Any, List, Optional, Sequence, Tuple import django.apps @@ -19,15 +18,6 @@ class AppConfig(django.apps.AppConfig): def ready(self): super().ready() - # Run model extension code - try: - import_module( - ".".join(self.__class__.__module__.split(".")[:-1] + ["model_extensions"]) - ) - except ImportError: - # ImportErrors are non-fatal because model extensions are optional. - pass - # Register default listeners pre_migrate.connect(self.pre_migrate, sender=self) post_migrate.connect(self.post_migrate, sender=self) @@ -38,13 +28,6 @@ class AppConfig(django.apps.AppConfig): # Getting an app ready means it should look at its config once self.preference_updated(self) - # Register system checks of this app - try: - import_module(".".join(self.__class__.__module__.split(".")[:-1] + ["checks"])) - except ImportError: - # ImportErrors are non-fatal because checks are optional. - pass - @classmethod def get_name(cls): """Get name of application package.""" @@ -206,6 +189,9 @@ class AppConfig(django.apps.AppConfig): pass def _maintain_default_data(self): + from django.contrib.auth.models import Permission + from django.contrib.contenttypes.models import ContentType + if not self.models_module: # This app does not have any models, so bail out early return @@ -214,3 +200,11 @@ class AppConfig(django.apps.AppConfig): if hasattr(model, "maintain_default_data"): # Method implemented by each model object; can be left out model.maintain_default_data() + if hasattr(model, "extra_permissions"): + ct = ContentType.objects.get_for_model(model) + for perm, verbose_name in model.extra_permissions: + Permission.objects.get_or_create( + codename=perm, + content_type=ct, + defaults={"name": verbose_name}, + ) diff --git a/aleksis/core/util/celery_progress.py b/aleksis/core/util/celery_progress.py new file mode 100644 index 0000000000000000000000000000000000000000..9de760eeb56b8ce535a0cd8e88f7380c5411f78d --- /dev/null +++ b/aleksis/core/util/celery_progress.py @@ -0,0 +1,34 @@ +from decimal import Decimal +from typing import Union + +from celery_progress.backend import PROGRESS_STATE, AbstractProgressRecorder + + +class ProgressRecorder(AbstractProgressRecorder): + def __init__(self, task): + self.task = task + self.messages = [] + self.total = 100 + self.current = 0 + + def set_progress(self, current: Union[int, float], **kwargs): + self.current = current + + percent = 0 + if self.total > 0: + percent = (Decimal(current) / Decimal(self.total)) * Decimal(100) + percent = float(round(percent, 2)) + + self.task.update_state( + state=PROGRESS_STATE, + meta={ + "current": current, + "total": self.total, + "percent": percent, + "messages": self.messages, + }, + ) + + def add_message(self, level: int, message: str, **kwargs): + self.messages.append((level, message)) + self.set_progress(self.current) diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py index dc62a7990844748e11859121854e44e13f2772f7..ba28f66292217b77a8d15eb1988774bbe09776fb 100644 --- a/aleksis/core/util/core_helpers.py +++ b/aleksis/core/util/core_helpers.py @@ -1,5 +1,6 @@ import os import pkgutil +import time from datetime import datetime, timedelta from importlib import import_module from itertools import groupby @@ -14,6 +15,10 @@ from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.functional import lazy +from django_global_request.middleware import get_request + +from aleksis.core.util import messages + def copyright_years(years: Sequence[int], seperator: str = ", ", joiner: str = "–") -> str: """Take a sequence of integegers and produces a string with ranges. @@ -170,6 +175,11 @@ def has_person(obj: Union[HttpRequest, Model]) -> bool: return True +def is_celery_enabled(): + """Check whether celery support is enabled.""" + return hasattr(settings, "CELERY_RESULT_BACKEND") + + def celery_optional(orig: Callable) -> Callable: """Add a decorator that makes Celery optional for a function. @@ -177,13 +187,13 @@ def celery_optional(orig: Callable) -> Callable: and calls its delay method when invoked; if not, it leaves it untouched and it is executed synchronously. """ - if hasattr(settings, "CELERY_RESULT_BACKEND"): + if is_celery_enabled(): from ..celery import app # noqa task = app.task(orig) def wrapped(*args, **kwargs): - if hasattr(settings, "CELERY_RESULT_BACKEND"): + if is_celery_enabled(): task.delay(*args, **kwargs) else: orig(*args, **kwargs) @@ -191,6 +201,108 @@ def celery_optional(orig: Callable) -> Callable: return wrapped +class DummyRecorder: + def set_progress(self, *args, **kwargs): + pass + + def add_message(self, level: int, message: str, **kwargs) -> Optional[Any]: + request = get_request() + return messages.add_message(request, level, message, **kwargs) + + +def celery_optional_progress(orig: Callable) -> Callable: + """Add a decorator that makes Celery with progress bar support 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. + + Additionally, it adds a recorder class as first argument + (`ProgressRecorder` if Celery is enabled, else `DummyRecoder`). + + This recorder provides the functions `set_progress` and `add_message` + which can be used to track the status of the task. + For further information, see the respective recorder classes. + + How to use + ---------- + 1. Write a function and include tracking methods + + :: + + from django.contrib import messages + + from aleksis.core.util.core_helpers import celery_optional_progress + + @celery_optional_progress + def do_something(recorder: Union[ProgressRecorder, DummyRecorder], foo, bar, baz=None): + # ... + recorder.total = len(list_with_data) + + for i, item in list_with_data: + # ... + recorder.set_progress(i + 1) + # ... + + recorder.add_message(messages.SUCCESS, "All data were imported successfully.") + + 2. Track process in view: + + :: + + def my_view(request): + context = {} + # ... + result = do_something(foo, bar, baz=baz) + + if result: + context = { + "title": _("Progress: Import data"), + "back_url": reverse("index"), + "progress": { + "task_id": result.task_id, + "title": _("Import objects …"), + "success": _("The import was done successfully."), + "error": _("There was a problem while importing data."), + }, + } + + # Render progress view + return render(request, "core/progress.html", context) + + # Render other view if Celery isn't enabled + return render(request, "my-app/other-view.html", context) + """ + + def recorder_func(self, *args, **kwargs): + if is_celery_enabled(): + from .celery_progress import ProgressRecorder # noqa + + recorder = ProgressRecorder(self) + else: + recorder = DummyRecorder() + orig(recorder, *args, **kwargs) + + # Needed to ensure that all messages are displayed by frontend + time.sleep(0.7) + + var_name = f"{orig.__module__}.{orig.__name__}" + + if is_celery_enabled(): + from ..celery import app # noqa + + task = app.task(recorder_func, bind=True, name=var_name) + + def wrapped(*args, **kwargs): + if is_celery_enabled(): + return task.delay(*args, **kwargs) + else: + recorder_func(None, *args, **kwargs) + return None + + return wrapped + + def path_and_rename(instance, filename: str, upload_to: str = "files") -> str: """Update path of an uploaded file and renames it to a random UUID in Django FileField.""" _, ext = os.path.splitext(filename) @@ -231,3 +343,9 @@ def objectgetter_optional( return eval(default) if default_eval else default # noqa:S307 return get_object + + +def handle_uploaded_file(f, filename: str): + with open(filename, "wb+") as destination: + for chunk in f.chunks(): + destination.write(chunk) diff --git a/aleksis/core/views.py b/aleksis/core/views.py index 45b15be491c57a71f0375d7a962c4ab398cbaa18..80ddd946c02d7e051f33cbdb5e4f4157c13384c5 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -2,20 +2,22 @@ from typing import Optional from django.apps import apps from django.conf import settings -from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.exceptions import PermissionDenied from django.core.paginator import Paginator from django.http import HttpRequest, HttpResponse, HttpResponseNotFound from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ -from django_tables2 import RequestConfig +import reversion +from django_tables2 import RequestConfig, SingleTableView from dynamic_preferences.forms import preference_form_builder from guardian.shortcuts import get_objects_for_user from haystack.inputs import AutoQuery from haystack.query import SearchQuerySet from haystack.views import SearchView -from rules.contrib.views import permission_required +from health_check.views import MainView +from rules.contrib.views import PermissionRequiredMixin, permission_required from .filters import GroupFilter from .forms import ( @@ -28,8 +30,10 @@ from .forms import ( GroupPreferenceForm, PersonPreferenceForm, PersonsAccountsFormSet, + SchoolTermForm, SitePreferenceForm, ) +from .mixins import AdvancedCreateView, AdvancedEditView from .models import ( AdditionalField, Announcement, @@ -38,13 +42,20 @@ from .models import ( GroupType, Notification, Person, + SchoolTerm, ) from .registries import ( group_preferences_registry, person_preferences_registry, site_preferences_registry, ) -from .tables import AdditionalFieldsTable, GroupsTable, GroupTypesTable, PersonsTable +from .tables import ( + AdditionalFieldsTable, + GroupsTable, + GroupTypesTable, + PersonsTable, + SchoolTermTable, +) from .util import messages from .util.apps import AppConfig from .util.core_helpers import objectgetter_optional @@ -77,7 +88,7 @@ def index(request: HttpRequest) -> HttpResponse: def offline(request: HttpRequest) -> HttpResponse: """Offline message for PWA.""" - return render(request, "core/offline.html") + return render(request, "core/pages/offline.html") def about(request: HttpRequest) -> HttpResponse: @@ -88,7 +99,38 @@ def about(request: HttpRequest) -> HttpResponse: filter(lambda a: isinstance(a, AppConfig), apps.get_app_configs()) ) - return render(request, "core/about.html", context) + return render(request, "core/pages/about.html", context) + + +class SchoolTermListView(SingleTableView, PermissionRequiredMixin): + """Table of all school terms.""" + + model = SchoolTerm + table_class = SchoolTermTable + permission_required = "core.view_schoolterm" + template_name = "core/school_term/list.html" + + +class SchoolTermCreateView(AdvancedCreateView, PermissionRequiredMixin): + """Create view for school terms.""" + + model = SchoolTerm + form_class = SchoolTermForm + permission_required = "core.add_schoolterm" + template_name = "core/school_term/create.html" + success_url = reverse_lazy("school_terms") + success_message = _("The school term has been created.") + + +class SchoolTermEditView(AdvancedEditView, PermissionRequiredMixin): + """Edit view for school terms.""" + + model = SchoolTerm + form_class = SchoolTermForm + permission_required = "core.edit_schoolterm" + template_name = "core/school_term/edit.html" + success_url = reverse_lazy("school_terms") + success_message = _("The school term has been saved.") @permission_required("core.view_persons") @@ -106,7 +148,7 @@ def persons(request: HttpRequest) -> HttpResponse: RequestConfig(request).configure(persons_table) context["persons_table"] = persons_table - return render(request, "core/persons.html", context) + return render(request, "core/person/list.html", context) @permission_required( @@ -127,7 +169,7 @@ def person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse: RequestConfig(request).configure(groups_table) context["groups_table"] = groups_table - return render(request, "core/person_full.html", context) + return render(request, "core/person/full.html", context) @permission_required("core.view_group", fn=objectgetter_optional(Group, None, False)) @@ -157,7 +199,7 @@ def group(request: HttpRequest, id_: int) -> HttpResponse: RequestConfig(request).configure(owners_table) context["owners_table"] = owners_table - return render(request, "core/group_full.html", context) + return render(request, "core/group/full.html", context) @permission_required("core.view_groups") @@ -173,7 +215,7 @@ def groups(request: HttpRequest) -> HttpResponse: RequestConfig(request).configure(groups_table) context["groups_table"] = groups_table - return render(request, "core/groups.html", context) + return render(request, "core/group/list.html", context) @permission_required("core.link_persons_accounts") @@ -193,7 +235,7 @@ def persons_accounts(request: HttpRequest) -> HttpResponse: context["persons_accounts_formset"] = persons_accounts_formset - return render(request, "core/persons_accounts.html", context) + return render(request, "core/person/accounts.html", context) @permission_required("core.assign_child_groups_to_groups") @@ -230,24 +272,31 @@ def groups_child_groups(request: HttpRequest) -> HttpResponse: context["group"] = group context["form"] = form - return render(request, "core/groups_child_groups.html", context) + return render(request, "core/group/child_groups.html", context) -@permission_required( - "core.edit_person", fn=objectgetter_optional(Person, "request.user.person", True) -) +@permission_required("core.edit_person", fn=objectgetter_optional(Person)) def edit_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse: """Edit view for a single person, defaulting to logged-in person.""" context = {} - person = objectgetter_optional(Person, "request.user.person", True)(request, id_) + person = objectgetter_optional(Person)(request, id_) context["person"] = person - edit_person_form = EditPersonForm(request.POST or None, request.FILES or None, instance=person) + if id_: + # Edit form for existing group + edit_person_form = EditGroupForm(request.POST or None, instance=person) + else: + # Empty form to create a new group + if request.user.has_perm("core.create_person"): + edit_person_form = EditPersonForm(request.POST or None) + else: + raise PermissionDenied() if request.method == "POST": if edit_person_form.is_valid(): - edit_person_form.save(commit=True) + with reversion.create_revision(): + edit_person_form.save(commit=True) messages.success(request, _("The person has been saved.")) # Redirect to self to ensure post-processed data is displayed @@ -255,7 +304,7 @@ def edit_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse context["edit_person_form"] = edit_person_form - return render(request, "core/edit_person.html", context) + return render(request, "core/person/edit.html", context) def get_group_by_id(request: HttpRequest, id_: Optional[int] = None): @@ -278,44 +327,54 @@ def edit_group(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse: edit_group_form = EditGroupForm(request.POST or None, instance=group) else: # Empty form to create a new group - edit_group_form = EditGroupForm(request.POST or None) + if request.user.has_perm("core.create_group"): + edit_group_form = EditGroupForm(request.POST or None) + else: + raise PermissionDenied() if request.method == "POST": if edit_group_form.is_valid(): - edit_group_form.save(commit=True) + with reversion.create_revision(): + group = edit_group_form.save(commit=True) messages.success(request, _("The group has been saved.")) - return redirect("groups") + return redirect("group_by_id", group.pk) context["edit_group_form"] = edit_group_form - return render(request, "core/edit_group.html", context) + return render(request, "core/group/edit.html", context) @permission_required("core.manage_data") def data_management(request: HttpRequest) -> HttpResponse: """View with special menu for data management.""" context = {} - return render(request, "core/data_management.html", context) + return render(request, "core/management/data_management.html", context) -@permission_required("core.view_system_status") -def system_status(request: HttpRequest) -> HttpResponse: +class SystemStatus(MainView, PermissionRequiredMixin): """View giving information about the system status.""" + + template_name = "core/pages/system_status.html" + permission_required = "core.view_system_status" context = {} - if "django_celery_results" in settings.INSTALLED_APPS: - from django_celery_results.models import TaskResult # noqa - from celery.task.control import inspect # noqa - if inspect().registered_tasks(): - job_list = list(inspect().registered_tasks().values())[0] - results = [] - for job in job_list: - results.append(TaskResult.objects.filter(task_name=job).last()) - context["tasks"] = results + def get(self, request, *args, **kwargs): + status_code = 500 if self.errors else 200 + + if "django_celery_results" in settings.INSTALLED_APPS: + from django_celery_results.models import TaskResult # noqa + from celery.task.control import inspect # noqa - return render(request, "core/system_status.html", context) + if inspect().registered_tasks(): + job_list = list(inspect().registered_tasks().values())[0] + results = [] + for job in job_list: + results.append(TaskResult.objects.filter(task_name=job).last()) + + context = {"plugins": self.plugins, "status_code": status_code} + return self.render_to_response(context, status=status_code) @permission_required( @@ -467,6 +526,33 @@ def preferences( return render(request, "dynamic_preferences/form.html", context) +@permission_required("core.delete_person", fn=objectgetter_optional(Person)) +def delete_person(request: HttpRequest, id_: int) -> HttpResponse: + """View to delete an person.""" + person = objectgetter_optional(Person)(request, id_) + + with reversion.create_revision(): + person.save() + + person.delete() + messages.success(request, _("The person has been deleted.")) + + return redirect("persons") + + +@permission_required("core.delete_group", fn=objectgetter_optional(Group)) +def delete_group(request: HttpRequest, id_: int) -> HttpResponse: + """View to delete an group.""" + group = objectgetter_optional(Group)(request, id_) + with reversion.create_revision(): + group.save() + + group.delete() + messages.success(request, _("The group has been deleted.")) + + return redirect("groups") + + @permission_required( "core.change_additionalfield", fn=objectgetter_optional(AdditionalField, None, False) ) @@ -499,7 +585,7 @@ def edit_additional_field(request: HttpRequest, id_: Optional[int] = None) -> Ht context["edit_additional_field_form"] = edit_additional_field_form - return render(request, "core/edit_additional_field.html", context) + return render(request, "core/additional_field/edit.html", context) @permission_required("core.view_additionalfield") @@ -517,7 +603,7 @@ def additional_fields(request: HttpRequest) -> HttpResponse: RequestConfig(request).configure(additional_fields_table) context["additional_fields_table"] = additional_fields_table - return render(request, "core/additional_fields.html", context) + return render(request, "core/additional_field/list.html", context) @permission_required( @@ -557,7 +643,7 @@ def edit_group_type(request: HttpRequest, id_: Optional[int] = None) -> HttpResp context["edit_group_type_form"] = edit_group_type_form - return render(request, "core/edit_group_type.html", context) + return render(request, "core/group_type/edit.html", context) @permission_required("core.view_grouptype") @@ -573,7 +659,7 @@ def group_types(request: HttpRequest) -> HttpResponse: RequestConfig(request).configure(group_types_table) context["group_types_table"] = group_types_table - return render(request, "core/group_types.html", context) + return render(request, "core/group_type/list.html", context) @permission_required("core.delete_grouptype", fn=objectgetter_optional(GroupType, None, False)) diff --git a/poetry.lock b/poetry.lock index 2722fb59608ca242b559e8991b00dca23df2328f..3ec2732928bd9ead01bf5b5834c62191f7a5f0a5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -31,10 +31,10 @@ description = "ASGI specs, helper code, and adapters" name = "asgiref" optional = false python-versions = ">=3.5" -version = "3.2.7" +version = "3.2.8" [package.extras] -tests = ["pytest (>=4.3.0,<4.4.0)", "pytest-asyncio (>=0.10.0,<0.11.0)"] +tests = ["pytest", "pytest-asyncio"] [[package]] category = "dev" @@ -147,7 +147,7 @@ description = "Define boolean algebras, create and parse boolean expressions and name = "boolean.py" optional = false python-versions = "*" -version = "3.7" +version = "3.8" [[package]] category = "main" @@ -166,10 +166,11 @@ 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.4" +version = "4.4.5" [package.dependencies] billiard = ">=3.6.3.0,<4.0" +future = ">=0.18.0" kombu = ">=4.6.10,<4.7" pytz = ">0.0-dev" vine = "1.3.0" @@ -227,13 +228,26 @@ version = "0.10" [package.dependencies] django-appconf = ">=0.4.1" +[[package]] +category = "main" +description = "Drop in, configurable, dependency-free progress bars for your Django/Celery applications." +name = "celery-progress" +optional = false +python-versions = "*" +version = "0.0.10" + +[package.extras] +rabbitmq = ["channels-rabbitmq"] +redis = ["channels-redis"] +websockets = ["channels"] + [[package]] category = "main" description = "Python package for providing Mozilla's CA Bundle." name = "certifi" optional = false python-versions = "*" -version = "2020.4.5.1" +version = "2020.4.5.2" [[package]] category = "main" @@ -509,11 +523,11 @@ 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" +python-versions = ">=3.5" +version = "2.3.0" [package.dependencies] -Django = ">=1.11" +Django = ">=2.2" [[package]] category = "main" @@ -532,10 +546,10 @@ description = "Implementation of per object permissions for Django." name = "django-guardian" optional = false python-versions = ">=3.5" -version = "2.2.0" +version = "2.3.0" [package.dependencies] -Django = ">=2.1" +Django = ">=2.2" [[package]] category = "main" @@ -563,6 +577,17 @@ version = "3.0b1" [package.dependencies] Django = ">=2.2" +[[package]] +category = "main" +description = "Run checks on services like databases, queue servers, celery processes, etc." +name = "django-health-check" +optional = false +python-versions = "*" +version = "3.12.1" + +[package.dependencies] +django = ">=1.11" + [[package]] category = "main" description = "A reusable app for cropping images easily and non-destructively in Django" @@ -698,6 +723,10 @@ version = "3.0.1" Django = ">=1.11.3" babel = "*" +[package.dependencies.phonenumbers] +optional = true +version = ">=7.0.2" + [package.extras] phonenumbers = ["phonenumbers (>=7.0.2)"] phonenumberslite = ["phonenumberslite (>=7.0.2)"] @@ -971,7 +1000,7 @@ description = "the modular source code checker: pep8 pyflakes and co" name = "flake8" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "3.8.2" +version = "3.8.3" [package.dependencies] mccabe = ">=0.6.0,<0.7.0" @@ -1108,6 +1137,14 @@ version = "0.0.13" flake8 = ">=3.0.0" restructuredtext_lint = "*" +[[package]] +category = "main" +description = "Clean single-source support for Python 3 and 2" +name = "future" +optional = true +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "0.18.2" + [[package]] category = "dev" description = "Git Object Database" @@ -1161,14 +1198,14 @@ 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.6.0" +version = "1.6.1" [package.dependencies] zipp = ">=0.5" [package.extras] docs = ["sphinx", "rst.linker"] -testing = ["packaging", "importlib-resources"] +testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] [[package]] category = "dev" @@ -1273,7 +1310,7 @@ description = "More routines for operating on iterables, beyond itertools" name = "more-itertools" optional = false python-versions = ">=3.5" -version = "8.3.0" +version = "8.4.0" [[package]] category = "dev" @@ -1341,10 +1378,10 @@ description = "PostgreSQL interface library" name = "pg8000" optional = false python-versions = ">=3.5" -version = "1.15.2" +version = "1.15.3" [package.dependencies] -scramp = "1.1.1" +scramp = "1.2.0" [[package]] category = "main" @@ -1352,7 +1389,7 @@ description = "Python version of Google's common library for parsing, formatting name = "phonenumbers" optional = false python-versions = "*" -version = "8.12.4" +version = "8.12.5" [[package]] category = "main" @@ -1378,6 +1415,17 @@ version = ">=0.12" [package.extras] dev = ["pre-commit", "tox"] +[[package]] +category = "main" +description = "Cross-platform lib for process and system monitoring in Python." +name = "psutil" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "5.7.0" + +[package.extras] +enum = ["enum34"] + [[package]] category = "main" description = "psycopg2 - Python-PostgreSQL Database Adapter" @@ -1392,7 +1440,7 @@ description = "library with cross-python path, ini-parsing, io, code, log facili name = "py" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.8.1" +version = "1.8.2" [[package]] category = "main" @@ -1509,11 +1557,11 @@ description = "Pytest plugin for measuring coverage." name = "pytest-cov" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.9.0" +version = "2.10.0" [package.dependencies] coverage = ">=4.4" -pytest = ">=3.6" +pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] @@ -1680,7 +1728,7 @@ description = "Alternative regular expression module, to replace re." name = "regex" optional = false python-versions = "*" -version = "2020.5.14" +version = "2020.6.8" [[package]] category = "main" @@ -1740,7 +1788,7 @@ description = "An implementation of the SCRAM protocol." name = "scramp" optional = false python-versions = ">=3.5" -version = "1.1.1" +version = "1.2.0" [[package]] category = "dev" @@ -1799,7 +1847,7 @@ description = "Python documentation generator" name = "sphinx" optional = false python-versions = ">=3.5" -version = "3.0.4" +version = "3.1.1" [package.dependencies] Jinja2 = ">=2.3" @@ -1822,7 +1870,7 @@ sphinxcontrib-serializinghtml = "*" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.770)", "docutils-stubs"] +lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.780)", "docutils-stubs"] test = ["pytest", "pytest-cov", "html5lib", "typed-ast", "cython"] [[package]] @@ -1932,12 +1980,11 @@ category = "dev" description = "Manage dynamic plugins for Python applications" name = "stevedore" optional = false -python-versions = "*" -version = "1.32.0" +python-versions = ">=3.6" +version = "2.0.0" [package.dependencies] pbr = ">=2.0.0,<2.1.0 || >2.1.0" -six = ">=1.10.0" [[package]] category = "dev" @@ -2019,7 +2066,7 @@ description = "Twilio API client and TwiML generator" name = "twilio" optional = false python-versions = "*" -version = "6.41.0" +version = "6.42.0" [package.dependencies] PyJWT = ">=1.4.2" @@ -2073,7 +2120,7 @@ description = "Measures the displayed width of unicode strings in a terminal" name = "wcwidth" optional = false python-versions = "*" -version = "0.2.3" +version = "0.2.4" [[package]] category = "main" @@ -2113,7 +2160,7 @@ celery = ["Celery", "django-celery-results", "django-celery-beat", "django-celer ldap = ["django-auth-ldap"] [metadata] -content-hash = "3e85d3bfff56719272c13ad591ca4ccd7a2b72324a33c2192be383536b66b5f8" +content-hash = "c6e41b0779be08ae7f643169504696ca0f6812408755a3acc9c1b0dc95752561" python-versions = "^3.7" [metadata.files] @@ -2130,8 +2177,8 @@ appdirs = [ {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] asgiref = [ - {file = "asgiref-3.2.7-py2.py3-none-any.whl", hash = "sha256:9ca8b952a0a9afa61d30aa6d3d9b570bb3fd6bafcf7ec9e6bed43b936133db1c"}, - {file = "asgiref-3.2.7.tar.gz", hash = "sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5"}, + {file = "asgiref-3.2.8-py3-none-any.whl", hash = "sha256:783254c9ec6f914f671919bbcef4346d4e57866bd7ed988ae79f881bbc0a9be8"}, + {file = "asgiref-3.2.8.tar.gz", hash = "sha256:a46c83b7d46212ec937e9ddb571cda2b1384b3d02f7edde9372e2778d1782d38"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, @@ -2167,24 +2214,28 @@ bleach = [ {file = "bleach-3.1.5.tar.gz", hash = "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b"}, ] "boolean.py" = [ - {file = "boolean.py-3.7-py2.py3-none-any.whl", hash = "sha256:82ae181f9c85cb5c893a5a4daba9f24d60b538a7dd27fd0c6752a77eba4fbeff"}, - {file = "boolean.py-3.7.tar.gz", hash = "sha256:bd19b412435611ecc712603d0fd7d0e280e24698e7a6e3d5f610473870c5dd1e"}, + {file = "boolean.py-3.8-py2.py3-none-any.whl", hash = "sha256:d75da0fd0354425fa64f6bbc6cec6ae1485d0eec3447b73187ff8cbf9b572e26"}, + {file = "boolean.py-3.8.tar.gz", hash = "sha256:cc24e20f985d60cd4a3a5a1c0956dd12611159d32a75081dabd0c9ab981acaa4"}, ] calendarweek = [ {file = "calendarweek-0.4.5-py3-none-any.whl", hash = "sha256:b35fcc087073969d017cede62a7295bcd714a1304bcb4c4e2b0f23acb0265fb1"}, {file = "calendarweek-0.4.5.tar.gz", hash = "sha256:5b1788ca435022f9348fc81a718974e51dd85d080f9aa3dad717df70a1bc6e1f"}, ] celery = [ - {file = "celery-4.4.4-py2.py3-none-any.whl", hash = "sha256:9ae2e73b93cc7d6b48b56aaf49a68c91752d0ffd7dfdcc47f842ca79a6f13eae"}, - {file = "celery-4.4.4.tar.gz", hash = "sha256:c2037b6a8463da43b19969a0fc13f9023ceca6352b4dd51be01c66fbbb13647e"}, + {file = "celery-4.4.5-py2.py3-none-any.whl", hash = "sha256:d1762d6065522879f341c3d67c2b9fe4615eb79756d59acb1434601d4aca474b"}, + {file = "celery-4.4.5.tar.gz", hash = "sha256:c3f4173f83ceb5a5c986c5fdaefb9456de3b0729a72a5776e46bd405fda7b647"}, ] celery-haystack = [ {file = "celery-haystack-0.10.tar.gz", hash = "sha256:b6e2a3c70071bef0838ca1a7d9f14fae6c2ecf385704092e59b82147a1ee552e"}, {file = "celery_haystack-0.10-py2.py3-none-any.whl", hash = "sha256:ec1f39050661e033f554de99cb9393c2e94427667ff5401f16393b2a68f888fc"}, ] +celery-progress = [ + {file = "celery-progress-0.0.10.tar.gz", hash = "sha256:3f7b35e1e6c79eec38f5647b024aa74193d0a41d5b47ecbb85b66f9ca68d5261"}, + {file = "celery_progress-0.0.10-py3-none-any.whl", hash = "sha256:90941bf3aaeac9333d554a2191fa6cd81ef323472329ace0dd77344ac6aab092"}, +] certifi = [ - {file = "certifi-2020.4.5.1-py2.py3-none-any.whl", hash = "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304"}, - {file = "certifi-2020.4.5.1.tar.gz", hash = "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"}, + {file = "certifi-2020.4.5.2-py2.py3-none-any.whl", hash = "sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc"}, + {file = "certifi-2020.4.5.2.tar.gz", hash = "sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1"}, ] chardet = [ {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, @@ -2309,16 +2360,16 @@ django-favicon-plus-reloaded = [ {file = "django_favicon_plus_reloaded-1.0.4-py3-none-any.whl", hash = "sha256:26e4316d41328a61ced52c7fc0ead795f0eb194d6a30311c34a9833c6fe30a7c"}, ] 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"}, + {file = "django-filter-2.3.0.tar.gz", hash = "sha256:11e63dd759835d9ba7a763926ffb2662cf8a6dcb4c7971a95064de34dbc7e5af"}, + {file = "django_filter-2.3.0-py3-none-any.whl", hash = "sha256:616848eab6fc50193a1b3730140c49b60c57a3eda1f7fc57fa8505ac156c6c75"}, ] 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-guardian = [ - {file = "django-guardian-2.2.0.tar.gz", hash = "sha256:8cacf49ebcc1e545f0a8997971eec0fe109f5ed31fc2a569a7bf5615453696e2"}, - {file = "django_guardian-2.2.0-py3-none-any.whl", hash = "sha256:ac81e88372fdf1795d84ba065550e739b42e9c6d07cdf201cf5bbf9efa7f396c"}, + {file = "django-guardian-2.3.0.tar.gz", hash = "sha256:ed2de26e4defb800919c5749fb1bbe370d72829fbd72895b6cf4f7f1a7607e1b"}, + {file = "django_guardian-2.3.0-py3-none-any.whl", hash = "sha256:0e70706c6cda88ddaf8849bddb525b8df49de05ba0798d4b3506049f0d95cbc8"}, ] django-hattori = [ {file = "django-hattori-0.2.1.tar.gz", hash = "sha256:6953d40881317252f19f62c4e7fe8058924b852c7498bc42beb7bc4d268c252c"}, @@ -2328,6 +2379,10 @@ django-haystack = [ {file = "django-haystack-3.0b1.tar.gz", hash = "sha256:9dba64f5c76cf147ac382d4a4a270f30d30a45a3a7a1738a9d05c96d18777c07"}, {file = "django_haystack-3.0b1-py3-none-any.whl", hash = "sha256:b83705e1cf8141cd1755fc6683ac65fea4e1281f4b4306bc9224af96495b0df3"}, ] +django-health-check = [ + {file = "django-health-check-3.12.1.tar.gz", hash = "sha256:0563827e003d25fd4d9ebbd7467dea5f390435628d645aaa63f8889deaded73a"}, + {file = "django_health_check-3.12.1-py2.py3-none-any.whl", hash = "sha256:9e6b7d93d4902901474efd4e25d31b5aaea7563b570c0260adce52cd3c3a9e36"}, +] django-image-cropping = [ {file = "django-image-cropping-1.4.0.tar.gz", hash = "sha256:6cc4a6bd8901e69b710caceea29b942fdb202da26626313cd9271ae989a83a52"}, {file = "django_image_cropping-1.4.0-py3-none-any.whl", hash = "sha256:fe6a139c6d5dfc480f2a1d4e7e3e928d5edaefc898e17be66bc5f73140762ad9"}, @@ -2446,8 +2501,8 @@ faker = [ {file = "Faker-4.1.0.tar.gz", hash = "sha256:103c46b9701a151299c5bffe6fefcd4fb5fb04c3b5d06bee4952d36255d44ea2"}, ] flake8 = [ - {file = "flake8-3.8.2-py2.py3-none-any.whl", hash = "sha256:ccaa799ef9893cebe69fdfefed76865aeaefbb94cb8545617b2298786a4de9a5"}, - {file = "flake8-3.8.2.tar.gz", hash = "sha256:c69ac1668e434d37a2d2880b3ca9aafd54b3a10a3ac1ab101d22f29e29cf8634"}, + {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, + {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, ] flake8-bandit = [ {file = "flake8_bandit-2.1.2.tar.gz", hash = "sha256:687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b"}, @@ -2486,6 +2541,9 @@ flake8-polyfill = [ flake8-rst-docstrings = [ {file = "flake8-rst-docstrings-0.0.13.tar.gz", hash = "sha256:b1b619d81d879b874533973ac04ee5d823fdbe8c9f3701bfe802bb41813997b4"}, ] +future = [ + {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, +] gitdb = [ {file = "gitdb-4.0.5-py3-none-any.whl", hash = "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac"}, {file = "gitdb-4.0.5.tar.gz", hash = "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"}, @@ -2507,8 +2565,8 @@ imagesize = [ {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"}, - {file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"}, + {file = "importlib_metadata-1.6.1-py2.py3-none-any.whl", hash = "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958"}, + {file = "importlib_metadata-1.6.1.tar.gz", hash = "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545"}, ] isort = [ {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, @@ -2579,8 +2637,8 @@ mccabe = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] more-itertools = [ - {file = "more-itertools-8.3.0.tar.gz", hash = "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be"}, - {file = "more_itertools-8.3.0-py3-none-any.whl", hash = "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982"}, + {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, + {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"}, ] mypy = [ {file = "mypy-0.770-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:a34b577cdf6313bf24755f7a0e3f3c326d5c1f4fe7422d1d06498eb25ad0c600"}, @@ -2618,12 +2676,12 @@ persisting-theory = [ {file = "persisting-theory-0.2.1.tar.gz", hash = "sha256:00ff7dcc8f481ff75c770ca5797d968e8725b6df1f77fe0cf7d20fa1e5790c0a"}, ] pg8000 = [ - {file = "pg8000-1.15.2-py3-none-any.whl", hash = "sha256:2bfdd03c2623302af655ef089a958ff329b2035c9d9aea406b5e4dac9c007524"}, - {file = "pg8000-1.15.2.tar.gz", hash = "sha256:eb42ba62fbc048c91d5cf1ac729e0ea4ee329cc526bddafed4e7a8aa6b57fbbb"}, + {file = "pg8000-1.15.3-py3-none-any.whl", hash = "sha256:79d2e761343e582dec6698cf7c06d49c33255cbafba29ff298dd4690fffb7d80"}, + {file = "pg8000-1.15.3.tar.gz", hash = "sha256:af97353076b8e5d271d91c64c8ca806e2157d11b7862c90ff6f0e23be0fc217d"}, ] phonenumbers = [ - {file = "phonenumbers-8.12.4-py2.py3-none-any.whl", hash = "sha256:c6c43d6459aac85b646d6b7a7ab79b3b629eb168f0e9b851b331e2e5872bbd01"}, - {file = "phonenumbers-8.12.4.tar.gz", hash = "sha256:46c5997fe076026aa2d4b66d0c53eea4babae2e808e8a5f39c09e2dfa6612d08"}, + {file = "phonenumbers-8.12.5-py2.py3-none-any.whl", hash = "sha256:67199749bbc5cb7c3a09f623e29f23dc294df6582968841f1ca2acbc06faafc1"}, + {file = "phonenumbers-8.12.5.tar.gz", hash = "sha256:3586f19abeb92aa6b539d7a4757cb507cf54efcd78224e895caf20fbdde07c26"}, ] pillow = [ {file = "Pillow-7.1.2-cp35-cp35m-macosx_10_10_intel.whl", hash = "sha256:ae2b270f9a0b8822b98655cb3a59cdb1bd54a34807c6c56b76dd2e786c3b7db3"}, @@ -2654,6 +2712,19 @@ pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] +psutil = [ + {file = "psutil-5.7.0-cp27-none-win32.whl", hash = "sha256:298af2f14b635c3c7118fd9183843f4e73e681bb6f01e12284d4d70d48a60953"}, + {file = "psutil-5.7.0-cp27-none-win_amd64.whl", hash = "sha256:75e22717d4dbc7ca529ec5063000b2b294fc9a367f9c9ede1f65846c7955fd38"}, + {file = "psutil-5.7.0-cp35-cp35m-win32.whl", hash = "sha256:f344ca230dd8e8d5eee16827596f1c22ec0876127c28e800d7ae20ed44c4b310"}, + {file = "psutil-5.7.0-cp35-cp35m-win_amd64.whl", hash = "sha256:e2d0c5b07c6fe5a87fa27b7855017edb0d52ee73b71e6ee368fae268605cc3f5"}, + {file = "psutil-5.7.0-cp36-cp36m-win32.whl", hash = "sha256:a02f4ac50d4a23253b68233b07e7cdb567bd025b982d5cf0ee78296990c22d9e"}, + {file = "psutil-5.7.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1413f4158eb50e110777c4f15d7c759521703bd6beb58926f1d562da40180058"}, + {file = "psutil-5.7.0-cp37-cp37m-win32.whl", hash = "sha256:d008ddc00c6906ec80040d26dc2d3e3962109e40ad07fd8a12d0284ce5e0e4f8"}, + {file = "psutil-5.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:73f35ab66c6c7a9ce82ba44b1e9b1050be2a80cd4dcc3352cc108656b115c74f"}, + {file = "psutil-5.7.0-cp38-cp38-win32.whl", hash = "sha256:60b86f327c198561f101a92be1995f9ae0399736b6eced8f24af41ec64fb88d4"}, + {file = "psutil-5.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:d84029b190c8a66a946e28b4d3934d2ca1528ec94764b180f7d6ea57b0e75e26"}, + {file = "psutil-5.7.0.tar.gz", hash = "sha256:685ec16ca14d079455892f25bd124df26ff9137664af445563c1bd36629b5e0e"}, +] psycopg2 = [ {file = "psycopg2-2.8.5-cp27-cp27m-win32.whl", hash = "sha256:a0984ff49e176062fcdc8a5a2a670c9bb1704a2f69548bce8f8a7bad41c661bf"}, {file = "psycopg2-2.8.5-cp27-cp27m-win_amd64.whl", hash = "sha256:acf56d564e443e3dea152efe972b1434058244298a94348fc518d6dd6a9fb0bb"}, @@ -2670,8 +2741,8 @@ psycopg2 = [ {file = "psycopg2-2.8.5.tar.gz", hash = "sha256:f7d46240f7a1ae1dd95aab38bd74f7428d46531f69219954266d669da60c0818"}, ] py = [ - {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, - {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, + {file = "py-1.8.2-py2.py3-none-any.whl", hash = "sha256:a673fa23d7000440cc885c17dbd34fafcb7d7a6e230b29f6766400de36a33c44"}, + {file = "py-1.8.2.tar.gz", hash = "sha256:f3b3a4c36512a4c4f024041ab51866f11761cc169670204b235f6b20523d4e6b"}, ] pyasn1 = [ {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, @@ -2764,8 +2835,8 @@ pytest = [ {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, ] pytest-cov = [ - {file = "pytest-cov-2.9.0.tar.gz", hash = "sha256:b6a814b8ed6247bd81ff47f038511b57fe1ce7f4cc25b9106f1a4b106f1d9322"}, - {file = "pytest_cov-2.9.0-py2.py3-none-any.whl", hash = "sha256:c87dfd8465d865655a8213859f1b4749b43448b5fae465cb981e16d52a811424"}, + {file = "pytest-cov-2.10.0.tar.gz", hash = "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87"}, + {file = "pytest_cov-2.10.0-py2.py3-none-any.whl", hash = "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c"}, ] pytest-django = [ {file = "pytest-django-3.9.0.tar.gz", hash = "sha256:664e5f42242e5e182519388f01b9f25d824a9feb7cd17d8f863c8d776f38baf9"}, @@ -2826,27 +2897,27 @@ redis = [ {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, ] regex = [ - {file = "regex-2020.5.14-cp27-cp27m-win32.whl", hash = "sha256:e565569fc28e3ba3e475ec344d87ed3cd8ba2d575335359749298a0899fe122e"}, - {file = "regex-2020.5.14-cp27-cp27m-win_amd64.whl", hash = "sha256:d466967ac8e45244b9dfe302bbe5e3337f8dc4dec8d7d10f5e950d83b140d33a"}, - {file = "regex-2020.5.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:27ff7325b297fb6e5ebb70d10437592433601c423f5acf86e5bc1ee2919b9561"}, - {file = "regex-2020.5.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ea55b80eb0d1c3f1d8d784264a6764f931e172480a2f1868f2536444c5f01e01"}, - {file = "regex-2020.5.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:c9bce6e006fbe771a02bda468ec40ffccbf954803b470a0345ad39c603402577"}, - {file = "regex-2020.5.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:d881c2e657c51d89f02ae4c21d9adbef76b8325fe4d5cf0e9ad62f850f3a98fd"}, - {file = "regex-2020.5.14-cp36-cp36m-win32.whl", hash = "sha256:99568f00f7bf820c620f01721485cad230f3fb28f57d8fbf4a7967ec2e446994"}, - {file = "regex-2020.5.14-cp36-cp36m-win_amd64.whl", hash = "sha256:70c14743320a68c5dac7fc5a0f685be63bc2024b062fe2aaccc4acc3d01b14a1"}, - {file = "regex-2020.5.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:a7c37f048ec3920783abab99f8f4036561a174f1314302ccfa4e9ad31cb00eb4"}, - {file = "regex-2020.5.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:89d76ce33d3266173f5be80bd4efcbd5196cafc34100fdab814f9b228dee0fa4"}, - {file = "regex-2020.5.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:51f17abbe973c7673a61863516bdc9c0ef467407a940f39501e786a07406699c"}, - {file = "regex-2020.5.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ce5cc53aa9fbbf6712e92c7cf268274eaff30f6bd12a0754e8133d85a8fb0f5f"}, - {file = "regex-2020.5.14-cp37-cp37m-win32.whl", hash = "sha256:8044d1c085d49673aadb3d7dc20ef5cb5b030c7a4fa253a593dda2eab3059929"}, - {file = "regex-2020.5.14-cp37-cp37m-win_amd64.whl", hash = "sha256:c2062c7d470751b648f1cacc3f54460aebfc261285f14bc6da49c6943bd48bdd"}, - {file = "regex-2020.5.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:329ba35d711e3428db6b45a53b1b13a0a8ba07cbbcf10bbed291a7da45f106c3"}, - {file = "regex-2020.5.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:579ea215c81d18da550b62ff97ee187b99f1b135fd894a13451e00986a080cad"}, - {file = "regex-2020.5.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:3a9394197664e35566242686d84dfd264c07b20f93514e2e09d3c2b3ffdf78fe"}, - {file = "regex-2020.5.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ce367d21f33e23a84fb83a641b3834dd7dd8e9318ad8ff677fbfae5915a239f7"}, - {file = "regex-2020.5.14-cp38-cp38-win32.whl", hash = "sha256:1386e75c9d1574f6aa2e4eb5355374c8e55f9aac97e224a8a5a6abded0f9c927"}, - {file = "regex-2020.5.14-cp38-cp38-win_amd64.whl", hash = "sha256:7e61be8a2900897803c293247ef87366d5df86bf701083b6c43119c7c6c99108"}, - {file = "regex-2020.5.14.tar.gz", hash = "sha256:ce450ffbfec93821ab1fea94779a8440e10cf63819be6e176eb1973a6017aff5"}, + {file = "regex-2020.6.8-cp27-cp27m-win32.whl", hash = "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"}, + {file = "regex-2020.6.8-cp27-cp27m-win_amd64.whl", hash = "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938"}, + {file = "regex-2020.6.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89"}, + {file = "regex-2020.6.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868"}, + {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f"}, + {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded"}, + {file = "regex-2020.6.8-cp36-cp36m-win32.whl", hash = "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3"}, + {file = "regex-2020.6.8-cp36-cp36m-win_amd64.whl", hash = "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd"}, + {file = "regex-2020.6.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a"}, + {file = "regex-2020.6.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae"}, + {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a"}, + {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3"}, + {file = "regex-2020.6.8-cp37-cp37m-win32.whl", hash = "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9"}, + {file = "regex-2020.6.8-cp37-cp37m-win_amd64.whl", hash = "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29"}, + {file = "regex-2020.6.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610"}, + {file = "regex-2020.6.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387"}, + {file = "regex-2020.6.8-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910"}, + {file = "regex-2020.6.8-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf"}, + {file = "regex-2020.6.8-cp38-cp38-win32.whl", hash = "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754"}, + {file = "regex-2020.6.8-cp38-cp38-win_amd64.whl", hash = "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5"}, + {file = "regex-2020.6.8.tar.gz", hash = "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac"}, ] requests = [ {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, @@ -2863,8 +2934,8 @@ safety = [ {file = "safety-1.9.0.tar.gz", hash = "sha256:23bf20690d4400edc795836b0c983c2b4cbbb922233108ff925b7dd7750f00c9"}, ] scramp = [ - {file = "scramp-1.1.1-py3-none-any.whl", hash = "sha256:a2c740624642de84f77327da8f56b2f030c5afd10deccaedbb8eb6108a66dfc1"}, - {file = "scramp-1.1.1.tar.gz", hash = "sha256:b57eb0ae2f9240b15b5d0dab2ea8e40b43eef13ac66d3f627a79ef85a6da0927"}, + {file = "scramp-1.2.0-py3-none-any.whl", hash = "sha256:74815c25aad1fe0b5fb994e96c3de63e8695164358a80138352aaadfa4760350"}, + {file = "scramp-1.2.0.tar.gz", hash = "sha256:d6865ed1d135ddb124a619d7cd3a5b505f69a7c92e248024dd7e48bc77752af5"}, ] selenium = [ {file = "selenium-3.141.0-py2.py3-none-any.whl", hash = "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c"}, @@ -2891,8 +2962,8 @@ spdx-license-list = [ {file = "spdx_license_list-0.5.0.tar.gz", hash = "sha256:40cd53ff16401bab7059e6d1ef61839196b12079929a2763a50145d3b6949bc1"}, ] sphinx = [ - {file = "Sphinx-3.0.4-py3-none-any.whl", hash = "sha256:779a519adbd3a70fc7c468af08c5e74829868b0a5b34587b33340e010291856c"}, - {file = "Sphinx-3.0.4.tar.gz", hash = "sha256:ea64df287958ee5aac46be7ac2b7277305b0381d213728c3a49d8bb9b8415807"}, + {file = "Sphinx-3.1.1-py3-none-any.whl", hash = "sha256:97c9e3bcce2f61d9f5edf131299ee9d1219630598d9f9a8791459a4d9e815be5"}, + {file = "Sphinx-3.1.1.tar.gz", hash = "sha256:74fbead182a611ce1444f50218a1c5fc70b6cc547f64948f5182fb30a2a20258"}, ] sphinx-autodoc-typehints = [ {file = "sphinx-autodoc-typehints-1.10.3.tar.gz", hash = "sha256:a6b3180167479aca2c4d1ed3b5cb044a70a76cccd6b38662d39288ebd9f0dff0"}, @@ -2931,8 +3002,8 @@ sqlparse = [ {file = "sqlparse-0.3.1.tar.gz", hash = "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"}, ] stevedore = [ - {file = "stevedore-1.32.0-py2.py3-none-any.whl", hash = "sha256:a4e7dc759fb0f2e3e2f7d8ffe2358c19d45b9b8297f393ef1256858d82f69c9b"}, - {file = "stevedore-1.32.0.tar.gz", hash = "sha256:18afaf1d623af5950cc0f7e75e70f917784c73b652a34a12d90b309451b5500b"}, + {file = "stevedore-2.0.0-py3-none-any.whl", hash = "sha256:471c920412265cc809540ae6fb01f3f02aba89c79bbc7091372f4745a50f9691"}, + {file = "stevedore-2.0.0.tar.gz", hash = "sha256:001e90cd704be6470d46cc9076434e2d0d566c1379187e7013eb296d3a6032d9"}, ] termcolor = [ {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"}, @@ -2962,7 +3033,7 @@ tqdm = [ {file = "tqdm-4.46.1.tar.gz", hash = "sha256:cd140979c2bebd2311dfb14781d8f19bd5a9debb92dcab9f6ef899c987fcf71f"}, ] twilio = [ - {file = "twilio-6.41.0.tar.gz", hash = "sha256:7c6329118583852bb06a2065dd2987a012310e5dfd834ef821d736b059bd1c74"}, + {file = "twilio-6.42.0.tar.gz", hash = "sha256:9d423321d577cab175712e4cc3636b68534572c3ab1c6c5b191925d3abac0223"}, ] typed-ast = [ {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, @@ -3001,8 +3072,8 @@ vine = [ {file = "vine-1.3.0.tar.gz", hash = "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87"}, ] wcwidth = [ - {file = "wcwidth-0.2.3-py2.py3-none-any.whl", hash = "sha256:980fbf4f3c196c0f329cdcd1e84c554d6a211f18e252e525a0cf4223154a41d6"}, - {file = "wcwidth-0.2.3.tar.gz", hash = "sha256:edbc2b718b4db6cdf393eefe3a420183947d6aa312505ce6754516f458ff8830"}, + {file = "wcwidth-0.2.4-py2.py3-none-any.whl", hash = "sha256:79375666b9954d4a1a10739315816324c3e73110af9d0e102d906fdb0aec009f"}, + {file = "wcwidth-0.2.4.tar.gz", hash = "sha256:8c6b5b6ee1360b842645f336d9e5d68c55817c26d3050f46b235ef2bc650e48f"}, ] webencodings = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, diff --git a/pyproject.toml b/pyproject.toml index 47ff4e661cf5237fb6f2245f84de728f97ff2b0d..62c12008070434590ad996dba60104d3dbc13373 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,9 @@ spdx-license-list = "^0.5.0" license-expression = "^1.2" django-reversion = "^3.0.7" django-favicon-plus-reloaded = "^1.0.4" +django-health-check = "^3.12.1" +psutil = "^5.7.0" +celery-progress = "^0.0.10" [tool.poetry.extras] ldap = ["django-auth-ldap"]