diff --git a/Dockerfile b/Dockerfile index 3eb58d43d6b60bcfa899de3df1fb11d156f60217..c2b7db5ebe8f4b8f2198ceb62fdfd984e541e897 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9-buster AS core +FROM debian:bullseye-slim AS core # Build arguments ARG EXTRAS="ldap,s3" @@ -12,6 +12,7 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK 1 ENV PIP_NO_CACHE_DIR 1 ENV PIP_EXTRA_INDEX_URL https://edugit.org/api/v4/projects/461/packages/pypi/simple ENV PIP_USE_DEPRECATED legacy-resolver +ENV DEBIAN_FRONTEND noninteractive # Configure app settings for build and runtime ENV ALEKSIS_static__root /usr/share/aleksis/static @@ -25,14 +26,18 @@ RUN apt-get -y update && \ eatmydata apt-get -y upgrade && \ eatmydata apt-get install -y --no-install-recommends \ build-essential \ + chromium \ dumb-init \ gettext \ libpq5 \ libpq-dev \ libssl-dev \ postgresql-client \ - yarnpkg && \ - eatmydata pip install uwsgi + python3-dev \ + python3-pip \ + uwsgi \ + uwsgi-plugin-python3 \ + yarnpkg # Install extra dependencies RUN case ",$EXTRAS," in \ @@ -73,7 +78,8 @@ RUN set -e; \ libpq-dev \ libssl-dev \ libldap2-dev \ - libsasl2-dev; \ + libsasl2-dev \ + python3-dev; \ eatmydata apt-get autoremove --purge -y; \ apt-get clean -y; \ rm -rf /root/.cache diff --git a/README.rst b/README.rst index 067f83e32bfedf6251c91b5d5e42162c1db97b9e..5946b0d4eb15331b918e95446fddbc440250bfcb 100644 --- a/README.rst +++ b/README.rst @@ -36,16 +36,16 @@ Licence :: - Copyright © 2021 magicfelix <felix@felix-zauberer.de> - Copyright © 2017, 2018, 2019, 2020 Jonathan Weth <wethjo@katharineum.de> - Copyright © 2017, 2018, 2019 Frank Poetzsch-Heffter <p-h@katharineum.de> - Copyright © 2018, 2019, 2020 Julian Leucker <leuckeju@katharineum.de> - Copyright © 2018, 2019, 2020 Hangzhi Yu <yuha@katharineum.de> - Copyright © 2019, 2020 Dominik George <dominik.george@teckids.org> - Copyright © 2019, 2020 Tom Teichler <tom.teichler@teckids.org> + Copyright © 2017, 2018, 2019, 2020, 2021 Jonathan Weth <wethjo@katharineum.de> + Copyright © 2017, 2018, 2019, 2020 Frank Poetzsch-Heffter <p-h@katharineum.de> + Copyright © 2018, 2019, 2020, 2021 Julian Leucker <leuckeju@katharineum.de> + Copyright © 2018, 2019, 2020, 2021 Hangzhi Yu <yuha@katharineum.de> + Copyright © 2019, 2020, 2021 Dominik George <dominik.george@teckids.org> + Copyright © 2019, 2020, 2021 Tom Teichler <tom.teichler@teckids.org> Copyright © 2019 mirabilos <thorsten.glaser@teckids.org> + Copyright © 2021 magicfelix <felix@felix-zauberer.de> - Licenced under the EUPL, version 1.2 or later + Licenced under the EUPL, version 1.2 or later, by Teckids e.V. (Bonn, Germany). Please see the LICENCE.rst file accompanying this distribution for the full licence text or on the `European Union Public Licence`_ website diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py index a3b9b7a9a3957b9af7ad0b9ddeb57d00a630ca73..fa48d04407a4e047ea98e6ce7b1102158ba25fbb 100644 --- a/aleksis/core/apps.py +++ b/aleksis/core/apps.py @@ -1,4 +1,4 @@ -from typing import Any, List, Optional, Tuple +from typing import Any, Optional import django.apps from django.apps import apps @@ -28,14 +28,14 @@ class CoreConfig(AppConfig): } licence = "EUPL-1.2+" copyright_info = ( - ([2021], "magicfelix", "felix@felix-zauberer.de"), - ([2017, 2018, 2019, 2020], "Jonathan Weth", "wethjo@katharineum.de"), - ([2017, 2018, 2019], "Frank Poetzsch-Heffter", "p-h@katharineum.de"), - ([2018, 2019, 2020], "Julian Leucker", "leuckeju@katharineum.de"), - ([2018, 2019, 2020], "Hangzhi Yu", "yuha@katharineum.de"), - ([2019, 2020], "Dominik George", "dominik.george@teckids.org"), - ([2019, 2020], "Tom Teichler", "tom.teichler@teckids.org"), + ([2017, 2018, 2019, 2020, 2021], "Jonathan Weth", "wethjo@katharineum.de"), + ([2017, 2018, 2019, 2020], "Frank Poetzsch-Heffter", "p-h@katharineum.de"), + ([2018, 2019, 2020, 2021], "Julian Leucker", "leuckeju@katharineum.de"), + ([2018, 2019, 2020, 2021], "Hangzhi Yu", "yuha@katharineum.de"), + ([2019, 2020, 2021], "Dominik George", "dominik.george@teckids.org"), + ([2019, 2020, 2021], "Tom Teichler", "tom.teichler@teckids.org"), ([2019], "mirabilos", "thorsten.glaser@teckids.org"), + ([2021], "magicfelix", "felix@felix-zauberer.de"), ) def ready(self): @@ -107,11 +107,9 @@ class CoreConfig(AppConfig): verbosity: int, interactive: bool, using: str, - plan: List[Tuple], - apps: django.apps.registry.Apps, **kwargs, ) -> None: - super().post_migrate(app_config, verbosity, interactive, using, plan, apps) + super().post_migrate(app_config, verbosity, interactive, using, **kwargs) # Ensure presence of an OTP YubiKey default config apps.get_model("otp_yubikey", "ValidationService").objects.using(using).update_or_create( diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py index feb7d996b7cca83c040ef7add8a5355e82335e82..dd738da735eded0be57bf60cb7f0f651e38d333e 100644 --- a/aleksis/core/forms.py +++ b/aleksis/core/forms.py @@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _ from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, Select2Widget from dynamic_preferences.forms import PreferenceForm +from guardian.core import ObjectPermissionChecker from material import Fieldset, Layout, Row from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm @@ -137,6 +138,22 @@ class EditPersonForm(ExtensibleForm): required=False, label=_("New user"), help_text=_("Create a new account") ) + def __init__(self, request: HttpRequest, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Disable non-editable fields + person_fields = set([field.name for field in Person.syncable_fields()]).intersection( + set(self.fields) + ) + + if self.instance: + checker = ObjectPermissionChecker(request.user) + checker.prefetch_perms([self.instance]) + + for field in person_fields: + if not checker.has_perm(f"core.change_person_field_{field}", self.instance): + self.fields[field].disabled = True + def clean(self) -> None: # Use code implemented in dedicated form to verify user selection return PersonAccountForm.clean(self) diff --git a/aleksis/core/locale/de_DE/LC_MESSAGES/django.po b/aleksis/core/locale/de_DE/LC_MESSAGES/django.po index c26e99b1bc38619b7b131cb67718516e4b6da5e1..d8a3b4e5a53a776cd9fb0433267539bde11efe99 100644 --- a/aleksis/core/locale/de_DE/LC_MESSAGES/django.po +++ b/aleksis/core/locale/de_DE/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: AlekSIS (School Information System) 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-01-11 21:30+0100\n" -"PO-Revision-Date: 2021-02-17 12:13+0000\n" +"PO-Revision-Date: 2021-03-14 11:51+0000\n" "Last-Translator: Jonathan Weth <teckids@jonathanweth.de>\n" "Language-Team: German <https://translate.edugit.org/projects/aleksis/aleksis/" "de/>\n" @@ -619,7 +619,7 @@ msgstr "Menü" #: models.py:787 msgid "Icon" -msgstr "Icon" +msgstr "Symbol" #: models.py:793 msgid "Custom menu item" diff --git a/aleksis/core/migrations/0001_initial.py b/aleksis/core/migrations/0001_initial.py index 07e4bf433df4d8aec43814493d7dde6527d1f8f1..14041d3eca0f1830dc4da2154f318ad7f7c1d516 100644 --- a/aleksis/core/migrations/0001_initial.py +++ b/aleksis/core/migrations/0001_initial.py @@ -37,7 +37,7 @@ class Migration(migrations.Migration): name='AdditionalField', 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)), + ('extended_data', models.JSONField(default=dict, editable=False)), ('title', models.CharField(max_length=255, verbose_name='Title of field')), ('field_type', models.CharField(choices=[('BooleanField', 'Boolean (Yes/No)'), ('CharField', 'Text (one line)'), ('DateField', 'Date'), ('DateTimeField', 'Date and time'), ('DecimalField', 'Decimal number'), ('EmailField', 'E-mail address'), ('IntegerField', 'Integer'), ('GenericIPAddressField', 'IP address'), ('NullBooleanField', 'Boolean or empty (Yes/No/Neither)'), ('TextField', 'Text (multi-line)'), ('TimeField', 'Time'), ('URLField', 'URL / Link')], max_length=50, verbose_name='Type of field')), ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')), @@ -54,7 +54,7 @@ class Migration(migrations.Migration): name='Announcement', 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)), + ('extended_data', models.JSONField(default=dict, editable=False)), ('title', models.CharField(max_length=150, verbose_name='Title')), ('description', models.TextField(blank=True, max_length=500, verbose_name='Description')), ('link', models.URLField(blank=True, verbose_name='Link to detailed view')), @@ -71,7 +71,7 @@ class Migration(migrations.Migration): name='CustomMenu', 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)), + ('extended_data', models.JSONField(default=dict, editable=False)), ('name', models.CharField(max_length=100, unique=True, verbose_name='Menu ID')), ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')), ], @@ -87,7 +87,7 @@ class Migration(migrations.Migration): name='Group', 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)), + ('extended_data', models.JSONField(default=dict, editable=False)), ('name', models.CharField(max_length=255, unique=True, verbose_name='Long name')), ('short_name', models.CharField(blank=True, max_length=255, null=True, unique=True, verbose_name='Short name')), ('additional_fields', models.ManyToManyField(to='core.AdditionalField', verbose_name='Additional fields')), @@ -106,7 +106,7 @@ class Migration(migrations.Migration): name='Person', 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)), + ('extended_data', models.JSONField(default=dict, editable=False)), ('is_active', models.BooleanField(default=True, verbose_name='Is person active?')), ('first_name', models.CharField(max_length=255, verbose_name='First name')), ('last_name', models.CharField(max_length=255, verbose_name='Last name')), @@ -178,7 +178,7 @@ class Migration(migrations.Migration): name='PersonGroupThrough', 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)), + ('extended_data', models.JSONField(default=dict, editable=False)), ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Group')), ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Person')), ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')), @@ -194,7 +194,7 @@ class Migration(migrations.Migration): name='Notification', 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)), + ('extended_data', models.JSONField(default=dict, editable=False)), ('sender', models.CharField(max_length=100, verbose_name='Sender')), ('title', models.CharField(max_length=150, verbose_name='Title')), ('description', models.TextField(max_length=500, verbose_name='Description')), @@ -216,7 +216,7 @@ class Migration(migrations.Migration): name='GroupType', 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)), + ('extended_data', models.JSONField(default=dict, editable=False)), ('name', models.CharField(max_length=50, verbose_name='Title of type')), ('description', models.CharField(max_length=500, verbose_name='Description')), ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')), @@ -283,7 +283,7 @@ class Migration(migrations.Migration): name='CustomMenuItem', 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)), + ('extended_data', models.JSONField(default=dict, editable=False)), ('name', models.CharField(max_length=150, verbose_name='Name')), ('url', models.URLField(verbose_name='Link')), ('icon', models.CharField(blank=True, choices=[('3d_rotation', '3d_rotation'), ('ac_unit', 'ac_unit'), ('access_alarm', 'access_alarm'), ('access_alarms', 'access_alarms'), ('access_time', 'access_time'), ('accessibility', 'accessibility'), ('accessible', 'accessible'), ('account_balance', 'account_balance'), ('account_balance_wallet', 'account_balance_wallet'), ('account_box', 'account_box'), ('account_circle', 'account_circle'), ('adb', 'adb'), ('add', 'add'), ('add_a_photo', 'add_a_photo'), ('add_alarm', 'add_alarm'), ('add_alert', 'add_alert'), ('add_box', 'add_box'), ('add_circle', 'add_circle'), ('add_circle_outline', 'add_circle_outline'), ('add_location', 'add_location'), ('add_shopping_cart', 'add_shopping_cart'), ('add_to_photos', 'add_to_photos'), ('add_to_queue', 'add_to_queue'), ('adjust', 'adjust'), ('airline_seat_flat', 'airline_seat_flat'), ('airline_seat_flat_angled', 'airline_seat_flat_angled'), ('airline_seat_individual_suite', 'airline_seat_individual_suite'), ('airline_seat_legroom_extra', 'airline_seat_legroom_extra'), ('airline_seat_legroom_normal', 'airline_seat_legroom_normal'), ('airline_seat_legroom_reduced', 'airline_seat_legroom_reduced'), ('airline_seat_recline_extra', 'airline_seat_recline_extra'), ('airline_seat_recline_normal', 'airline_seat_recline_normal'), ('airplanemode_active', 'airplanemode_active'), ('airplanemode_inactive', 'airplanemode_inactive'), ('airplay', 'airplay'), ('airport_shuttle', 'airport_shuttle'), ('alarm', 'alarm'), ('alarm_add', 'alarm_add'), ('alarm_off', 'alarm_off'), ('alarm_on', 'alarm_on'), ('album', 'album'), ('all_inclusive', 'all_inclusive'), ('all_out', 'all_out'), ('android', 'android'), ('announcement', 'announcement'), ('apps', 'apps'), ('archive', 'archive'), ('arrow_back', 'arrow_back'), ('arrow_downward', 'arrow_downward'), ('arrow_drop_down', 'arrow_drop_down'), ('arrow_drop_down_circle', 'arrow_drop_down_circle'), ('arrow_drop_up', 'arrow_drop_up'), ('arrow_forward', 'arrow_forward'), ('arrow_upward', 'arrow_upward'), ('art_track', 'art_track'), ('aspect_ratio', 'aspect_ratio'), ('assessment', 'assessment'), ('assignment', 'assignment'), ('assignment_ind', 'assignment_ind'), ('assignment_late', 'assignment_late'), ('assignment_return', 'assignment_return'), ('assignment_returned', 'assignment_returned'), ('assignment_turned_in', 'assignment_turned_in'), ('assistant', 'assistant'), ('assistant_photo', 'assistant_photo'), ('attach_file', 'attach_file'), ('attach_money', 'attach_money'), ('attachment', 'attachment'), ('audiotrack', 'audiotrack'), ('autorenew', 'autorenew'), ('av_timer', 'av_timer'), ('backspace', 'backspace'), ('backup', 'backup'), ('battery_alert', 'battery_alert'), ('battery_charging_full', 'battery_charging_full'), ('battery_full', 'battery_full'), ('battery_std', 'battery_std'), ('battery_unknown', 'battery_unknown'), ('beach_access', 'beach_access'), ('beenhere', 'beenhere'), ('block', 'block'), ('bluetooth', 'bluetooth'), ('bluetooth_audio', 'bluetooth_audio'), ('bluetooth_connected', 'bluetooth_connected'), ('bluetooth_disabled', 'bluetooth_disabled'), ('bluetooth_searching', 'bluetooth_searching'), ('blur_circular', 'blur_circular'), ('blur_linear', 'blur_linear'), ('blur_off', 'blur_off'), ('blur_on', 'blur_on'), ('book', 'book'), ('bookmark', 'bookmark'), ('bookmark_border', 'bookmark_border'), ('border_all', 'border_all'), ('border_bottom', 'border_bottom'), ('border_clear', 'border_clear'), ('border_color', 'border_color'), ('border_horizontal', 'border_horizontal'), ('border_inner', 'border_inner'), ('border_left', 'border_left'), ('border_outer', 'border_outer'), ('border_right', 'border_right'), ('border_style', 'border_style'), ('border_top', 'border_top'), ('border_vertical', 'border_vertical'), ('branding_watermark', 'branding_watermark'), ('brightness_1', 'brightness_1'), ('brightness_2', 'brightness_2'), ('brightness_3', 'brightness_3'), ('brightness_4', 'brightness_4'), ('brightness_5', 'brightness_5'), ('brightness_6', 'brightness_6'), ('brightness_7', 'brightness_7'), ('brightness_auto', 'brightness_auto'), ('brightness_high', 'brightness_high'), ('brightness_low', 'brightness_low'), ('brightness_medium', 'brightness_medium'), ('broken_image', 'broken_image'), ('brush', 'brush'), ('bubble_chart', 'bubble_chart'), ('bug_report', 'bug_report'), ('build', 'build'), ('burst_mode', 'burst_mode'), ('business', 'business'), ('business_center', 'business_center'), ('cached', 'cached'), ('cake', 'cake'), ('call', 'call'), ('call_end', 'call_end'), ('call_made', 'call_made'), ('call_merge', 'call_merge'), ('call_missed', 'call_missed'), ('call_missed_outgoing', 'call_missed_outgoing'), ('call_received', 'call_received'), ('call_split', 'call_split'), ('call_to_action', 'call_to_action'), ('camera', 'camera'), ('camera_alt', 'camera_alt'), ('camera_enhance', 'camera_enhance'), ('camera_front', 'camera_front'), ('camera_rear', 'camera_rear'), ('camera_roll', 'camera_roll'), ('cancel', 'cancel'), ('card_giftcard', 'card_giftcard'), ('card_membership', 'card_membership'), ('card_travel', 'card_travel'), ('casino', 'casino'), ('cast', 'cast'), ('cast_connected', 'cast_connected'), ('center_focus_strong', 'center_focus_strong'), ('center_focus_weak', 'center_focus_weak'), ('change_history', 'change_history'), ('chat', 'chat'), ('chat_bubble', 'chat_bubble'), ('chat_bubble_outline', 'chat_bubble_outline'), ('check', 'check'), ('check_box', 'check_box'), ('check_box_outline_blank', 'check_box_outline_blank'), ('check_circle', 'check_circle'), ('chevron_left', 'chevron_left'), ('chevron_right', 'chevron_right'), ('child_care', 'child_care'), ('child_friendly', 'child_friendly'), ('chrome_reader_mode', 'chrome_reader_mode'), ('class', 'class'), ('clear', 'clear'), ('clear_all', 'clear_all'), ('close', 'close'), ('closed_caption', 'closed_caption'), ('cloud', 'cloud'), ('cloud_circle', 'cloud_circle'), ('cloud_done', 'cloud_done'), ('cloud_download', 'cloud_download'), ('cloud_off', 'cloud_off'), ('cloud_queue', 'cloud_queue'), ('cloud_upload', 'cloud_upload'), ('code', 'code'), ('collections', 'collections'), ('collections_bookmark', 'collections_bookmark'), ('color_lens', 'color_lens'), ('colorize', 'colorize'), ('comment', 'comment'), ('compare', 'compare'), ('compare_arrows', 'compare_arrows'), ('computer', 'computer'), ('confirmation_number', 'confirmation_number'), ('contact_mail', 'contact_mail'), ('contact_phone', 'contact_phone'), ('contacts', 'contacts'), ('content_copy', 'content_copy'), ('content_cut', 'content_cut'), ('content_paste', 'content_paste'), ('control_point', 'control_point'), ('control_point_duplicate', 'control_point_duplicate'), ('copyright', 'copyright'), ('create', 'create'), ('create_new_folder', 'create_new_folder'), ('credit_card', 'credit_card'), ('crop', 'crop'), ('crop_16_9', 'crop_16_9'), ('crop_3_2', 'crop_3_2'), ('crop_5_4', 'crop_5_4'), ('crop_7_5', 'crop_7_5'), ('crop_din', 'crop_din'), ('crop_free', 'crop_free'), ('crop_landscape', 'crop_landscape'), ('crop_original', 'crop_original'), ('crop_portrait', 'crop_portrait'), ('crop_rotate', 'crop_rotate'), ('crop_square', 'crop_square'), ('dashboard', 'dashboard'), ('data_usage', 'data_usage'), ('date_range', 'date_range'), ('dehaze', 'dehaze'), ('delete', 'delete'), ('delete_forever', 'delete_forever'), ('delete_sweep', 'delete_sweep'), ('description', 'description'), ('desktop_mac', 'desktop_mac'), ('desktop_windows', 'desktop_windows'), ('details', 'details'), ('developer_board', 'developer_board'), ('developer_mode', 'developer_mode'), ('device_hub', 'device_hub'), ('devices', 'devices'), ('devices_other', 'devices_other'), ('dialer_sip', 'dialer_sip'), ('dialpad', 'dialpad'), ('directions', 'directions'), ('directions_bike', 'directions_bike'), ('directions_boat', 'directions_boat'), ('directions_bus', 'directions_bus'), ('directions_car', 'directions_car'), ('directions_railway', 'directions_railway'), ('directions_run', 'directions_run'), ('directions_subway', 'directions_subway'), ('directions_transit', 'directions_transit'), ('directions_walk', 'directions_walk'), ('disc_full', 'disc_full'), ('dns', 'dns'), ('do_not_disturb', 'do_not_disturb'), ('do_not_disturb_alt', 'do_not_disturb_alt'), ('do_not_disturb_off', 'do_not_disturb_off'), ('do_not_disturb_on', 'do_not_disturb_on'), ('dock', 'dock'), ('domain', 'domain'), ('done', 'done'), ('done_all', 'done_all'), ('donut_large', 'donut_large'), ('donut_small', 'donut_small'), ('drafts', 'drafts'), ('drag_handle', 'drag_handle'), ('drive_eta', 'drive_eta'), ('dvr', 'dvr'), ('edit', 'edit'), ('edit_location', 'edit_location'), ('eject', 'eject'), ('email', 'email'), ('enhanced_encryption', 'enhanced_encryption'), ('equalizer', 'equalizer'), ('error', 'error'), ('error_outline', 'error_outline'), ('euro_symbol', 'euro_symbol'), ('ev_station', 'ev_station'), ('event', 'event'), ('event_available', 'event_available'), ('event_busy', 'event_busy'), ('event_note', 'event_note'), ('event_seat', 'event_seat'), ('exit_to_app', 'exit_to_app'), ('expand_less', 'expand_less'), ('expand_more', 'expand_more'), ('explicit', 'explicit'), ('explore', 'explore'), ('exposure', 'exposure'), ('exposure_neg_1', 'exposure_neg_1'), ('exposure_neg_2', 'exposure_neg_2'), ('exposure_plus_1', 'exposure_plus_1'), ('exposure_plus_2', 'exposure_plus_2'), ('exposure_zero', 'exposure_zero'), ('extension', 'extension'), ('face', 'face'), ('fast_forward', 'fast_forward'), ('fast_rewind', 'fast_rewind'), ('favorite', 'favorite'), ('favorite_border', 'favorite_border'), ('featured_play_list', 'featured_play_list'), ('featured_video', 'featured_video'), ('feedback', 'feedback'), ('fiber_dvr', 'fiber_dvr'), ('fiber_manual_record', 'fiber_manual_record'), ('fiber_new', 'fiber_new'), ('fiber_pin', 'fiber_pin'), ('fiber_smart_record', 'fiber_smart_record'), ('file_download', 'file_download'), ('file_upload', 'file_upload'), ('filter', 'filter'), ('filter_1', 'filter_1'), ('filter_2', 'filter_2'), ('filter_3', 'filter_3'), ('filter_4', 'filter_4'), ('filter_5', 'filter_5'), ('filter_6', 'filter_6'), ('filter_7', 'filter_7'), ('filter_8', 'filter_8'), ('filter_9', 'filter_9'), ('filter_9_plus', 'filter_9_plus'), ('filter_b_and_w', 'filter_b_and_w'), ('filter_center_focus', 'filter_center_focus'), ('filter_drama', 'filter_drama'), ('filter_frames', 'filter_frames'), ('filter_hdr', 'filter_hdr'), ('filter_list', 'filter_list'), ('filter_none', 'filter_none'), ('filter_tilt_shift', 'filter_tilt_shift'), ('filter_vintage', 'filter_vintage'), ('find_in_page', 'find_in_page'), ('find_replace', 'find_replace'), ('fingerprint', 'fingerprint'), ('first_page', 'first_page'), ('fitness_center', 'fitness_center'), ('flag', 'flag'), ('flare', 'flare'), ('flash_auto', 'flash_auto'), ('flash_off', 'flash_off'), ('flash_on', 'flash_on'), ('flight', 'flight'), ('flight_land', 'flight_land'), ('flight_takeoff', 'flight_takeoff'), ('flip', 'flip'), ('flip_to_back', 'flip_to_back'), ('flip_to_front', 'flip_to_front'), ('folder', 'folder'), ('folder_open', 'folder_open'), ('folder_shared', 'folder_shared'), ('folder_special', 'folder_special'), ('font_download', 'font_download'), ('format_align_center', 'format_align_center'), ('format_align_justify', 'format_align_justify'), ('format_align_left', 'format_align_left'), ('format_align_right', 'format_align_right'), ('format_bold', 'format_bold'), ('format_clear', 'format_clear'), ('format_color_fill', 'format_color_fill'), ('format_color_reset', 'format_color_reset'), ('format_color_text', 'format_color_text'), ('format_indent_decrease', 'format_indent_decrease'), ('format_indent_increase', 'format_indent_increase'), ('format_italic', 'format_italic'), ('format_line_spacing', 'format_line_spacing'), ('format_list_bulleted', 'format_list_bulleted'), ('format_list_numbered', 'format_list_numbered'), ('format_paint', 'format_paint'), ('format_quote', 'format_quote'), ('format_shapes', 'format_shapes'), ('format_size', 'format_size'), ('format_strikethrough', 'format_strikethrough'), ('format_textdirection_l_to_r', 'format_textdirection_l_to_r'), ('format_textdirection_r_to_l', 'format_textdirection_r_to_l'), ('format_underlined', 'format_underlined'), ('forum', 'forum'), ('forward', 'forward'), ('forward_10', 'forward_10'), ('forward_30', 'forward_30'), ('forward_5', 'forward_5'), ('free_breakfast', 'free_breakfast'), ('fullscreen', 'fullscreen'), ('fullscreen_exit', 'fullscreen_exit'), ('functions', 'functions'), ('g_translate', 'g_translate'), ('gamepad', 'gamepad'), ('games', 'games'), ('gavel', 'gavel'), ('gesture', 'gesture'), ('get_app', 'get_app'), ('gif', 'gif'), ('golf_course', 'golf_course'), ('gps_fixed', 'gps_fixed'), ('gps_not_fixed', 'gps_not_fixed'), ('gps_off', 'gps_off'), ('grade', 'grade'), ('gradient', 'gradient'), ('grain', 'grain'), ('graphic_eq', 'graphic_eq'), ('grid_off', 'grid_off'), ('grid_on', 'grid_on'), ('group', 'group'), ('group_add', 'group_add'), ('group_work', 'group_work'), ('hd', 'hd'), ('hdr_off', 'hdr_off'), ('hdr_on', 'hdr_on'), ('hdr_strong', 'hdr_strong'), ('hdr_weak', 'hdr_weak'), ('headset', 'headset'), ('headset_mic', 'headset_mic'), ('healing', 'healing'), ('hearing', 'hearing'), ('help', 'help'), ('help_outline', 'help_outline'), ('high_quality', 'high_quality'), ('highlight', 'highlight'), ('highlight_off', 'highlight_off'), ('history', 'history'), ('home', 'home'), ('hot_tub', 'hot_tub'), ('hotel', 'hotel'), ('hourglass_empty', 'hourglass_empty'), ('hourglass_full', 'hourglass_full'), ('http', 'http'), ('https', 'https'), ('image', 'image'), ('image_aspect_ratio', 'image_aspect_ratio'), ('import_contacts', 'import_contacts'), ('import_export', 'import_export'), ('important_devices', 'important_devices'), ('inbox', 'inbox'), ('indeterminate_check_box', 'indeterminate_check_box'), ('info', 'info'), ('info_outline', 'info_outline'), ('input', 'input'), ('insert_chart', 'insert_chart'), ('insert_comment', 'insert_comment'), ('insert_drive_file', 'insert_drive_file'), ('insert_emoticon', 'insert_emoticon'), ('insert_invitation', 'insert_invitation'), ('insert_link', 'insert_link'), ('insert_photo', 'insert_photo'), ('invert_colors', 'invert_colors'), ('invert_colors_off', 'invert_colors_off'), ('iso', 'iso'), ('keyboard', 'keyboard'), ('keyboard_arrow_down', 'keyboard_arrow_down'), ('keyboard_arrow_left', 'keyboard_arrow_left'), ('keyboard_arrow_right', 'keyboard_arrow_right'), ('keyboard_arrow_up', 'keyboard_arrow_up'), ('keyboard_backspace', 'keyboard_backspace'), ('keyboard_capslock', 'keyboard_capslock'), ('keyboard_hide', 'keyboard_hide'), ('keyboard_return', 'keyboard_return'), ('keyboard_tab', 'keyboard_tab'), ('keyboard_voice', 'keyboard_voice'), ('kitchen', 'kitchen'), ('label', 'label'), ('label_outline', 'label_outline'), ('landscape', 'landscape'), ('language', 'language'), ('laptop', 'laptop'), ('laptop_chromebook', 'laptop_chromebook'), ('laptop_mac', 'laptop_mac'), ('laptop_windows', 'laptop_windows'), ('last_page', 'last_page'), ('launch', 'launch'), ('layers', 'layers'), ('layers_clear', 'layers_clear'), ('leak_add', 'leak_add'), ('leak_remove', 'leak_remove'), ('lens', 'lens'), ('library_add', 'library_add'), ('library_books', 'library_books'), ('library_music', 'library_music'), ('lightbulb_outline', 'lightbulb_outline'), ('line_style', 'line_style'), ('line_weight', 'line_weight'), ('linear_scale', 'linear_scale'), ('link', 'link'), ('linked_camera', 'linked_camera'), ('list', 'list'), ('live_help', 'live_help'), ('live_tv', 'live_tv'), ('local_activity', 'local_activity'), ('local_airport', 'local_airport'), ('local_atm', 'local_atm'), ('local_bar', 'local_bar'), ('local_cafe', 'local_cafe'), ('local_car_wash', 'local_car_wash'), ('local_convenience_store', 'local_convenience_store'), ('local_dining', 'local_dining'), ('local_drink', 'local_drink'), ('local_florist', 'local_florist'), ('local_gas_station', 'local_gas_station'), ('local_grocery_store', 'local_grocery_store'), ('local_hospital', 'local_hospital'), ('local_hotel', 'local_hotel'), ('local_laundry_service', 'local_laundry_service'), ('local_library', 'local_library'), ('local_mall', 'local_mall'), ('local_movies', 'local_movies'), ('local_offer', 'local_offer'), ('local_parking', 'local_parking'), ('local_pharmacy', 'local_pharmacy'), ('local_phone', 'local_phone'), ('local_pizza', 'local_pizza'), ('local_play', 'local_play'), ('local_post_office', 'local_post_office'), ('local_printshop', 'local_printshop'), ('local_see', 'local_see'), ('local_shipping', 'local_shipping'), ('local_taxi', 'local_taxi'), ('location_city', 'location_city'), ('location_disabled', 'location_disabled'), ('location_off', 'location_off'), ('location_on', 'location_on'), ('location_searching', 'location_searching'), ('lock', 'lock'), ('lock_open', 'lock_open'), ('lock_outline', 'lock_outline'), ('looks', 'looks'), ('looks_3', 'looks_3'), ('looks_4', 'looks_4'), ('looks_5', 'looks_5'), ('looks_6', 'looks_6'), ('looks_one', 'looks_one'), ('looks_two', 'looks_two'), ('loop', 'loop'), ('loupe', 'loupe'), ('low_priority', 'low_priority'), ('loyalty', 'loyalty'), ('mail', 'mail'), ('mail_outline', 'mail_outline'), ('map', 'map'), ('markunread', 'markunread'), ('markunread_mailbox', 'markunread_mailbox'), ('memory', 'memory'), ('menu', 'menu'), ('merge_type', 'merge_type'), ('message', 'message'), ('mic', 'mic'), ('mic_none', 'mic_none'), ('mic_off', 'mic_off'), ('mms', 'mms'), ('mode_comment', 'mode_comment'), ('mode_edit', 'mode_edit'), ('monetization_on', 'monetization_on'), ('money_off', 'money_off'), ('monochrome_photos', 'monochrome_photos'), ('mood', 'mood'), ('mood_bad', 'mood_bad'), ('more', 'more'), ('more_horiz', 'more_horiz'), ('more_vert', 'more_vert'), ('motorcycle', 'motorcycle'), ('mouse', 'mouse'), ('move_to_inbox', 'move_to_inbox'), ('movie', 'movie'), ('movie_creation', 'movie_creation'), ('movie_filter', 'movie_filter'), ('multiline_chart', 'multiline_chart'), ('music_note', 'music_note'), ('music_video', 'music_video'), ('my_location', 'my_location'), ('nature', 'nature'), ('nature_people', 'nature_people'), ('navigate_before', 'navigate_before'), ('navigate_next', 'navigate_next'), ('navigation', 'navigation'), ('near_me', 'near_me'), ('network_cell', 'network_cell'), ('network_check', 'network_check'), ('network_locked', 'network_locked'), ('network_wifi', 'network_wifi'), ('new_releases', 'new_releases'), ('next_week', 'next_week'), ('nfc', 'nfc'), ('no_encryption', 'no_encryption'), ('no_sim', 'no_sim'), ('not_interested', 'not_interested'), ('note', 'note'), ('note_add', 'note_add'), ('notifications', 'notifications'), ('notifications_active', 'notifications_active'), ('notifications_none', 'notifications_none'), ('notifications_off', 'notifications_off'), ('notifications_paused', 'notifications_paused'), ('offline_pin', 'offline_pin'), ('ondemand_video', 'ondemand_video'), ('opacity', 'opacity'), ('open_in_browser', 'open_in_browser'), ('open_in_new', 'open_in_new'), ('open_with', 'open_with'), ('pages', 'pages'), ('pageview', 'pageview'), ('palette', 'palette'), ('pan_tool', 'pan_tool'), ('panorama', 'panorama'), ('panorama_fish_eye', 'panorama_fish_eye'), ('panorama_horizontal', 'panorama_horizontal'), ('panorama_vertical', 'panorama_vertical'), ('panorama_wide_angle', 'panorama_wide_angle'), ('party_mode', 'party_mode'), ('pause', 'pause'), ('pause_circle_filled', 'pause_circle_filled'), ('pause_circle_outline', 'pause_circle_outline'), ('payment', 'payment'), ('people', 'people'), ('people_outline', 'people_outline'), ('perm_camera_mic', 'perm_camera_mic'), ('perm_contact_calendar', 'perm_contact_calendar'), ('perm_data_setting', 'perm_data_setting'), ('perm_device_information', 'perm_device_information'), ('perm_identity', 'perm_identity'), ('perm_media', 'perm_media'), ('perm_phone_msg', 'perm_phone_msg'), ('perm_scan_wifi', 'perm_scan_wifi'), ('person', 'person'), ('person_add', 'person_add'), ('person_outline', 'person_outline'), ('person_pin', 'person_pin'), ('person_pin_circle', 'person_pin_circle'), ('personal_video', 'personal_video'), ('pets', 'pets'), ('phone', 'phone'), ('phone_android', 'phone_android'), ('phone_bluetooth_speaker', 'phone_bluetooth_speaker'), ('phone_forwarded', 'phone_forwarded'), ('phone_in_talk', 'phone_in_talk'), ('phone_iphone', 'phone_iphone'), ('phone_locked', 'phone_locked'), ('phone_missed', 'phone_missed'), ('phone_paused', 'phone_paused'), ('phonelink', 'phonelink'), ('phonelink_erase', 'phonelink_erase'), ('phonelink_lock', 'phonelink_lock'), ('phonelink_off', 'phonelink_off'), ('phonelink_ring', 'phonelink_ring'), ('phonelink_setup', 'phonelink_setup'), ('photo', 'photo'), ('photo_album', 'photo_album'), ('photo_camera', 'photo_camera'), ('photo_filter', 'photo_filter'), ('photo_library', 'photo_library'), ('photo_size_select_actual', 'photo_size_select_actual'), ('photo_size_select_large', 'photo_size_select_large'), ('photo_size_select_small', 'photo_size_select_small'), ('picture_as_pdf', 'picture_as_pdf'), ('picture_in_picture', 'picture_in_picture'), ('picture_in_picture_alt', 'picture_in_picture_alt'), ('pie_chart', 'pie_chart'), ('pie_chart_outlined', 'pie_chart_outlined'), ('pin_drop', 'pin_drop'), ('place', 'place'), ('play_arrow', 'play_arrow'), ('play_circle_filled', 'play_circle_filled'), ('play_circle_outline', 'play_circle_outline'), ('play_for_work', 'play_for_work'), ('playlist_add', 'playlist_add'), ('playlist_add_check', 'playlist_add_check'), ('playlist_play', 'playlist_play'), ('plus_one', 'plus_one'), ('poll', 'poll'), ('polymer', 'polymer'), ('pool', 'pool'), ('portable_wifi_off', 'portable_wifi_off'), ('portrait', 'portrait'), ('power', 'power'), ('power_input', 'power_input'), ('power_settings_new', 'power_settings_new'), ('pregnant_woman', 'pregnant_woman'), ('present_to_all', 'present_to_all'), ('print', 'print'), ('priority_high', 'priority_high'), ('public', 'public'), ('publish', 'publish'), ('query_builder', 'query_builder'), ('question_answer', 'question_answer'), ('queue', 'queue'), ('queue_music', 'queue_music'), ('queue_play_next', 'queue_play_next'), ('radio', 'radio'), ('radio_button_checked', 'radio_button_checked'), ('radio_button_unchecked', 'radio_button_unchecked'), ('rate_review', 'rate_review'), ('receipt', 'receipt'), ('recent_actors', 'recent_actors'), ('record_voice_over', 'record_voice_over'), ('redeem', 'redeem'), ('redo', 'redo'), ('refresh', 'refresh'), ('remove', 'remove'), ('remove_circle', 'remove_circle'), ('remove_circle_outline', 'remove_circle_outline'), ('remove_from_queue', 'remove_from_queue'), ('remove_red_eye', 'remove_red_eye'), ('remove_shopping_cart', 'remove_shopping_cart'), ('reorder', 'reorder'), ('repeat', 'repeat'), ('repeat_one', 'repeat_one'), ('replay', 'replay'), ('replay_10', 'replay_10'), ('replay_30', 'replay_30'), ('replay_5', 'replay_5'), ('reply', 'reply'), ('reply_all', 'reply_all'), ('report', 'report'), ('report_problem', 'report_problem'), ('restaurant', 'restaurant'), ('restaurant_menu', 'restaurant_menu'), ('restore', 'restore'), ('restore_page', 'restore_page'), ('ring_volume', 'ring_volume'), ('room', 'room'), ('room_service', 'room_service'), ('rotate_90_degrees_ccw', 'rotate_90_degrees_ccw'), ('rotate_left', 'rotate_left'), ('rotate_right', 'rotate_right'), ('rounded_corner', 'rounded_corner'), ('router', 'router'), ('rowing', 'rowing'), ('rss_feed', 'rss_feed'), ('rv_hookup', 'rv_hookup'), ('satellite', 'satellite'), ('save', 'save'), ('scanner', 'scanner'), ('schedule', 'schedule'), ('school', 'school'), ('screen_lock_landscape', 'screen_lock_landscape'), ('screen_lock_portrait', 'screen_lock_portrait'), ('screen_lock_rotation', 'screen_lock_rotation'), ('screen_rotation', 'screen_rotation'), ('screen_share', 'screen_share'), ('sd_card', 'sd_card'), ('sd_storage', 'sd_storage'), ('search', 'search'), ('security', 'security'), ('select_all', 'select_all'), ('send', 'send'), ('sentiment_dissatisfied', 'sentiment_dissatisfied'), ('sentiment_neutral', 'sentiment_neutral'), ('sentiment_satisfied', 'sentiment_satisfied'), ('sentiment_very_dissatisfied', 'sentiment_very_dissatisfied'), ('sentiment_very_satisfied', 'sentiment_very_satisfied'), ('settings', 'settings'), ('settings_applications', 'settings_applications'), ('settings_backup_restore', 'settings_backup_restore'), ('settings_bluetooth', 'settings_bluetooth'), ('settings_brightness', 'settings_brightness'), ('settings_cell', 'settings_cell'), ('settings_ethernet', 'settings_ethernet'), ('settings_input_antenna', 'settings_input_antenna'), ('settings_input_component', 'settings_input_component'), ('settings_input_composite', 'settings_input_composite'), ('settings_input_hdmi', 'settings_input_hdmi'), ('settings_input_svideo', 'settings_input_svideo'), ('settings_overscan', 'settings_overscan'), ('settings_phone', 'settings_phone'), ('settings_power', 'settings_power'), ('settings_remote', 'settings_remote'), ('settings_system_daydream', 'settings_system_daydream'), ('settings_voice', 'settings_voice'), ('share', 'share'), ('shop', 'shop'), ('shop_two', 'shop_two'), ('shopping_basket', 'shopping_basket'), ('shopping_cart', 'shopping_cart'), ('short_text', 'short_text'), ('show_chart', 'show_chart'), ('shuffle', 'shuffle'), ('signal_cellular_4_bar', 'signal_cellular_4_bar'), ('signal_cellular_connected_no_internet_4_bar', 'signal_cellular_connected_no_internet_4_bar'), ('signal_cellular_no_sim', 'signal_cellular_no_sim'), ('signal_cellular_null', 'signal_cellular_null'), ('signal_cellular_off', 'signal_cellular_off'), ('signal_wifi_4_bar', 'signal_wifi_4_bar'), ('signal_wifi_4_bar_lock', 'signal_wifi_4_bar_lock'), ('signal_wifi_off', 'signal_wifi_off'), ('sim_card', 'sim_card'), ('sim_card_alert', 'sim_card_alert'), ('skip_next', 'skip_next'), ('skip_previous', 'skip_previous'), ('slideshow', 'slideshow'), ('slow_motion_video', 'slow_motion_video'), ('smartphone', 'smartphone'), ('smoke_free', 'smoke_free'), ('smoking_rooms', 'smoking_rooms'), ('sms', 'sms'), ('sms_failed', 'sms_failed'), ('snooze', 'snooze'), ('sort', 'sort'), ('sort_by_alpha', 'sort_by_alpha'), ('spa', 'spa'), ('space_bar', 'space_bar'), ('speaker', 'speaker'), ('speaker_group', 'speaker_group'), ('speaker_notes', 'speaker_notes'), ('speaker_notes_off', 'speaker_notes_off'), ('speaker_phone', 'speaker_phone'), ('spellcheck', 'spellcheck'), ('star', 'star'), ('star_border', 'star_border'), ('star_half', 'star_half'), ('stars', 'stars'), ('stay_current_landscape', 'stay_current_landscape'), ('stay_current_portrait', 'stay_current_portrait'), ('stay_primary_landscape', 'stay_primary_landscape'), ('stay_primary_portrait', 'stay_primary_portrait'), ('stop', 'stop'), ('stop_screen_share', 'stop_screen_share'), ('storage', 'storage'), ('store', 'store'), ('store_mall_directory', 'store_mall_directory'), ('straighten', 'straighten'), ('streetview', 'streetview'), ('strikethrough_s', 'strikethrough_s'), ('style', 'style'), ('subdirectory_arrow_left', 'subdirectory_arrow_left'), ('subdirectory_arrow_right', 'subdirectory_arrow_right'), ('subject', 'subject'), ('subscriptions', 'subscriptions'), ('subtitles', 'subtitles'), ('subway', 'subway'), ('supervisor_account', 'supervisor_account'), ('surround_sound', 'surround_sound'), ('swap_calls', 'swap_calls'), ('swap_horiz', 'swap_horiz'), ('swap_vert', 'swap_vert'), ('swap_vertical_circle', 'swap_vertical_circle'), ('switch_camera', 'switch_camera'), ('switch_video', 'switch_video'), ('sync', 'sync'), ('sync_disabled', 'sync_disabled'), ('sync_problem', 'sync_problem'), ('system_update', 'system_update'), ('system_update_alt', 'system_update_alt'), ('tab', 'tab'), ('tab_unselected', 'tab_unselected'), ('tablet', 'tablet'), ('tablet_android', 'tablet_android'), ('tablet_mac', 'tablet_mac'), ('tag_faces', 'tag_faces'), ('tap_and_play', 'tap_and_play'), ('terrain', 'terrain'), ('text_fields', 'text_fields'), ('text_format', 'text_format'), ('textsms', 'textsms'), ('texture', 'texture'), ('theaters', 'theaters'), ('thumb_down', 'thumb_down'), ('thumb_up', 'thumb_up'), ('thumbs_up_down', 'thumbs_up_down'), ('time_to_leave', 'time_to_leave'), ('timelapse', 'timelapse'), ('timeline', 'timeline'), ('timer', 'timer'), ('timer_10', 'timer_10'), ('timer_3', 'timer_3'), ('timer_off', 'timer_off'), ('title', 'title'), ('toc', 'toc'), ('today', 'today'), ('toll', 'toll'), ('tonality', 'tonality'), ('touch_app', 'touch_app'), ('toys', 'toys'), ('track_changes', 'track_changes'), ('traffic', 'traffic'), ('train', 'train'), ('tram', 'tram'), ('transfer_within_a_station', 'transfer_within_a_station'), ('transform', 'transform'), ('translate', 'translate'), ('trending_down', 'trending_down'), ('trending_flat', 'trending_flat'), ('trending_up', 'trending_up'), ('tune', 'tune'), ('turned_in', 'turned_in'), ('turned_in_not', 'turned_in_not'), ('tv', 'tv'), ('unarchive', 'unarchive'), ('undo', 'undo'), ('unfold_less', 'unfold_less'), ('unfold_more', 'unfold_more'), ('update', 'update'), ('usb', 'usb'), ('verified_user', 'verified_user'), ('vertical_align_bottom', 'vertical_align_bottom'), ('vertical_align_center', 'vertical_align_center'), ('vertical_align_top', 'vertical_align_top'), ('vibration', 'vibration'), ('video_call', 'video_call'), ('video_label', 'video_label'), ('video_library', 'video_library'), ('videocam', 'videocam'), ('videocam_off', 'videocam_off'), ('videogame_asset', 'videogame_asset'), ('view_agenda', 'view_agenda'), ('view_array', 'view_array'), ('view_carousel', 'view_carousel'), ('view_column', 'view_column'), ('view_comfy', 'view_comfy'), ('view_compact', 'view_compact'), ('view_day', 'view_day'), ('view_headline', 'view_headline'), ('view_list', 'view_list'), ('view_module', 'view_module'), ('view_quilt', 'view_quilt'), ('view_stream', 'view_stream'), ('view_week', 'view_week'), ('vignette', 'vignette'), ('visibility', 'visibility'), ('visibility_off', 'visibility_off'), ('voice_chat', 'voice_chat'), ('voicemail', 'voicemail'), ('volume_down', 'volume_down'), ('volume_mute', 'volume_mute'), ('volume_off', 'volume_off'), ('volume_up', 'volume_up'), ('vpn_key', 'vpn_key'), ('vpn_lock', 'vpn_lock'), ('wallpaper', 'wallpaper'), ('warning', 'warning'), ('watch', 'watch'), ('watch_later', 'watch_later'), ('wb_auto', 'wb_auto'), ('wb_cloudy', 'wb_cloudy'), ('wb_incandescent', 'wb_incandescent'), ('wb_iridescent', 'wb_iridescent'), ('wb_sunny', 'wb_sunny'), ('wc', 'wc'), ('web', 'web'), ('web_asset', 'web_asset'), ('weekend', 'weekend'), ('whatshot', 'whatshot'), ('widgets', 'widgets'), ('wifi', 'wifi'), ('wifi_lock', 'wifi_lock'), ('wifi_tethering', 'wifi_tethering'), ('work', 'work'), ('wrap_text', 'wrap_text'), ('youtube_searched_for', 'youtube_searched_for'), ('zoom_in', 'zoom_in'), ('zoom_out', 'zoom_out'), ('zoom_out_map', 'zoom_out_map')], max_length=50, verbose_name='Icon')), @@ -302,7 +302,7 @@ class Migration(migrations.Migration): name='AnnouncementRecipient', 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)), + ('extended_data', models.JSONField(default=dict, editable=False)), ('recipient_id', models.PositiveIntegerField()), ('announcement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recipients', to='core.Announcement')), ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), @@ -320,7 +320,7 @@ class Migration(migrations.Migration): name='Activity', 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)), + ('extended_data', models.JSONField(default=dict, editable=False)), ('title', models.CharField(max_length=150, verbose_name='Title')), ('description', models.TextField(max_length=500, verbose_name='Description')), ('app', models.CharField(max_length=100, verbose_name='Application')), diff --git a/aleksis/core/migrations/0002_school_term.py b/aleksis/core/migrations/0002_school_term.py index 456f78ebf7e16f32b92b954d36ad39f5d323e866..fba542ed9c4a7be9b60e94d28414ade4189a6d6b 100644 --- a/aleksis/core/migrations/0002_school_term.py +++ b/aleksis/core/migrations/0002_school_term.py @@ -17,7 +17,7 @@ class Migration(migrations.Migration): 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)), + ('extended_data', models.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')), diff --git a/aleksis/core/migrations/0011_globalpermissions_options.py b/aleksis/core/migrations/0011_globalpermissions_options.py new file mode 100644 index 0000000000000000000000000000000000000000..a338d910a07843804f8c7e8833e045191d743356 --- /dev/null +++ b/aleksis/core/migrations/0011_globalpermissions_options.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.7 on 2021-04-08 19:15 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_external_link_widget'), + ] + + operations = [ + migrations.AlterModelOptions( + name='globalpermissions', + options={'default_permissions': (), 'managed': False, 'permissions': (('view_system_status', 'Can view system status'), ('link_persons_accounts', 'Can link persons to accounts'), ('manage_data', 'Can manage data'), ('impersonate', 'Can impersonate'), ('search', 'Can use search'), ('change_site_preferences', 'Can change site preferences'), ('change_person_preferences', 'Can change person preferences'), ('change_group_preferences', 'Can change group preferences'))}, + ), + ] diff --git a/aleksis/core/migrations/0012_valid_from_announcement.py b/aleksis/core/migrations/0012_valid_from_announcement.py new file mode 100644 index 0000000000000000000000000000000000000000..abfc8cdeb0b9dece8f23389c3d3158d63bca13c2 --- /dev/null +++ b/aleksis/core/migrations/0012_valid_from_announcement.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.7 on 2021-04-08 19:15 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0011_globalpermissions_options'), + ] + + operations = [ + migrations.AlterField( + model_name='announcement', + name='valid_from', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Date and time from when to show'), + ), + ] diff --git a/aleksis/core/migrations/0011_oauth_permissions.py b/aleksis/core/migrations/0013_oauth_permissions.py similarity index 95% rename from aleksis/core/migrations/0011_oauth_permissions.py rename to aleksis/core/migrations/0013_oauth_permissions.py index 207776bc679436c6e5d64c6646343acd88234541..67193ebcd6aba64b675226c07de2c980be733fb2 100644 --- a/aleksis/core/migrations/0011_oauth_permissions.py +++ b/aleksis/core/migrations/0013_oauth_permissions.py @@ -7,7 +7,7 @@ import django.utils.timezone class Migration(migrations.Migration): dependencies = [ - ('core', '0010_external_link_widget'), + ('core', '0012_valid_from_announcement'), ] operations = [ diff --git a/aleksis/core/migrations/0013_pdf_file.py b/aleksis/core/migrations/0013_pdf_file.py new file mode 100644 index 0000000000000000000000000000000000000000..63b8a95607ce306f8d14d8b4266e184e12a26838 --- /dev/null +++ b/aleksis/core/migrations/0013_pdf_file.py @@ -0,0 +1,46 @@ +# Generated by Django 3.2 on 2021-04-10 18:58 + +import aleksis.core.models +import django.contrib.sites.managers +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('sites', '0002_alter_domain_unique'), + ('core', '0012_valid_from_announcement'), + ] + + operations = [ + migrations.AlterModelOptions( + name='globalpermissions', + options={'default_permissions': (), 'managed': False, 'permissions': ( + ('view_system_status', 'Can view system status'), ('link_persons_accounts', 'Can link persons to accounts'), + ('manage_data', 'Can manage data'), ('impersonate', 'Can impersonate'), ('search', 'Can use search'), + ('change_site_preferences', 'Can change site preferences'), + ('change_person_preferences', 'Can change person preferences'), + ('change_group_preferences', 'Can change group preferences'), ('test_pdf', 'Can test PDF generation'))}, + ), + migrations.CreateModel( + name='PDFFile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extended_data', models.JSONField(default=dict, editable=False)), + ('expires_at', models.DateTimeField(default=aleksis.core.models.PDFFile._get_default_expiration, verbose_name='File expires at')), + ('html', models.TextField(verbose_name='Rendered HTML')), + ('file', models.FileField(blank=True, null=True, upload_to='pdfs/', verbose_name='Generated PDF file')), + ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pdf_files', to='core.person', verbose_name='Owner')), + ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')), + ], + options={ + 'verbose_name': 'PDF file', + 'verbose_name_plural': 'PDF files', + }, + managers=[ + ('objects', django.contrib.sites.managers.CurrentSiteManager()), + ], + ), + ] diff --git a/aleksis/core/migrations/0014_alter_pdffile_file.py b/aleksis/core/migrations/0014_alter_pdffile_file.py new file mode 100644 index 0000000000000000000000000000000000000000..2668488942f93db37703cccc79b9a43b6403904b --- /dev/null +++ b/aleksis/core/migrations/0014_alter_pdffile_file.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2 on 2021-04-17 18:47 + +import aleksis.core.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0013_pdf_file'), + ] + + operations = [ + migrations.AlterField( + model_name='pdffile', + name='file', + field=models.FileField(blank=True, null=True, upload_to=aleksis.core.models.PDFFile._get_upload_path, verbose_name='Generated PDF file'), + ), + ] diff --git a/aleksis/core/models.py b/aleksis/core/models.py index 78674942228bf23066e606e2a1a94fb59df6d74d..473b4e9447eea70de1ccb3d269d62a895f0d596b 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -1,7 +1,8 @@ # flake8: noqa: DJ01 - -from datetime import date, datetime +import hmac +from datetime import date, datetime, timedelta from typing import Iterable, List, Optional, Sequence, Union +from urllib.parse import urlparse from django.conf import settings from django.contrib.auth import get_user_model @@ -909,6 +910,7 @@ class GlobalPermissions(GlobalPermissionModel): ("view_oauth_applications", _("Can view oauth applications")), ("update_oauth_applications", _("Can update oauth applications")), ("delete_oauth_applications", _("Can delete oauth applications")), + ("test_pdf", _("Can test PDF generation")), ) @@ -972,3 +974,49 @@ class DataCheckResult(ExtensibleModel): ("run_data_checks", _("Can run data checks")), ("solve_data_problem", _("Can solve data check problems")), ) + + +class PDFFile(ExtensibleModel): + """Link to a rendered PDF file.""" + + def _get_default_expiration(): # noqa + return timezone.now() + timedelta(minutes=get_site_preferences()["general__pdf_expiration"]) + + def _get_upload_path(instance, filename): # noqa + return f"pdfs/{instance.secret}.pdf" + + person = models.ForeignKey( + to=Person, on_delete=models.CASCADE, verbose_name=_("Owner"), related_name="pdf_files" + ) + expires_at = models.DateTimeField( + verbose_name=_("File expires at"), default=_get_default_expiration + ) + html = models.TextField(verbose_name=_("Rendered HTML")) + file = models.FileField( + upload_to=_get_upload_path, blank=True, null=True, verbose_name=_("Generated PDF file") + ) + + def __str__(self): + return f"{self.person} ({self.pk})" + + @property + def secret(self) -> str: + """Get secret needed for accessing the HTML page.""" + return hmac.new( + bytes(settings.SECRET_KEY, "utf-8"), + msg=bytes(self.html + str(self.expires_at), "utf-8"), + digestmod="sha256", + ).hexdigest() + + @property + def html_url(self) -> str: + """Get URL for the HTML page.""" + return ( + urlparse(reverse("html_for_pdf_file", args=[self.pk])) + ._replace(query=f"secret={self.secret}") + .geturl() + ) + + class Meta: + verbose_name = _("PDF file") + verbose_name_plural = _("PDF files") diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py index 60574f3a8863a4107f04dae66f0c1361b5a5f3c1..c4cac9ee8a26e80b1bd3834ed5957a22ffd37041 100644 --- a/aleksis/core/preferences.py +++ b/aleksis/core/preferences.py @@ -9,6 +9,7 @@ from dynamic_preferences.types import ( BooleanPreference, ChoicePreference, FilePreference, + IntegerPreference, ModelMultipleChoicePreference, MultipleChoicePreference, StringPreference, @@ -278,3 +279,52 @@ class DashboardEditing(BooleanPreference): default = True required = False verbose_name = _("Allow users to edit their dashboard") + + +@site_preferences_registry.register +class EditableFieldsPerson(MultipleChoicePreference): + """Fields on person model that should be editable by the person.""" + + section = account + name = "editable_fields_person" + default = [] + widget = SelectMultiple + verbose_name = _("Fields on person model which are editable by themselves.") + field_attribute = {"initial": []} + choices = [(field.name, field.name) for field in Person.syncable_fields()] + + +@site_preferences_registry.register +class SendNotificationOnPersonChange(MultipleChoicePreference): + """Fields on the person model that should trigger a notification on change.""" + + section = account + name = "notification_on_person_change" + default = [] + widget = SelectMultiple + verbose_name = _( + "Editable fields on person model which should trigger a notification on change" + ) + field_attribute = {"initial": []} + choices = [(field.name, field.name) for field in Person.syncable_fields()] + + +@site_preferences_registry.register +class PersonChangeNotificationContact(StringPreference): + """Mail recipient address for change notifications.""" + + section = account + name = "person_change_notification_contact" + default = "" + verbose_name = _("Contact for notification if a person changes their data") + + +@site_preferences_registry.register +class PDFFileExpirationDuration(IntegerPreference): + """PDF file expiration duration.""" + + section = general + name = "pdf_expiration" + default = 3 + verbose_name = _("PDF file expiration duration") + help_text = _("in minutes") diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py index c198c10a784dfa4299029f2e4b85ebba215446f7..5fd398a4b0824969db525998365a4bca03187705 100644 --- a/aleksis/core/rules.py +++ b/aleksis/core/rules.py @@ -2,6 +2,7 @@ import rules from .models import AdditionalField, Announcement, Group, GroupType, Person from .util.predicates import ( + contains_site_preference_value, has_any_object, has_global_perm, has_object_perm, @@ -67,7 +68,9 @@ rules.add_perm("core.view_person_groups", view_groups_predicate) # Edit person edit_person_predicate = has_person & ( - has_global_perm("core.change_person") | has_object_perm("core.change_person") + has_global_perm("core.change_person") + | has_object_perm("core.change_person") + | is_current_person & is_site_preference_set("account", "editable_fields_person") ) rules.add_perm("core.edit_person", edit_person_predicate) @@ -357,3 +360,18 @@ rules.add_perm("core.delete_oauth_applications", delete_oauth_applications_predi # Upload and browse files via CKEditor upload_files_ckeditor_predicate = has_person & has_global_perm("core.upload_files_ckeditor") rules.add_perm("core.upload_files_ckeditor", upload_files_ckeditor_predicate) + +test_pdf_generation_predicate = has_person & has_global_perm("core.test_pdf") +rules.add_perm("core.test_pdf", test_pdf_generation_predicate) + +# Generate rules for syncable fields +for field in Person._meta.fields: + perm = ( + has_global_perm("core.edit_person") + | has_object_perm("core.edit_person") + | ( + is_current_person + & contains_site_preference_value("account", "editable_fields_person", field.name) + ) + ) + rules.add_perm(f"core.change_person_field_{field.name}", perm) diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index f5b2cd8d0b35f0006eba3ec3e685271748e08300..302ae8c9c57e19b02d6bc28c7d7cbb8956f66926 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -87,7 +87,6 @@ INSTALLED_APPS = [ "rules.apps.AutodiscoverRulesConfig", "haystack", "polymorphic", - "django_global_request", "dbbackup", "django_celery_beat", "django_celery_results", @@ -156,7 +155,6 @@ MIDDLEWARE = [ "debug_toolbar.middleware.DebugToolbarMiddleware", "django.middleware.locale.LocaleMiddleware", "django.middleware.http.ConditionalGetMiddleware", - "django_global_request.middleware.GlobalRequestMiddleware", "django.contrib.sites.middleware.CurrentSiteMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", @@ -246,7 +244,7 @@ INSTALLED_APPS.append("cachalot") DEBUG_TOOLBAR_PANELS.append("cachalot.panels.CachalotPanel") CACHALOT_TIMEOUT = _settings.get("caching.cachalot.timeout", None) CACHALOT_DATABASES = set(["default"]) -SILENCED_SYSTEM_CHECKS.append("cachalot.W001") +SILENCED_SYSTEM_CHECKS += ["cachalot.W001", "cachalot.E003"] SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_CACHE_ALIAS = "default" @@ -314,7 +312,7 @@ if _settings.get("ldap.uri", None): AUTH_LDAP_BIND_PASSWORD = _settings.get("ldap.bind.password") # Keep local password for users to be required to proveide their old password on change - AUTH_LDAP_SET_USABLE_PASSWORD = True + AUTH_LDAP_SET_USABLE_PASSWORD = _settings.get("ldap.handle_passwords", True) # Keep bound as the authenticating user # Ensures proper read permissions, and ability to change password without admin @@ -527,6 +525,13 @@ DBBACKUP_CONNECTOR_MAPPING = { "django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector", } +if _settings.get("backup.storage.type", "").lower() == "s3": + DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" + + DBBACKUP_STORAGE_OPTIONS = { + key: value for (key, value) in _settings.get("backup.storage.s3").items() + } + IMPERSONATE = {"USE_HTTP_REFERER": True, "REQUIRE_SUPERUSER": True, "ALLOW_SUPERUSER": True} DJANGO_TABLES2_TEMPLATE = "django_tables2/materialize.html" @@ -810,7 +815,7 @@ PROMETHEUS_EXPORT_MIGRATIONS = False SECURE_PROXY_SSL_HEADER = ("REQUEST_SCHEME", "https") -if _settings.get("storage.s3.enabled", False): +if _settings.get("storage.type", "").lower() == "s3": INSTALLED_APPS.append("storages") DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" @@ -822,11 +827,12 @@ if _settings.get("storage.s3.enabled", False): "storage.s3.static.max_age_seconds", 24 * 60 * 60 ) - AWS_REGION = _settings.get("storage.s3.region", "") - AWS_ACCESS_KEY_ID = _settings.get("storage.s3.access_key_id", "") + AWS_REGION = _settings.get("storage.s3.region_name", "") + AWS_ACCESS_KEY_ID = _settings.get("storage.s3.access_key", "") AWS_SECRET_ACCESS_KEY = _settings.get("storage.s3.secret_key", "") AWS_SESSION_TOKEN = _settings.get("storage.s3.session_token", "") AWS_STORAGE_BUCKET_NAME = _settings.get("storage.s3.bucket_name", "") + AWS_LOCATION = _settings.get("storage.s3.location", "") AWS_S3_ADDRESSING_STYLE = _settings.get("storage.s3.addressing_style", "auto") AWS_S3_ENDPOINT_URL = _settings.get("storage.s3.endpoint_url", "") AWS_S3_KEY_PREFIX = _settings.get("storage.s3.key_prefix", "") @@ -846,3 +852,6 @@ else: DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage" SASS_PROCESSOR_STORAGE = DEFAULT_FILE_STORAGE + +# Add django-cleanup after all apps to ensure that it gets all signals as last app +INSTALLED_APPS.append("django_cleanup.apps.CleanupConfig") diff --git a/aleksis/core/static/js/progress.js b/aleksis/core/static/js/progress.js index 4d4c0692f39e78af60bdc621544ba7a2c2b720c4..8b897096f18c221bde8fd34c88bd93f9800ff06d 100644 --- a/aleksis/core/static/js/progress.js +++ b/aleksis/core/static/js/progress.js @@ -47,6 +47,10 @@ function customSuccess(progressBarElement, progressBarMessageElement) { $("#result-icon").text("check_circle"); $("#result-text").text(OPTIONS.success); $("#result-box").show(); + const redirect = "redirect_on_success" in OPTIONS; + if (redirect) { + window.location.replace(OPTIONS.redirect_on_success); + } } function customError(progressBarElement, progressBarMessageElement) { diff --git a/aleksis/core/static/print.css b/aleksis/core/static/print.css index cd3eeeea0accb97cb9e2b48054b8ad033476b41c..cda82eacab1d51a39f7f455170eb6ff12121dc30 100644 --- a/aleksis/core/static/print.css +++ b/aleksis/core/static/print.css @@ -5,6 +5,7 @@ @page { size: A4; padding: 30mm; + margin: 0; } header { diff --git a/aleksis/core/templates/core/base.html b/aleksis/core/templates/core/base.html index d34db4f65d05021f1d68e61190bf323c249e5ada..1553f5511439dc551f4f29056c23568874bf8f07 100644 --- a/aleksis/core/templates/core/base.html +++ b/aleksis/core/templates/core/base.html @@ -47,9 +47,9 @@ <!-- Nav bar (logged in as, logout) --> <nav> - <a class="brand-logo" href="/">{{ request.site.preferences.general__title }}</a> - <div class="nav-wrapper"> + <a class="brand-logo" href="/">{{ request.site.preferences.general__title }}</a> + <ul id="nav-mobile" class="right hide-on-med-and-down"> {% if user.is_authenticated %} <li>{% trans "Logged in as" %} {{ user.get_username }}</li> diff --git a/aleksis/core/templates/core/index.html b/aleksis/core/templates/core/index.html index 70103f517963f3a9e306e43c9212ac856594b229..ca838aa237a97b9d1773e17c984f734cbcdb92f1 100644 --- a/aleksis/core/templates/core/index.html +++ b/aleksis/core/templates/core/index.html @@ -16,9 +16,6 @@ {% trans "Edit dashboard" %} </a> {% endif %} - <h4> - {{ request.site.preferences.general__title }} - </h4> {% for notification in unread_notifications %} <div class="alert primary scale-transition"> diff --git a/aleksis/core/templates/core/pages/progress.html b/aleksis/core/templates/core/pages/progress.html index 6292fb0d243b1b2ecb72ff1cec7c3b6339409c18..799f1f93648ab1de05be3f9432653b8c4e8b1466 100644 --- a/aleksis/core/templates/core/pages/progress.html +++ b/aleksis/core/templates/core/pages/progress.html @@ -46,6 +46,12 @@ <i class="material-icons left">arrow_back</i> {% trans "Go back" %} </a> + {% if additional_button %} + <a class="btn waves-effect waves-light" href="{{ additional_button.href }}"> + <i class="material-icons left">{{ additional_button.icon|default:"" }}</i> + {{ additional_button.caption }} + </a> + {% endif %} </div> </div> </div> diff --git a/aleksis/core/templates/core/pages/test_pdf.html b/aleksis/core/templates/core/pages/test_pdf.html new file mode 100644 index 0000000000000000000000000000000000000000..f0c0a6169c85a6f37252136b3777dc0967350e43 --- /dev/null +++ b/aleksis/core/templates/core/pages/test_pdf.html @@ -0,0 +1,19 @@ +{# -*- engine:django -*- #} + +{% extends "core/base_print.html" %} + +{% load i18n %} + +{% block browser_title %}{% blocktrans %}Test PDF generation{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}Test PDF generation{% endblocktrans %}{% endblock %} + +{% block content %} + <div class="alert primary"> + <p> + <i class="material-icons left">info</i> + {% blocktrans %} + This simple view can be used to ensure the correct function of the built-in PDF generation system. + {% endblocktrans %} + </p> + </div> +{% endblock %} diff --git a/aleksis/core/templates/oauth2_provider/application_form.html b/aleksis/core/templates/oauth2_provider/application_form.html index 51f4b5edcff15113787859c3d0c14b434aa8456c..520fa107449dfef6999a3708f0e1f1bbe0090404 100644 --- a/aleksis/core/templates/oauth2_provider/application_form.html +++ b/aleksis/core/templates/oauth2_provider/application_form.html @@ -13,9 +13,9 @@ <form method="post"> {% csrf_token %} {% form form=form %}{% endform %} + {% include "core/partials/save_button.html" %} <a class="btn waves-effect red waves-light" href="{% block app-form-back-url %}{% url "oauth_detail" application.id %}{% endblock app-form-back-url %}"> <i class="material-icons left">clear</i> {% trans "Cancel"%} </a> - {% include "core/partials/save_button.html" %} </form> {% endblock %} diff --git a/aleksis/core/templates/oauth2_provider/authorize.html b/aleksis/core/templates/oauth2_provider/authorize.html index 36423b7ffb074013c4f85ea94e3fe3c3a1ed42cf..f80659cbb39de3375f50e0bc7d342f086999c290 100644 --- a/aleksis/core/templates/oauth2_provider/authorize.html +++ b/aleksis/core/templates/oauth2_provider/authorize.html @@ -28,12 +28,12 @@ {% csrf_token %} {% form form=form %} {% endform %} - <a class="btn red waves-effect waves-light btn-margin" href="{% block app-form-back-url %}{% url "oauth_detail" application.id %}{% endblock app-form-back-url %}"> - <i class="material-icons left">cancel</i> {% trans "Disallow" %} - </a> <button type="submit" class="btn green waves-effect waves-light btn-margin"> <i class="material-icons left">done_all</i> {% trans "Allow" %} </button> + <a class="btn red waves-effect waves-light btn-margin" href="{% block app-form-back-url %}{% url "oauth_detail" application.id %}{% endblock app-form-back-url %}"> + <i class="material-icons left">cancel</i> {% trans "Disallow" %} + </a> </form> </div> </div> diff --git a/aleksis/core/templates/templated_email/person_changed.email b/aleksis/core/templates/templated_email/person_changed.email new file mode 100644 index 0000000000000000000000000000000000000000..2e1db653256873d3f72afcd34a1e19914d116480 --- /dev/null +++ b/aleksis/core/templates/templated_email/person_changed.email @@ -0,0 +1,32 @@ +{% load i18n %} + +{% block subject %} + {% blocktrans with person=person %}{{ person }} changed their data!{% endblocktrans %} +{% endblock %} + +{% block plain %} + {% trans "Hello," %} + + {% blocktrans with person=person %} + the person {{ person }} recently changed the following fields: + {% endblocktrans %} + + {% for field in send_notification_fields %} + * {{ field }} + {% endfor %} +{% endblock %} + +{% block html %} + <p>{% trans "Hello," %}</p> + <p> + {% blocktrans with person=person %} + the person {{ person }} recently changed the following fields: + {% endblocktrans %} + </p> + + <ul> + {% for field in send_notification_fields %} + <li>{{ field }}</li> + {% endfor %} + </ul> +{% endblock %} diff --git a/aleksis/core/tests/browser/test_selenium.py b/aleksis/core/tests/browser/test_selenium.py index a4861d6c18797f7d3e6ed37fa221a2ea1753b96b..8afd4b1584e3b5e6f35d69b2df73625ad8da5fad 100644 --- a/aleksis/core/tests/browser/test_selenium.py +++ b/aleksis/core/tests/browser/test_selenium.py @@ -1,10 +1,15 @@ import os from django.conf import settings +from django.contrib.auth.models import User +from django.test import override_settings from django.test.selenium import SeleniumTestCase, SeleniumTestCaseBase from django.urls import reverse import pytest +from selenium.webdriver.support.wait import WebDriverWait + +from aleksis.core.models import Person pytestmark = pytest.mark.django_db @@ -15,6 +20,8 @@ SeleniumTestCaseBase.browsers = list( SeleniumTestCaseBase.selenium_hub = os.environ.get("TEST_SELENIUM_HUB", "") or None +@pytest.mark.usefixtures("celery_worker") +@override_settings(CELERY_BROKER_URL="memory://localhost//") class SeleniumTests(SeleniumTestCase): serialized_rollback = True @@ -29,18 +36,11 @@ class SeleniumTests(SeleniumTestCase): else: return False - def test_index(self): - self.selenium.get(self.live_server_url + "/") - assert "AlekSIS" in self.selenium.title - self._screenshot("index.png") - - def test_login_default_superuser(self): - username = "admin" - password = "admin" - + def _login(self, username="admin", password="admin", with_screenshots=False): # Navigate to configured login page self.selenium.get(self.live_server_url + reverse(settings.LOGIN_URL)) - self._screenshot("login_default_superuser_blank.png") + if with_screenshots: + self._screenshot("login_default_superuser_blank.png") # Find login form input fields and enter defined credentials self.selenium.find_element_by_xpath( @@ -49,11 +49,33 @@ class SeleniumTests(SeleniumTestCase): self.selenium.find_element_by_xpath( '//label[contains(text(), "Password")]/../input' ).send_keys(password) - self._screenshot("login_default_superuser_filled.png") + if with_screenshots: + self._screenshot("login_default_superuser_filled.png") # Submit form by clicking django-two-factor-auth's Next button self.selenium.find_element_by_xpath('//button[contains(text(), "Login")]').click() - self._screenshot("login_default_superuser_submitted.png") + if with_screenshots: + self._screenshot("login_default_superuser_submitted.png") + + def _create_person(self, username="admin"): + user = User.objects.get(username=username) + person = Person.objects.create(user=user, first_name="Jane", last_name="Doe") + return person + + def test_index(self): + self.selenium.get(self.live_server_url + "/") + assert "AlekSIS" in self.selenium.title + self._screenshot("index.png") + + def test_login_default_superuser(self): + self._login("admin", "admin", with_screenshots=True) # Should redirect away from login page and not put up an alert about wrong credentials assert "Please enter a correct username and password." not in self.selenium.page_source + + def test_pdf_generation(self): + self._login() + self._create_person() + self.selenium.get(self.live_server_url + reverse("test_pdf")) + el = WebDriverWait(self.selenium, 10).until(lambda d: ".pdf" in self.selenium.current_url) + self._screenshot("pdf.png") diff --git a/aleksis/core/tests/models/test.pdf b/aleksis/core/tests/models/test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4e6c1457f663de82eb95a8613f4626b84146dd45 Binary files /dev/null and b/aleksis/core/tests/models/test.pdf differ diff --git a/aleksis/core/tests/models/test_openid.py b/aleksis/core/tests/models/test_openid.py index a253a91de862e6aedd3c1bf1dd2d9ea065df29b4..eee068a36af45131bab94e7e58424aa9957ca88a 100644 --- a/aleksis/core/tests/models/test_openid.py +++ b/aleksis/core/tests/models/test_openid.py @@ -7,10 +7,10 @@ pytestmark = pytest.mark.django_db def test_application_create(): _application = Application.objects.create( - name = "Test Application", - redirect_uris = "https://example.com/redirect https://example.de/redirect", - client_type = "public", - authorization_grant_type = "authorization-code" + name="Test Application", + redirect_uris="https://example.com/redirect https://example.de/redirect", + client_type="public", + authorization_grant_type="authorization-code", ) assert _application.name == "Test Application" diff --git a/aleksis/core/tests/models/test_pdffile.py b/aleksis/core/tests/models/test_pdffile.py new file mode 100644 index 0000000000000000000000000000000000000000..778ff8b5676a93279f9f9261e9c27ccbe6d85939 --- /dev/null +++ b/aleksis/core/tests/models/test_pdffile.py @@ -0,0 +1,102 @@ +import os +import re +from datetime import datetime, timedelta + +from django.core.files import File +from django.core.files.storage import default_storage +from django.template.loader import render_to_string +from django.test import TransactionTestCase, override_settings +from django.utils import timezone + +import freezegun +import pytest + +from aleksis.core.models import PDFFile, Person +from aleksis.core.util.pdf import clean_up_expired_pdf_files + +pytestmark = pytest.mark.django_db + + +@pytest.mark.usefixtures("celery_worker") +@override_settings(CELERY_BROKER_URL="memory://localhost//") +class PDFFIleTest(TransactionTestCase): + serialized_rollback = True + + _test_pdf = os.path.join(os.path.dirname(os.path.abspath(__file__)), "test.pdf") + + def _get_test_html(self): + return render_to_string("core/pages/test_pdf.html") + + def test_pdf_file(self): + dummy_person = Person.objects.create(first_name="Jane", last_name="Doe") + + html = self._get_test_html() + assert "html" in html + file_object = PDFFile.objects.create(person=dummy_person, html=html) + assert isinstance(file_object.expires_at, datetime) + assert file_object.expires_at > timezone.now() + assert not bool(file_object.file) + + with open(self._test_pdf, "rb") as f: + file_object.file.save("print.pdf", File(f)) + file_object.save() + re_base = r"pdfs/[a-zA-Z0-9]+\.pdf" + assert re.match(re_base, file_object.file.name) + + def test_delete_signal(self): + dummy_person = Person.objects.create(first_name="Jane", last_name="Doe") + file_object = PDFFile.objects.create(person=dummy_person, html=self._get_test_html()) + with open(self._test_pdf, "rb") as f: + file_object.file.save("print.pdf", File(f)) + file_object.save() + + file_path = file_object.file.path + + assert default_storage.exists(file_path) + file_object.delete() + assert not default_storage.exists(file_path) + + def test_delete_expired_files(self): + # Create test instances + dummy_person = Person.objects.create(first_name="Jane", last_name="Doe") + file_object = PDFFile.objects.create(person=dummy_person, html=self._get_test_html()) + file_object2 = PDFFile.objects.create( + person=dummy_person, + html=self._get_test_html(), + expires_at=timezone.now() + timedelta(minutes=10), + ) + with open(self._test_pdf, "rb") as f: + file_object.file.save("print.pdf", File(f)) + file_object2.file.save("print.pdf", File(f)) + file_object.save() + file_object2.save() + + clean_up_expired_pdf_files() + assert PDFFile.objects.get(pk=file_object.pk) + assert PDFFile.objects.get(pk=file_object2.pk) + + # Prepare times + test_time_before = timezone.now() + timedelta(minutes=2.5) + test_time_between = timezone.now() + timedelta(minutes=4) + test_time_after = timezone.now() + timedelta(minutes=15) + + # None of the files are expired + with freezegun.freeze_time(test_time_before): + clean_up_expired_pdf_files() + assert PDFFile.objects.get(pk=file_object.pk) + assert PDFFile.objects.get(pk=file_object2.pk) + + # One file is expired + with freezegun.freeze_time(test_time_between): + clean_up_expired_pdf_files() + with pytest.raises(PDFFile.DoesNotExist): + PDFFile.objects.get(pk=file_object.pk) + assert PDFFile.objects.get(pk=file_object2.pk) + + # Both files are expired + with freezegun.freeze_time(test_time_after): + clean_up_expired_pdf_files() + with pytest.raises(PDFFile.DoesNotExist): + PDFFile.objects.get(pk=file_object.pk) + with pytest.raises(PDFFile.DoesNotExist): + PDFFile.objects.get(pk=file_object2.pk) diff --git a/aleksis/core/tests/util/test_core_helpers.py b/aleksis/core/tests/util/test_core_helpers.py deleted file mode 100644 index d018ddba3b610251aafcf47698f7669c49ef8fef..0000000000000000000000000000000000000000 --- a/aleksis/core/tests/util/test_core_helpers.py +++ /dev/null @@ -1,19 +0,0 @@ -import re - -from aleksis.core.util.core_helpers import path_and_rename - - -def test_path_and_rename(): - re_base = r"[a-z0-9]+" - - example_1 = "sdjaasjkl.jpg" - re_example_1 = "files/" + re_base + r"\.jpg" - re2_example_1 = "images/" + re_base + r"\.jpg" - assert re.match(re_example_1, path_and_rename(None, example_1)) - assert re.match(re2_example_1, path_and_rename(None, example_1, upload_to="images")) - - example_2 = "sdjaasjkl" - re_example_2 = "files/" + re_base - re2_example_2 = "images/" + re_base - assert re.match(re_example_2, path_and_rename(None, example_2)) - assert re.match(re2_example_2, path_and_rename(None, example_2, upload_to="images")) diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index f0b355237ea1ff51d522a4452210227f40e688b5..a1a8e2d62790e64895f2f6e7183862ee2c4c9599 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -178,6 +178,7 @@ urlpatterns = [ name="preferences_group", ), path("health/", include(health_urls)), + path("health/pdf/", views.TestPDFGenerationView.as_view(), name="test_pdf"), path("data_check/", views.DataCheckView.as_view(), name="check_data",), path("data_check/run/", views.RunDataChecks.as_view(), name="data_check_run",), path( @@ -207,6 +208,8 @@ urlpatterns = [ {"default": True}, name="edit_default_dashboard", ), + path("pdfs/<int:pk>/", views.RedirectToPDFFile.as_view(), name="redirect_to_pdf_file"), + path("pdfs/<int:pk>/html/", views.HTMLForPDFFile.as_view(), name="html_for_pdf_file"), ] # Add URLs for optional features diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py index d7539c22616abd2d76013546acb15b592862ceb4..a2a6ced0e87b610dde1d74efe57b0fdf46f969c9 100644 --- a/aleksis/core/util/apps.py +++ b/aleksis/core/util/apps.py @@ -186,8 +186,6 @@ class AppConfig(django.apps.AppConfig): verbosity: int, interactive: bool, using: str, - plan: List[Tuple], - apps: django.apps.registry.Apps, **kwargs, ) -> None: """Call on every app instance after its models have been migrated. diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py index 676894d28d62f32ee9c08de874edf6857f94a629..69350b13967b0cf1d57a6fc6307adcbc90725fe6 100644 --- a/aleksis/core/util/core_helpers.py +++ b/aleksis/core/util/core_helpers.py @@ -1,11 +1,9 @@ -import os import sys from datetime import datetime, timedelta from importlib import import_module from itertools import groupby from operator import itemgetter from typing import Any, Callable, Optional, Sequence, Union -from uuid import uuid4 if sys.version_info >= (3, 9): from importlib import metadata @@ -183,20 +181,6 @@ def has_person(obj: Union[HttpRequest, Model]) -> bool: return True -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) - - # set filename as random string - new_filename = f"{uuid4().hex}{ext}" - - # Create upload directory if necessary - os.makedirs(os.path.join(settings.MEDIA_ROOT, upload_to), exist_ok=True) - - # return the whole path to the file - return os.path.join(upload_to, new_filename) - - def custom_information_processor(request: HttpRequest) -> dict: """Provide custom information in all templates.""" from ..models import CustomMenu diff --git a/aleksis/core/util/ldap.py b/aleksis/core/util/ldap.py index 5a17cbbb5aea91030fab467c21897826f74eb6aa..96b058ac0060975cdd08281be94ba4df371aa7db 100644 --- a/aleksis/core/util/ldap.py +++ b/aleksis/core/util/ldap.py @@ -20,13 +20,13 @@ class LDAPBackend(_LDAPBackend): Django database in order to not require it to have global admin permissions on the LDAP directory. """ - user = ldap_user.authenticate(password) - - if not user: - # Fail early and do not try other backends - raise PermissionDenied("LDAP failed to authenticate user") + user = super().authenticate_ldap_user(ldap_user, password) if self.settings.SET_USABLE_PASSWORD: + if not user: + # Fail early and do not try other backends + raise PermissionDenied("LDAP failed to authenticate user") + # Set a usable password so users can change their LDAP password user.set_password(password) user.save() diff --git a/aleksis/core/util/pdf.py b/aleksis/core/util/pdf.py new file mode 100644 index 0000000000000000000000000000000000000000..18640a0e8667d91b7c31b8130b597e7992985e8d --- /dev/null +++ b/aleksis/core/util/pdf.py @@ -0,0 +1,112 @@ +import os +import subprocess # noqa +from tempfile import TemporaryDirectory +from typing import Optional + +from django.core.files import File +from django.http.request import HttpRequest +from django.http.response import HttpResponse +from django.shortcuts import get_object_or_404, render +from django.template.loader import render_to_string +from django.urls import reverse +from django.utils import timezone +from django.utils.translation import get_language +from django.utils.translation import gettext as _ + +from celery_progress.backend import ProgressRecorder + +from aleksis.core.celery import app +from aleksis.core.models import PDFFile +from aleksis.core.util.celery_progress import recorded_task + + +@recorded_task +def generate_pdf( + file_pk: int, html_url: str, recorder: ProgressRecorder, lang: Optional[str] = None +): + """Generate a PDF file by rendering the HTML code using a headless Chromium.""" + file_object = get_object_or_404(PDFFile, pk=file_pk) + + recorder.set_progress(0, 1) + + # Open a temporary directory + with TemporaryDirectory() as temp_dir: + pdf_path = os.path.join(temp_dir, "print.pdf") + lang = lang or get_language() + + # Run PDF generation using a headless Chromium + cmd = [ + "chromium", + "--headless", + "--no-sandbox", + "--run-all-compositor-stages-before-draw", + "--temp-profile", + "--disable-dev-shm-usage", + "--disable-gpu", + "--disable-setuid-sandbox", + "--dbus-stub", + f"--home-dir={temp_dir}", + f"--lang={lang}", + f"--print-to-pdf={pdf_path}", + html_url, + ] + res = subprocess.run(cmd) # noqa + + # Let the task fail on a non-success return code + res.check_returncode() + + # Upload PDF file to media storage + with open(pdf_path, "rb") as f: + file_object.file.save("print.pdf", File(f)) + file_object.save() + + recorder.set_progress(1, 1) + + +def render_pdf(request: HttpRequest, template_name: str, context: dict = None) -> HttpResponse: + """Start PDF generation and show progress page. + + The progress page will redirect to the PDF after completion. + """ + if not context: + context = {} + + html_template = render_to_string(template_name, context) + + file_object = PDFFile.objects.create(person=request.user.person, html=html_template) + html_url = request.build_absolute_uri(file_object.html_url) + + result = generate_pdf.delay(file_object.pk, html_url, lang=get_language()) + + redirect_url = reverse("redirect_to_pdf_file", args=[file_object.pk]) + + progress_context = { + "title": _("Progress: Generate PDF file"), + "back_url": context.get("back_url", "index"), + "progress": { + "task_id": result.task_id, + "title": _("Generating PDF file …"), + "success": _("The PDF file has been generated successfully."), + "error": _("There was a problem while generating the PDF file."), + "redirect_on_success": redirect_url, + }, + "additional_button": { + "href": redirect_url, + "caption": _("Download PDF"), + "icon": "picture_as_pdf", + }, + } + + # Render progress view + return render(request, "core/pages/progress.html", progress_context) + + +def clean_up_expired_pdf_files() -> None: + """Clean up expired PDF files.""" + PDFFile.objects.filter(expires_at__lt=timezone.now()).delete() + + +@app.task +def clean_up_expired_pdf_files_task() -> None: + """Clean up expired PDF files.""" + return clean_up_expired_pdf_files() diff --git a/aleksis/core/util/predicates.py b/aleksis/core/util/predicates.py index c456ee329e3560c65abc9953a689ad23053736f3..c6e6035301be735ac439bd21a229446ad490208c 100644 --- a/aleksis/core/util/predicates.py +++ b/aleksis/core/util/predicates.py @@ -131,3 +131,14 @@ def is_notification_recipient(user: User, obj: Model) -> bool: notification a user wants to mark read is this user. """ return user == obj.recipient.user + + +def contains_site_preference_value(section: str, pref: str, value: str): + """Check if given site preference contains a value.""" + name = f"check_site_preference_value:{section}__{pref}" + + @predicate(name) + def fn() -> bool: + return bool(value in get_site_preferences()[f"{section}__{pref}"]) + + return fn diff --git a/aleksis/core/views.py b/aleksis/core/views.py index 0ef10817f58eae5bb61b52bfda041a7d4e293ab2..25c3ae8ebf2d6e39604d25629564ddcabce742c9 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -6,14 +6,14 @@ from django.core.exceptions import PermissionDenied from django.core.paginator import Paginator from django.db.models import QuerySet from django.forms.models import BaseModelForm, modelform_factory -from django.http import HttpRequest, HttpResponse, HttpResponseNotFound +from django.http import Http404, HttpRequest, HttpResponse, HttpResponseNotFound from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse, reverse_lazy from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ from django.views.decorators.cache import never_cache -from django.views.generic.base import View -from django.views.generic.detail import DetailView +from django.views.generic.base import TemplateView, View +from django.views.generic.detail import DetailView, SingleObjectMixin from django.views.generic.edit import DeleteView, UpdateView from django.views.generic.list import ListView @@ -30,6 +30,7 @@ from oauth2_provider.models import Application from reversion import set_user from reversion.views import RevisionMixin from rules.contrib.views import PermissionRequiredMixin, permission_required +from templated_email import send_templated_mail from aleksis.core.data_checks import DataCheckRegistry, check_data @@ -60,6 +61,7 @@ from .models import ( Group, GroupType, Notification, + PDFFile, Person, SchoolTerm, ) @@ -78,8 +80,20 @@ from .tables import ( ) from .util import messages from .util.apps import AppConfig -from .util.core_helpers import has_person, objectgetter_optional +from .util.core_helpers import get_site_preferences, has_person, objectgetter_optional from .util.forms import PreferenceLayout +from .util.pdf import render_pdf + + +class RenderPDFView(TemplateView): + """View to render a PDF file from a template. + + Makes use of ``render_pdf``. + """ + + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + context = self.get_context_data(**kwargs) + return render_pdf(request, self.template_name, context) @permission_required("core.view_dashboard") @@ -341,16 +355,39 @@ def edit_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse if id_: # Edit form for existing group edit_person_form = EditPersonForm( - request.POST or None, request.FILES or None, instance=person + request, request.POST or None, request.FILES 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, request.FILES or None) + edit_person_form = EditPersonForm(request, request.POST or None, request.FILES or None) else: raise PermissionDenied() if request.method == "POST": if edit_person_form.is_valid(): + if person and person == request.user.person: + # Check if user edited non-editable field + notification_fields = get_site_preferences()[ + "account__notification_on_person_change" + ] + send_notification_fields = set(edit_person_form.changed_data).intersection( + set(notification_fields) + ) + context["send_notification_fields"] = send_notification_fields + if send_notification_fields: + context["send_notification_fields"] = send_notification_fields + send_templated_mail( + template_name="person_changed", + from_email=request.user.person.mail_sender_via, + headers={ + "Reply-To": request.user.person.mail_sender, + "Sender": request.user.person.mail_sender, + }, + recipient_list=[ + get_site_preferences()["account__person_change_notification_contact"] + ], + context=context, + ) with reversion.create_revision(): set_user(request.user) edit_person_form.save(commit=True) @@ -431,6 +468,11 @@ class SystemStatus(PermissionRequiredMixin, MainView): return self.render_to_response(context, status=status_code) +class TestPDFGenerationView(PermissionRequiredMixin, RenderPDFView): + template_name = "core/pages/test_pdf.html" + permission_required = "core.test_pdf" + + @permission_required( "core.mark_notification_as_read", fn=objectgetter_optional(Notification, None, False) ) @@ -1002,3 +1044,27 @@ class OAuth2Update(PermissionRequiredMixin, UpdateView): "redirect_uris", ), ) + + +class RedirectToPDFFile(SingleObjectMixin, View): + """Redirect to a generated PDF file.""" + + model = PDFFile + + def get(self, *args, **kwargs): + file_object = self.get_object() + if not file_object.file: + raise Http404() + return redirect(file_object.file.url) + + +class HTMLForPDFFile(SingleObjectMixin, View): + """Return rendered HTML for generating a PDF file.""" + + model = PDFFile + + def get(self, request, *args, **kwargs): + file_object = self.get_object() + if request.GET.get("secret") != file_object.secret: + raise PermissionDenied() + return HttpResponse(file_object.html) diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..2aa286cdb37550088652b10cced7bfc3d3b56aa9 --- /dev/null +++ b/conftest.py @@ -0,0 +1 @@ +pytest_plugins = ("celery.contrib.pytest", ) diff --git a/docs/dev/01_setup.rst b/docs/dev/01_setup.rst index 31c72ee6c8bcaea746291ab038318fa614f0103d..122a9baa14ce942886bd8d1a73fc5584f77cad19 100644 --- a/docs/dev/01_setup.rst +++ b/docs/dev/01_setup.rst @@ -28,8 +28,7 @@ Install native dependencies Some system libraries are required to install AlekSIS:: - sudo apt install build-essential libpq-dev libpq5 libssl-dev python3-dev python3-pip python3-venv yarnpkg gettext - + sudo apt install build-essential libpq-dev libpq5 libssl-dev python3-dev python3-pip python3-venv yarnpkg gettext chromium Get Poetry ---------- diff --git a/poetry.lock b/poetry.lock index 739da325fa6d81610f79f897974b6751e12293e4..39968896fc96a41ef5702cb84fa58ff3283bc705 100644 --- a/poetry.lock +++ b/poetry.lock @@ -8,11 +8,11 @@ python-versions = "*" [[package]] name = "aleksis-builddeps" -version = "2" +version = "3" description = "AlekSIS (School Information System) — Build/Dev dependencies for apps" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6,<4.0" [package.dependencies] black = ">=19.10b0,<20.0" @@ -27,7 +27,8 @@ flake8-docstrings = ">=1.5.0,<2.0.0" flake8-fixme = ">=1.1.1,<2.0.0" flake8-isort = ">=4.0.0,<5.0.0" flake8-mypy = ">=17.8.0,<18.0.0" -flake8-rst-docstrings = ">=0.0.14,<0.0.15" +flake8-rst-docstrings = ">=0.1.0,<0.2.0" +freezegun = ">=1.1.0,<2.0.0" isort = ">=5.0.0,<6.0.0" pytest = ">=6.0,<7.0" pytest-cov = ">=2.8.1,<3.0.0" @@ -74,14 +75,17 @@ python-versions = "*" [[package]] name = "asgiref" -version = "3.3.1" +version = "3.3.4" description = "ASGI specs, helper code, and adapters" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -tests = ["pytest", "pytest-asyncio"] +tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] [[package]] name = "asn1crypto" @@ -115,7 +119,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (> [[package]] name = "babel" -version = "2.9.0" +version = "2.9.1" description = "Internationalization utilities" category = "dev" optional = false @@ -213,20 +217,20 @@ python-versions = "*" [[package]] name = "boto3" -version = "1.17.44" +version = "1.17.62" description = "The AWS SDK for Python" category = "main" optional = true python-versions = ">= 2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.dependencies] -botocore = ">=1.20.44,<1.21.0" +botocore = ">=1.20.62,<1.21.0" jmespath = ">=0.7.1,<1.0.0" -s3transfer = ">=0.3.0,<0.4.0" +s3transfer = ">=0.4.0,<0.5.0" [[package]] name = "botocore" -version = "1.20.44" +version = "1.20.62" description = "Low-level, data-driven core of boto 3." category = "main" optional = true @@ -238,7 +242,7 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = ">=1.25.4,<1.27" [package.extras] -crt = ["awscrt (==0.10.8)"] +crt = ["awscrt (==0.11.11)"] [[package]] name = "bs4" @@ -495,7 +499,7 @@ dev = ["black (==19.10b0)", "flake8 (==3.8.4)", "mypy (==0.812)", "pytest (==6.2 [[package]] name = "decorator" -version = "5.0.5" +version = "5.0.7" description = "Decorators for Humans" category = "main" optional = false @@ -511,31 +515,31 @@ python-versions = "*" [[package]] name = "django" -version = "3.1.7" +version = "3.2" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -asgiref = ">=3.2.10,<4" +asgiref = ">=3.3.2,<4" pytz = "*" sqlparse = ">=0.2.2" [package.extras] -argon2 = ["argon2-cffi (>=16.1.0)"] +argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] [[package]] name = "django-any-js" -version = "1.0.3.post1" -description = "Include JavaScript libraries with readable template tags" +version = "1.1" +description = "Include JavaScript/CSS libraries with readable template tags" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.7,<4.0" [package.dependencies] -Django = ">=1.11" +Django = ">=2.2,<4.0" [[package]] name = "django-appconf" @@ -550,7 +554,7 @@ django = "*" [[package]] name = "django-auth-ldap" -version = "2.3.0" +version = "2.4.0" description = "Django LDAP authentication backend." category = "main" optional = true @@ -654,6 +658,14 @@ python-versions = "*" [package.dependencies] django-js-asset = ">=1.2.2" +[[package]] +name = "django-cleanup" +version = "5.2.0" +description = "Deletes old files." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "django-colorfield" version = "0.4.1" @@ -677,7 +689,7 @@ six = "*" [[package]] name = "django-debug-toolbar" -version = "3.2" +version = "3.2.1" description = "A configurable set of panels that display various debug information about the current request/response." category = "main" optional = false @@ -702,12 +714,15 @@ six = "*" [[package]] name = "django-extensions" -version = "3.1.1" +version = "3.1.3" description = "Extensions for Django" category = "main" optional = false python-versions = ">=3.6" +[package.dependencies] +Django = ">=2.2" + [[package]] name = "django-favicon-plus-reloaded" version = "1.0.4" @@ -733,14 +748,14 @@ Django = ">=2.2" [[package]] name = "django-formtools" -version = "2.2" +version = "2.3" description = "A set of high-level abstractions for Django forms" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [package.dependencies] -Django = ">=1.11" +Django = ">=2.2" [[package]] name = "django-guardian" @@ -781,14 +796,14 @@ Django = ">=2.2" [[package]] name = "django-health-check" -version = "3.16.3" +version = "3.16.4" description = "Run checks on services like databases, queue servers, celery processes, etc." category = "main" optional = false python-versions = "*" [package.dependencies] -django = ">=1.11" +django = ">=2.2" [[package]] name = "django-impersonate" @@ -847,7 +862,7 @@ python-versions = "*" [[package]] name = "django-material" -version = "1.7.6" +version = "1.9.0" description = "Material design for django forms and admin" category = "main" optional = false @@ -864,17 +879,6 @@ category = "main" optional = false python-versions = "*" -[[package]] -name = "django-middleware-global-request" -version = "0.1.2" -description = "Django middleware that keep request instance for every thread." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -django = "*" - [[package]] name = "django-model-utils" version = "4.1.1" @@ -903,7 +907,7 @@ six = "*" [[package]] name = "django-otp" -version = "1.0.3" +version = "1.0.4" description = "A pluggable framework for adding two-factor authentication to Django using one-time passwords." category = "main" optional = false @@ -917,7 +921,7 @@ qrcode = ["qrcode"] [[package]] name = "django-otp-yubikey" -version = "1.0.0" +version = "1.0.0.post1" description = "A django-otp plugin that verifies YubiKey OTP tokens." category = "main" optional = false @@ -929,11 +933,11 @@ YubiOTP = ">=0.2.2" [[package]] name = "django-phonenumber-field" -version = "5.0.0" +version = "5.1.0" description = "An international phone number field for django models." category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] Django = ">=2.2" @@ -1012,7 +1016,7 @@ django = ">=1.11" [[package]] name = "django-sass-processor" -version = "1.0.0" +version = "1.0.1" description = "SASS processor to compile SCSS files into *.css, while rendering, or offline." category = "main" optional = false @@ -1023,7 +1027,7 @@ management_command = ["django-compressor (>=2.4)"] [[package]] name = "django-select2" -version = "7.7.0" +version = "7.7.1" description = "Select2 option fields for Django" category = "main" optional = false @@ -1065,7 +1069,7 @@ sftp = ["paramiko"] [[package]] name = "django-stubs" -version = "1.7.0" +version = "1.8.0" description = "Mypy stubs for Django" category = "dev" optional = false @@ -1073,9 +1077,21 @@ python-versions = ">=3.6" [package.dependencies] django = "*" +django-stubs-ext = "*" mypy = ">=0.790" typing-extensions = "*" +[[package]] +name = "django-stubs-ext" +version = "0.2.0" +description = "Monkey-patching and extensions for django-stubs" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +django = "*" + [[package]] name = "django-tables2" version = "2.3.4" @@ -1175,7 +1191,7 @@ six = "*" [[package]] name = "docutils" -version = "0.17" +version = "0.16" description = "Docutils -- Python Documentation Utilities" category = "dev" optional = false @@ -1221,7 +1237,7 @@ yaml = ["ruamel.yaml"] [[package]] name = "faker" -version = "7.0.1" +version = "8.1.2" description = "Faker is a Python package that generates fake data for you." category = "main" optional = false @@ -1233,7 +1249,7 @@ text-unidecode = "1.3" [[package]] name = "flake8" -version = "3.9.0" +version = "3.9.1" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false @@ -1358,15 +1374,28 @@ flake8 = "*" [[package]] name = "flake8-rst-docstrings" -version = "0.0.14" +version = "0.1.2" description = "Python docstring reStructuredText (RST) validator" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.3" [package.dependencies] flake8 = ">=3.0.0" -restructuredtext_lint = "*" +pydocstyle = ">=3.0.0" +pygments = "*" +restructuredtext-lint = "*" + +[[package]] +name = "freezegun" +version = "1.1.0" +description = "Let your Python tests travel through time" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +python-dateutil = ">=2.7" [[package]] name = "gitdb" @@ -1381,14 +1410,15 @@ smmap = ">=3.0.1,<5" [[package]] name = "gitpython" -version = "3.1.14" +version = "3.1.15" description = "Python Git Library" category = "dev" optional = false -python-versions = ">=3.4" +python-versions = ">=3.5" [package.dependencies] gitdb = ">=4.0.1,<5" +typing-extensions = ">=3.7.4.0" [[package]] name = "html2text" @@ -1416,7 +1446,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "3.10.0" +version = "4.0.1" description = "Read metadata from Python packages" category = "main" optional = false @@ -1682,7 +1712,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pbr" -version = "5.5.1" +version = "5.6.0" description = "Python Build Reasonableness" category = "dev" optional = false @@ -1709,18 +1739,18 @@ ptyprocess = ">=0.5" [[package]] name = "pg8000" -version = "1.19.1" +version = "1.19.3" description = "PostgreSQL interface library" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -scramp = "1.3.0" +scramp = "1.4.0" [[package]] name = "phonenumbers" -version = "8.12.20" +version = "8.12.22" description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." category = "main" optional = false @@ -1758,7 +1788,7 @@ dev = ["pre-commit", "tox"] [[package]] name = "prometheus-client" -version = "0.10.0" +version = "0.10.1" description = "Python client for the Prometheus monitoring system." category = "main" optional = false @@ -1943,7 +1973,7 @@ testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", [[package]] name = "pytest-django" -version = "4.1.0" +version = "4.2.0" description = "A Django plugin for pytest." category = "dev" optional = false @@ -2103,7 +2133,7 @@ docutils = ">=0.11,<1.0" [[package]] name = "ruamel.yaml" -version = "0.17.2" +version = "0.17.4" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" category = "main" optional = false @@ -2134,7 +2164,7 @@ python-versions = "*" [[package]] name = "s3transfer" -version = "0.3.6" +version = "0.4.2" description = "An Amazon S3 Transfer Manager" category = "main" optional = true @@ -2143,6 +2173,9 @@ python-versions = "*" [package.dependencies] botocore = ">=1.12.36,<2.0a.0" +[package.extras] +crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] + [[package]] name = "safety" version = "1.10.3" @@ -2159,7 +2192,7 @@ requests = "*" [[package]] name = "scramp" -version = "1.3.0" +version = "1.4.0" description = "An implementation of the SCRAM protocol." category = "dev" optional = false @@ -2221,7 +2254,7 @@ python-versions = "*" [[package]] name = "sphinx" -version = "3.5.3" +version = "3.5.4" description = "Python documentation generator" category = "dev" optional = false @@ -2231,7 +2264,7 @@ python-versions = ">=3.5" alabaster = ">=0.7,<0.8" babel = ">=1.3" colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.12" +docutils = ">=0.12,<0.17" imagesize = "*" Jinja2 = ">=2.3" packaging = "*" @@ -2252,11 +2285,11 @@ test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] [[package]] name = "sphinx-autodoc-typehints" -version = "1.11.1" +version = "1.12.0" description = "Type hints (PEP 484) support for the Sphinx autodoc extension" category = "dev" optional = false -python-versions = ">=3.5.2" +python-versions = ">=3.6" [package.dependencies] Sphinx = ">=3.0" @@ -2429,7 +2462,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tqdm" -version = "4.59.0" +version = "4.60.0" description = "Fast, Extensible Progress Meter" category = "main" optional = false @@ -2456,7 +2489,7 @@ test = ["pytest"] [[package]] name = "twilio" -version = "6.55.0" +version = "6.57.0" description = "Twilio API client and TwiML generator" category = "main" optional = false @@ -2470,7 +2503,7 @@ six = "*" [[package]] name = "typed-ast" -version = "1.4.2" +version = "1.4.3" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false @@ -2559,7 +2592,7 @@ s3 = ["boto3", "django-storages"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "40f73ca3816a2b7654803fd9169fc13c8a203cb40512231ff0a7d280a7a332c1" +content-hash = "d5a493b473e4d884266797986af002cd2c44271946df31b3523a5bd25311ceda" [metadata.files] alabaster = [ @@ -2567,7 +2600,7 @@ alabaster = [ {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, ] aleksis-builddeps = [ - {file = "AlekSIS-Builddeps-2.tar.gz", hash = "sha256:fdf8b230ba4a690c279d99004316e84d7d9d72962768ca6b3205df54db9abaab"}, + {file = "AlekSIS-Builddeps-3.tar.gz", hash = "sha256:04597e29a861e576d78adc068c9f1bf85450c81dc43cb1f7db0ed43e975b64a2"}, ] amqp = [ {file = "amqp-5.0.6-py3-none-any.whl", hash = "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"}, @@ -2582,8 +2615,8 @@ appnope = [ {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, ] asgiref = [ - {file = "asgiref-3.3.1-py3-none-any.whl", hash = "sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17"}, - {file = "asgiref-3.3.1.tar.gz", hash = "sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"}, + {file = "asgiref-3.3.4-py3-none-any.whl", hash = "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee"}, + {file = "asgiref-3.3.4.tar.gz", hash = "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"}, ] asn1crypto = [ {file = "asn1crypto-1.4.0-py2.py3-none-any.whl", hash = "sha256:4bcdf33c861c7d40bdcd74d8e4dd7661aac320fcdf40b9a3f95b4ee12fde2fa8"}, @@ -2598,8 +2631,8 @@ attrs = [ {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, ] babel = [ - {file = "Babel-2.9.0-py2.py3-none-any.whl", hash = "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5"}, - {file = "Babel-2.9.0.tar.gz", hash = "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05"}, + {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, + {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, ] backcall = [ {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, @@ -2631,12 +2664,12 @@ bleach = [ {file = "boolean.py-3.8.tar.gz", hash = "sha256:cc24e20f985d60cd4a3a5a1c0956dd12611159d32a75081dabd0c9ab981acaa4"}, ] boto3 = [ - {file = "boto3-1.17.44-py2.py3-none-any.whl", hash = "sha256:e74da1da74fbefbe2db7a9c53082018d862433f35e2ecd4c173632efc5742f40"}, - {file = "boto3-1.17.44.tar.gz", hash = "sha256:ffb9b192b2b52ab88cde09e2af7d9fd6e541287e5719098be97ffd7144f47eb1"}, + {file = "boto3-1.17.62-py2.py3-none-any.whl", hash = "sha256:da1b2c884dbf56cc3ece07940a7b654f41a93b9fc40ee1ed21a76da25a05989c"}, + {file = "boto3-1.17.62.tar.gz", hash = "sha256:d856a71d74351649ca8dd59ad17c8c3e79ea57734ff4a38a97611e1e10b06863"}, ] botocore = [ - {file = "botocore-1.20.44-py2.py3-none-any.whl", hash = "sha256:8a7f85bf05ad62551b0e6dfeeec471147b330cb2b5c7f48795057e811e6a2e77"}, - {file = "botocore-1.20.44.tar.gz", hash = "sha256:2958e3912939558fd789a64b23a10039d8b0c0c84a23b573f3f2e3154de357ad"}, + {file = "botocore-1.20.62-py2.py3-none-any.whl", hash = "sha256:e4f8cb923edf035c2ae5f6169c70e77e31df70b88919b92b826a6b9bd14511b1"}, + {file = "botocore-1.20.62.tar.gz", hash = "sha256:f7c2c5c5ed5212b2628d8fb1c587b31c6e8d413ecbbd1a1cdf6f96ed6f5c8d5e"}, ] bs4 = [ {file = "bs4-0.0.1.tar.gz", hash = "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a"}, @@ -2802,27 +2835,28 @@ curlylint = [ {file = "curlylint-0.12.2.tar.gz", hash = "sha256:76b557cf8d007bd92df2dae61a02e65f8aa2ff3e05c6398b1314d92692fbb0d8"}, ] decorator = [ - {file = "decorator-5.0.5-py3-none-any.whl", hash = "sha256:b7157d62ea3c2c0c57b81a05e4569853e976a3dda5dd7a1cb86be78978c3c5f8"}, - {file = "decorator-5.0.5.tar.gz", hash = "sha256:acda948ffcfe4bd0c4a57834b74ad968b91925b8201b740ca9d46fb8c5c618ce"}, + {file = "decorator-5.0.7-py3-none-any.whl", hash = "sha256:945d84890bb20cc4a2f4a31fc4311c0c473af65ea318617f13a7257c9a58bc98"}, + {file = "decorator-5.0.7.tar.gz", hash = "sha256:6f201a6c4dac3d187352661f508b9364ec8091217442c9478f1f83c003a0f060"}, ] dj-database-url = [ {file = "dj-database-url-0.5.0.tar.gz", hash = "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163"}, {file = "dj_database_url-0.5.0-py2.py3-none-any.whl", hash = "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9"}, ] django = [ - {file = "Django-3.1.7-py3-none-any.whl", hash = "sha256:baf099db36ad31f970775d0be5587cc58a6256a6771a44eb795b554d45f211b8"}, - {file = "Django-3.1.7.tar.gz", hash = "sha256:32ce792ee9b6a0cbbec340123e229ac9f765dff8c2a4ae9247a14b2ba3a365a7"}, + {file = "Django-3.2-py3-none-any.whl", hash = "sha256:0604e84c4fb698a5e53e5857b5aea945b2f19a18f25f10b8748dbdf935788927"}, + {file = "Django-3.2.tar.gz", hash = "sha256:21f0f9643722675976004eb683c55d33c05486f94506672df3d6a141546f389d"}, ] django-any-js = [ - {file = "django-any-js-1.0.3.post1.tar.gz", hash = "sha256:32306643d4989b3cdbbf6a87bb43ca4d5ca35863c96ad96a8bc0d50bcf9d4ab4"}, + {file = "django-any-js-1.1.tar.gz", hash = "sha256:2972946902ba049f73bf8bb87e0a0118f77a8c9dca89438f193598bff758422f"}, + {file = "django_any_js-1.1-py3-none-any.whl", hash = "sha256:1499934e293bbcaad29b8edaaefca87dda79eb3df1faeaaea67b80e2866ae1f8"}, ] django-appconf = [ {file = "django-appconf-1.0.4.tar.gz", hash = "sha256:be58deb54a43d77d2e1621fe59f787681376d3cd0b8bd8e4758ef6c3a6453380"}, {file = "django_appconf-1.0.4-py2.py3-none-any.whl", hash = "sha256:1b1d0e1069c843ebe8ae5aa48ec52403b1440402b320c3e3a206a0907e97bb06"}, ] django-auth-ldap = [ - {file = "django-auth-ldap-2.3.0.tar.gz", hash = "sha256:5894317122a086c9955ed366562869a81459cf6b663636b152857bb5d3a0a3b7"}, - {file = "django_auth_ldap-2.3.0-py3-none-any.whl", hash = "sha256:cbbb476eff2504b5ab4fdf1fa92d93d2d3408fd9c8bc0c426169d987d0733153"}, + {file = "django-auth-ldap-2.4.0.tar.gz", hash = "sha256:60fcbfc3141c99c3c49d3ccd7311a3992a231c319d94b6d2c143968f63676676"}, + {file = "django_auth_ldap-2.4.0-py3-none-any.whl", hash = "sha256:2d869955da8a0c9a4448671bd9826b9f87458f6a9fc20278e84de8a81200a2be"}, ] django-bleach = [ {file = "django-bleach-0.6.1.tar.gz", hash = "sha256:674709c26040618aff0741ce8261fd151e5ead405bd50568c2034662d69daac3"}, @@ -2856,6 +2890,10 @@ django-ckeditor = [ {file = "django-ckeditor-6.0.0.tar.gz", hash = "sha256:29fd1a333cb9741ac2c3fd4e427a5c00115ed33a2389716a09af7656022dcdde"}, {file = "django_ckeditor-6.0.0-py2.py3-none-any.whl", hash = "sha256:cc2d377f1bdcd4ca1540caeebe85f7e2cd006198d57328ef6c718d3eaa5a0846"}, ] +django-cleanup = [ + {file = "django-cleanup-5.2.0.tar.gz", hash = "sha256:909d10ff574f5ce1a40fa63bd5c94c9ed866fd7ae770994c46cdf66c3db3e846"}, + {file = "django_cleanup-5.2.0-py2.py3-none-any.whl", hash = "sha256:193cf69de54b9fc0a0f4547edbb3a63bbe01728cb029f9f4b7912098cc1bced7"}, +] django-colorfield = [ {file = "django-colorfield-0.4.1.tar.gz", hash = "sha256:63a542c417b72d0dac898a0f61a2a00aed3c9aabc2f5057c926efccf421f7887"}, {file = "django_colorfield-0.4.1-py3-none-any.whl", hash = "sha256:e38f8b9dabbab48a6dab3d1eb5bd802decb92970d56a28128c9a70cdbf383e30"}, @@ -2864,16 +2902,16 @@ django-dbbackup = [ {file = "django-dbbackup-3.3.0.tar.gz", hash = "sha256:bb109735cae98b64ad084e5b461b7aca2d7b39992f10c9ed9435e3ebb6fb76c8"}, ] django-debug-toolbar = [ - {file = "django-debug-toolbar-3.2.tar.gz", hash = "sha256:84e2607d900dbd571df0a2acf380b47c088efb787dce9805aefeb407341961d2"}, - {file = "django_debug_toolbar-3.2-py3-none-any.whl", hash = "sha256:9e5a25d0c965f7e686f6a8ba23613ca9ca30184daa26487706d4829f5cfb697a"}, + {file = "django-debug-toolbar-3.2.1.tar.gz", hash = "sha256:a5ff2a54f24bf88286f9872836081078f4baa843dc3735ee88524e89f8821e33"}, + {file = "django_debug_toolbar-3.2.1-py3-none-any.whl", hash = "sha256:e759e63e3fe2d3110e0e519639c166816368701eab4a47fed75d7de7018467b9"}, ] django-dynamic-preferences = [ {file = "django-dynamic-preferences-1.10.1.tar.gz", hash = "sha256:e4b2bb7b2563c5064ba56dd76441c77e06b850ff1466a386a1cd308909a6c7de"}, {file = "django_dynamic_preferences-1.10.1-py2.py3-none-any.whl", hash = "sha256:9419fa925fd2cbb665269ae72059eb3058bf080913d853419b827e4e7a141902"}, ] django-extensions = [ - {file = "django-extensions-3.1.1.tar.gz", hash = "sha256:674ad4c3b1587a884881824f40212d51829e662e52f85b012cd83d83fe1271d9"}, - {file = "django_extensions-3.1.1-py3-none-any.whl", hash = "sha256:9507f8761ee760748938fd8af766d0608fb2738cf368adfa1b2451f61c15ae35"}, + {file = "django-extensions-3.1.3.tar.gz", hash = "sha256:5f0fea7bf131ca303090352577a9e7f8bfbf5489bd9d9c8aea9401db28db34a0"}, + {file = "django_extensions-3.1.3-py3-none-any.whl", hash = "sha256:50de8977794a66a91575dd40f87d5053608f679561731845edbd325ceeb387e3"}, ] django-favicon-plus-reloaded = [ {file = "django-favicon-plus-reloaded-1.0.4.tar.gz", hash = "sha256:90c761c636a338e6e9fb1d086649d82095085f92cff816c9cf074607f28c85a5"}, @@ -2884,8 +2922,8 @@ django-filter = [ {file = "django_filter-2.4.0-py3-none-any.whl", hash = "sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1"}, ] 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"}, + {file = "django-formtools-2.3.tar.gz", hash = "sha256:9663b6eca64777b68d6d4142efad8597fe9a685924673b25aa8a1dcff4db00c3"}, + {file = "django_formtools-2.3-py3-none-any.whl", hash = "sha256:4699937e19ee041d803943714fe0c1c7ad4cab802600eb64bbf4cdd0a1bfe7d9"}, ] django-guardian = [ {file = "django-guardian-2.3.0.tar.gz", hash = "sha256:ed2de26e4defb800919c5749fb1bbe370d72829fbd72895b6cf4f7f1a7607e1b"}, @@ -2899,8 +2937,8 @@ django-haystack = [ {file = "django-haystack-3.0.tar.gz", hash = "sha256:d490f920afa85471dd1fa5000bc8eff4b704daacbe09aee1a64e75cbc426f3be"}, ] django-health-check = [ - {file = "django-health-check-3.16.3.tar.gz", hash = "sha256:a6aa6ea423eae4fd0665f6372b826af1ed20dfc3e88cf52789d0b49cfb64969c"}, - {file = "django_health_check-3.16.3-py2.py3-none-any.whl", hash = "sha256:d0628ffc11aee7e62e73b58ff39179ea2a9ca5abfbc92cb345ceca268593dd71"}, + {file = "django-health-check-3.16.4.tar.gz", hash = "sha256:334bcbbb9273a6dbd9c928e78474306e623dfb38cc442281cb9fd230a20a7fdb"}, + {file = "django_health_check-3.16.4-py2.py3-none-any.whl", hash = "sha256:86a8869d67e72394a1dd73e37819a7d2cfd915588b96927fda611d7451fd4735"}, ] django-impersonate = [ {file = "django-impersonate-1.7.3.tar.gz", hash = "sha256:282003957577c7143fe31e5861f8fffdf6fe0c25557aedb28fcf8b11474eaa23"}, @@ -2925,15 +2963,12 @@ django-maintenance-mode = [ {file = "django_maintenance_mode-0.16.0-py3-none-any.whl", hash = "sha256:88287573b4e95285052f664d4f08e15ac4c350c1a6c77bc743ca3fc6e1f6410c"}, ] django-material = [ - {file = "django-material-1.7.6.tar.gz", hash = "sha256:5488e8fe24069cc6682801692ad05293a4b60a637a87a31e0ebd9f3319cd371d"}, - {file = "django_material-1.7.6-py2.py3-none-any.whl", hash = "sha256:b5496505da7dd92f23ca694bc411c6bf0ff584fc30f4239d890ab29f9260160c"}, + {file = "django-material-1.9.0.tar.gz", hash = "sha256:5a7144d1029b4a2bfee2e5d0d8d00f30742dd7e3f868b3787d8cd61e54f26437"}, + {file = "django_material-1.9.0-py2.py3-none-any.whl", hash = "sha256:816513170771bcb2540b5ce314fbef1a906906220587a9cb9521e61092a6f610"}, ] django-menu-generator-ng = [ {file = "django-menu-generator-ng-1.2.3.tar.gz", hash = "sha256:0c21a094b094add909655728b6b2d4a8baa5a2047da8f649be52589bea0e3ba2"}, ] -django-middleware-global-request = [ - {file = "django-middleware-global-request-0.1.2.tar.gz", hash = "sha256:f6490759bc9f7dbde4001709554e29ca715daf847f2222914b4e47117dca9313"}, -] django-model-utils = [ {file = "django-model-utils-4.1.1.tar.gz", hash = "sha256:eb5dd05ef7d7ce6bc79cae54ea7c4a221f6f81e2aad7722933aee66489e7264b"}, {file = "django_model_utils-4.1.1-py3-none-any.whl", hash = "sha256:ef7c440024e797796a3811432abdd2be8b5225ae64ef346f8bfc6de7d8e5d73c"}, @@ -2943,16 +2978,16 @@ django-oauth-toolkit = [ {file = "django_oauth_toolkit-1.5.0-py3-none-any.whl", hash = "sha256:b2e346a7c1e222774bfb370f21b556b92b408395b4c23914e2d1b241b2e5376a"}, ] django-otp = [ - {file = "django-otp-1.0.3.tar.gz", hash = "sha256:381a15e65293b8b06d47b7d6b306e0b7af2e104137ac92f6c566d3b9b90b6244"}, - {file = "django_otp-1.0.3-py3-none-any.whl", hash = "sha256:f4ab096b424c33ffe69453620356e1b7517f30dfb9ba13bfeaa1d1f20faddc13"}, + {file = "django-otp-1.0.4.tar.gz", hash = "sha256:04852c5301befb02d1d8ba4a31d375eb08d7c2cb6fe86b5f840867435ab1309c"}, + {file = "django_otp-1.0.4-py3-none-any.whl", hash = "sha256:3916fc7652c2f934b1cf3807dd8ed257ce7605c10dfefa27fadda5628d9a9c9e"}, ] django-otp-yubikey = [ - {file = "django-otp-yubikey-1.0.0.tar.gz", hash = "sha256:fbd409277892229b7e3578faa4f63ea766e242659456939164c8f71b845287b6"}, - {file = "django_otp_yubikey-1.0.0-py2.py3-none-any.whl", hash = "sha256:07743473024900c3b7a14647039f2cf66148cf6243d6aee0853ba45516c224a4"}, + {file = "django-otp-yubikey-1.0.0.post1.tar.gz", hash = "sha256:1da060257611d06e681848b7923fd788d878a79e8c358a373374deab13a085af"}, + {file = "django_otp_yubikey-1.0.0.post1-py2.py3-none-any.whl", hash = "sha256:613c96be211c1267400a5a78ae63f212c722f82dffb9daef3c8b1df370abb9be"}, ] django-phonenumber-field = [ - {file = "django-phonenumber-field-5.0.0.tar.gz", hash = "sha256:1eb7af3a108744665f7c3939d38aa15b3728c57d13d45d656b0a2aa11e8cdc3c"}, - {file = "django_phonenumber_field-5.0.0-py3-none-any.whl", hash = "sha256:adb46905cc4ecb19d8494424e1c4352f24946bb472340a2a17257d44bf8228e6"}, + {file = "django-phonenumber-field-5.1.0.tar.gz", hash = "sha256:9eda963ac15b363393f677cc084efd45c3bd97bb5a0cdb4a06409ac99e05dd4b"}, + {file = "django_phonenumber_field-5.1.0-py3-none-any.whl", hash = "sha256:48724ba235ee8248a474204faa0934c5baf9536f429859d05cb131fbd6b1c695"}, ] django-polymorphic = [ {file = "django-polymorphic-3.0.0.tar.gz", hash = "sha256:9d886f19f031d26bb1391c055ed9be06fb226a04a4cec1842b372c58873b3caa"}, @@ -2979,11 +3014,12 @@ django-reversion = [ {file = "django_reversion-3.0.9-py3-none-any.whl", hash = "sha256:1b57127a136b969f4b843a915c72af271febe7f336469db6c27121f8adcad35c"}, ] django-sass-processor = [ - {file = "django-sass-processor-1.0.0.tar.gz", hash = "sha256:cb90efee38cd7b0fe727c78d8993ad7804de33f40328200dfc1a481307ef0466"}, + {file = "django-sass-processor-1.0.1.tar.gz", hash = "sha256:dcaad47c591a2d52689c1bd209259e922e902d886293f0d5c9e0d1a4eb85eda2"}, + {file = "django_sass_processor-1.0.1-py3-none-any.whl", hash = "sha256:1f043180c47754018e803a77da003377f5ea6558de57cd6946eb27a32e9c16a2"}, ] django-select2 = [ - {file = "django-select2-7.7.0.tar.gz", hash = "sha256:26b4c59cbeba57aea1737187b930a83c8070788286b4236b13f7873c01b32684"}, - {file = "django_select2-7.7.0-py2.py3-none-any.whl", hash = "sha256:e56bfe3074d6b87524c5dbc139884c18c74a5e7324d66f0b93e42b6012ea0dc0"}, + {file = "django-select2-7.7.1.tar.gz", hash = "sha256:dd091342e99436818b3fa98783ae6c24fb2a0cbc37ebd3faa0aef68422b6e416"}, + {file = "django_select2-7.7.1-py2.py3-none-any.whl", hash = "sha256:8c54984bb931d842eab6a46d1b427c6883e5f5347529cda27dcd942fb37d87b9"}, ] django-settings-context-processor = [ {file = "django-settings-context-processor-0.2.tar.gz", hash = "sha256:d37c853d69a3069f5abbf94c7f4f6fc0fac38bbd0524190cd5a250ba800e496a"}, @@ -2993,8 +3029,12 @@ django-storages = [ {file = "django_storages-1.11.1-py3-none-any.whl", hash = "sha256:f28765826d507a0309cfaa849bd084894bc71d81bf0d09479168d44785396f80"}, ] django-stubs = [ - {file = "django-stubs-1.7.0.tar.gz", hash = "sha256:ddd190aca5b9adb4d30760d5c64f67cb3658703f5f42c3bb0c2c71ff4d752c39"}, - {file = "django_stubs-1.7.0-py3-none-any.whl", hash = "sha256:30a7d99c694acf79c5d93d69a5a8e4b54d2a8c11dd672aa869006789e2189fa6"}, + {file = "django-stubs-1.8.0.tar.gz", hash = "sha256:717967d7fee0a6af0746724a0be80d72831a982a40fa8f245a6a46f4cafd157b"}, + {file = "django_stubs-1.8.0-py3-none-any.whl", hash = "sha256:bde9e44e3c4574c2454e74a3e607cc3bc23b0441bb7d1312cd677d5e30984b74"}, +] +django-stubs-ext = [ + {file = "django-stubs-ext-0.2.0.tar.gz", hash = "sha256:c14f297835a42c1122421ec7e2d06579996b29d33b8016002762afa5d78863af"}, + {file = "django_stubs_ext-0.2.0-py3-none-any.whl", hash = "sha256:bd4a1e36ef2ba0ef15801933c85c68e59b383302c873795c6ecfc25950c7ecdb"}, ] django-tables2 = [ {file = "django-tables2-2.3.4.tar.gz", hash = "sha256:50ccadbd13740a996d8a4d4f144ef80134745cd0b5ec278061537e341f5ef7a2"}, @@ -3022,8 +3062,8 @@ django-yarnpkg = [ {file = "django-yarnpkg-6.0.1.tar.gz", hash = "sha256:aa059347b246c6f242401581d2c129bdcb45aa726be59fe2f288762a9843348a"}, ] docutils = [ - {file = "docutils-0.17-py2.py3-none-any.whl", hash = "sha256:a71042bb7207c03d5647f280427f14bfbd1a65c9eb84f4b341d85fafb6bb4bdf"}, - {file = "docutils-0.17.tar.gz", hash = "sha256:e2ffeea817964356ba4470efba7c2f42b6b0de0b04e66378507e3e2504bbff4c"}, + {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, + {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, ] dparse = [ {file = "dparse-0.5.1-py3-none-any.whl", hash = "sha256:e953a25e44ebb60a5c6efc2add4420c177f1d8404509da88da9729202f306994"}, @@ -3034,18 +3074,19 @@ dynaconf = [ {file = "dynaconf-3.1.4.tar.gz", hash = "sha256:b2f472d83052f809c5925565b8a2ba76a103d5dc1dbb9748b693ed67212781b9"}, ] faker = [ - {file = "Faker-7.0.1-py3-none-any.whl", hash = "sha256:08c4cfbfd498c0e90aff6741771c01803d894013df858db6a573182c6a47951f"}, - {file = "Faker-7.0.1.tar.gz", hash = "sha256:20c6e4253b73ef2a783d38e085e7c8d8916295fff31c7403116d2af8f908f7ca"}, + {file = "Faker-8.1.2-py3-none-any.whl", hash = "sha256:156854f36d4086bb21ff85a79b4d6a6403a240cd2c17a33a44b8ea4ff4e957c2"}, + {file = "Faker-8.1.2.tar.gz", hash = "sha256:a2ed065342e91a7672407325848cd5728d5e5eb4928d0a1c478fd4f0dd97d1f7"}, ] flake8 = [ - {file = "flake8-3.9.0-py2.py3-none-any.whl", hash = "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff"}, - {file = "flake8-3.9.0.tar.gz", hash = "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0"}, + {file = "flake8-3.9.1-py2.py3-none-any.whl", hash = "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a"}, + {file = "flake8-3.9.1.tar.gz", hash = "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378"}, ] flake8-bandit = [ {file = "flake8_bandit-2.1.2.tar.gz", hash = "sha256:687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b"}, ] flake8-black = [ {file = "flake8-black-0.2.1.tar.gz", hash = "sha256:f26651bc10db786c03f4093414f7c9ea982ed8a244cec323c984feeffdf4c118"}, + {file = "flake8_black-0.2.1-py3-none-any.whl", hash = "sha256:941514149cb8b489cb17a4bb1cf18d84375db3b34381bb018de83509437931a0"}, ] flake8-builtins = [ {file = "flake8-builtins-1.5.3.tar.gz", hash = "sha256:09998853b2405e98e61d2ff3027c47033adbdc17f9fe44ca58443d876eb00f3b"}, @@ -3076,15 +3117,20 @@ flake8-polyfill = [ {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, ] flake8-rst-docstrings = [ - {file = "flake8-rst-docstrings-0.0.14.tar.gz", hash = "sha256:8f8bcb18f1408b506dd8ba2c99af3eac6128f6911d4bf6ff874b94caa70182a2"}, + {file = "flake8-rst-docstrings-0.1.2.tar.gz", hash = "sha256:7d34d2175a0cd92aba0872ade74268b2f2c12582c7267d4a0e6ef1c32a828ce3"}, + {file = "flake8_rst_docstrings-0.1.2-py3-none-any.whl", hash = "sha256:73b5db2fd9d4d7c7e6b7767931730c4570e5a89f34a1501b837afb3b6d31537a"}, +] +freezegun = [ + {file = "freezegun-1.1.0-py2.py3-none-any.whl", hash = "sha256:2ae695f7eb96c62529f03a038461afe3c692db3465e215355e1bb4b0ab408712"}, + {file = "freezegun-1.1.0.tar.gz", hash = "sha256:177f9dd59861d871e27a484c3332f35a6e3f5d14626f2bf91be37891f18927f3"}, ] gitdb = [ {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"}, {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, ] gitpython = [ - {file = "GitPython-3.1.14-py3-none-any.whl", hash = "sha256:3283ae2fba31c913d857e12e5ba5f9a7772bbc064ae2bb09efafa71b0dd4939b"}, - {file = "GitPython-3.1.14.tar.gz", hash = "sha256:be27633e7509e58391f10207cd32b2a6cf5b908f92d9cd30da2e514e1137af61"}, + {file = "GitPython-3.1.15-py3-none-any.whl", hash = "sha256:a77824e516d3298b04fb36ec7845e92747df8fcfee9cacc32dd6239f9652f867"}, + {file = "GitPython-3.1.15.tar.gz", hash = "sha256:05af150f47a5cca3f4b0af289b73aef8cf3c4fe2385015b06220cbcdee48bb6e"}, ] html2text = [ {file = "html2text-2020.1.16-py3-none-any.whl", hash = "sha256:c7c629882da0cf377d66f073329ccf34a12ed2adf0169b9285ae4e63ef54c82b"}, @@ -3099,8 +3145,8 @@ imagesize = [ {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-3.10.0-py3-none-any.whl", hash = "sha256:d2d46ef77ffc85cbf7dac7e81dd663fde71c45326131bea8033b9bad42268ebe"}, - {file = "importlib_metadata-3.10.0.tar.gz", hash = "sha256:c9db46394197244adf2f0b08ec5bc3cf16757e9590b02af1fca085c16c0d600a"}, + {file = "importlib_metadata-4.0.1-py3-none-any.whl", hash = "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d"}, + {file = "importlib_metadata-4.0.1.tar.gz", hash = "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -3264,8 +3310,8 @@ pathspec = [ {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, ] pbr = [ - {file = "pbr-5.5.1-py2.py3-none-any.whl", hash = "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"}, - {file = "pbr-5.5.1.tar.gz", hash = "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9"}, + {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"}, + {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"}, ] persisting-theory = [ {file = "persisting-theory-0.2.1.tar.gz", hash = "sha256:00ff7dcc8f481ff75c770ca5797d968e8725b6df1f77fe0cf7d20fa1e5790c0a"}, @@ -3275,12 +3321,12 @@ pexpect = [ {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, ] pg8000 = [ - {file = "pg8000-1.19.1-py3-none-any.whl", hash = "sha256:35c1f3db6e5540456aa38da5f42589274e7266c25365d2829dc8d52513520abe"}, - {file = "pg8000-1.19.1.tar.gz", hash = "sha256:cb7ace8c582b7000a5ee428efa8ff6c82a7d710cc0f7d2d76258703a2aa7afe3"}, + {file = "pg8000-1.19.3-py3-none-any.whl", hash = "sha256:2f9599f455f813e9298982a741f40b62b7bdcf6816e51031d4adc2deda6525e1"}, + {file = "pg8000-1.19.3.tar.gz", hash = "sha256:f547572d12de77b65888e4e72922e8ad55b88791c486df93f3d71207aea7f25f"}, ] phonenumbers = [ - {file = "phonenumbers-8.12.20-py2.py3-none-any.whl", hash = "sha256:7c2b26ee026f765a8032fc2a333b46fa1860445c7ce6df3b717b9f6985106084"}, - {file = "phonenumbers-8.12.20.tar.gz", hash = "sha256:ee5a8508c4a414262abad92ec33f050347f681973ed0fb36e98b52bfe159f6b8"}, + {file = "phonenumbers-8.12.22-py2.py3-none-any.whl", hash = "sha256:f9cb4882e5c7daeaa183af7b0390c44a02604731c7aab561d9456e1df4582207"}, + {file = "phonenumbers-8.12.22.tar.gz", hash = "sha256:b20765cb9d1392308071a3462c06edcaa953cb383e3565f5fc0a6fb93f240150"}, ] pickleshare = [ {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, @@ -3326,8 +3372,8 @@ pluggy = [ {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] prometheus-client = [ - {file = "prometheus_client-0.10.0-py2.py3-none-any.whl", hash = "sha256:c5843b3e1b4689a3599a2463e5b5850d110d1a7e28a94bdc2c6f5bb6585cfb18"}, - {file = "prometheus_client-0.10.0.tar.gz", hash = "sha256:1e7bc14fd6ca9c3fc07309b73a7a3469920dfe88ca9f331c02258cc62736cbc2"}, + {file = "prometheus_client-0.10.1-py2.py3-none-any.whl", hash = "sha256:030e4f9df5f53db2292eec37c6255957eb76168c6f974e4176c711cf91ed34aa"}, + {file = "prometheus_client-0.10.1.tar.gz", hash = "sha256:b6c5a9643e3545bcbfd9451766cbaa5d9c67e7303c7bc32c750b6fa70ecb107d"}, ] prompt-toolkit = [ {file = "prompt_toolkit-3.0.18-py3-none-any.whl", hash = "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04"}, @@ -3487,8 +3533,8 @@ pytest-cov = [ {file = "pytest_cov-2.11.1-py2.py3-none-any.whl", hash = "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"}, ] pytest-django = [ - {file = "pytest-django-4.1.0.tar.gz", hash = "sha256:26f02c16d36fd4c8672390deebe3413678d89f30720c16efb8b2a6bf63b9041f"}, - {file = "pytest_django-4.1.0-py3-none-any.whl", hash = "sha256:10e384e6b8912ded92db64c58be8139d9ae23fb8361e5fc139d8e4f8fc601bc2"}, + {file = "pytest-django-4.2.0.tar.gz", hash = "sha256:80f8875226ec4dc0b205f0578072034563879d98d9b1bec143a80b9045716cb0"}, + {file = "pytest_django-4.2.0-py3-none-any.whl", hash = "sha256:a51150d8962200250e850c6adcab670779b9c2aa07271471059d1fb92a843fa9"}, ] pytest-django-testing-postgresql = [ {file = "pytest-django-testing-postgresql-0.1.post0.tar.gz", hash = "sha256:78b0c58930084cb4393407b2e5a2a3b8734c627b841ecef7d62d39bbfb8e8a45"}, @@ -3601,8 +3647,8 @@ restructuredtext-lint = [ {file = "restructuredtext_lint-1.3.2.tar.gz", hash = "sha256:d3b10a1fe2ecac537e51ae6d151b223b78de9fafdd50e5eb6b08c243df173c80"}, ] "ruamel.yaml" = [ - {file = "ruamel.yaml-0.17.2-py3-none-any.whl", hash = "sha256:0850def9ebca23b3a8c64c4b4115ebb6b364a10d49f89d289a26ee965e1e7d9d"}, - {file = "ruamel.yaml-0.17.2.tar.gz", hash = "sha256:8f1e15421668b9edf30ed02899f5f81aff9808a4271935776f61a99a569a13da"}, + {file = "ruamel.yaml-0.17.4-py3-none-any.whl", hash = "sha256:ac79fb25f5476e8e9ed1c53b8a2286d2c3f5dde49eb37dbcee5c7eb6a8415a22"}, + {file = "ruamel.yaml-0.17.4.tar.gz", hash = "sha256:44bc6b54fddd45e4bc0619059196679f9e8b79c027f4131bb072e6a22f4d5e28"}, ] "ruamel.yaml.clib" = [ {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:28116f204103cb3a108dfd37668f20abe6e3cafd0d3fd40dba126c732457b3cc"}, @@ -3641,16 +3687,16 @@ rules = [ {file = "rules-2.2.tar.gz", hash = "sha256:9bae429f9d4f91a375402990da1541f9e093b0ac077221d57124d06eeeca4405"}, ] s3transfer = [ - {file = "s3transfer-0.3.6-py2.py3-none-any.whl", hash = "sha256:5d48b1fd2232141a9d5fb279709117aaba506cacea7f86f11bc392f06bfa8fc2"}, - {file = "s3transfer-0.3.6.tar.gz", hash = "sha256:c5dadf598762899d8cfaecf68eba649cd25b0ce93b6c954b156aaa3eed160547"}, + {file = "s3transfer-0.4.2-py2.py3-none-any.whl", hash = "sha256:9b3752887a2880690ce628bc263d6d13a3864083aeacff4890c1c9839a5eb0bc"}, + {file = "s3transfer-0.4.2.tar.gz", hash = "sha256:cb022f4b16551edebbb31a377d3f09600dbada7363d8c5db7976e7f47732e1b2"}, ] safety = [ {file = "safety-1.10.3-py2.py3-none-any.whl", hash = "sha256:5f802ad5df5614f9622d8d71fedec2757099705c2356f862847c58c6dfe13e84"}, {file = "safety-1.10.3.tar.gz", hash = "sha256:30e394d02a20ac49b7f65292d19d38fa927a8f9582cdfd3ad1adbbc66c641ad5"}, ] scramp = [ - {file = "scramp-1.3.0-py3-none-any.whl", hash = "sha256:6d73eae03e7a3d647a8c36ca95dc8082fe56496db6f803b561ab231627022f82"}, - {file = "scramp-1.3.0.tar.gz", hash = "sha256:f56208b544387b98e9d39735cc054e273d060efcdf44bb4a20935180772d1ccf"}, + {file = "scramp-1.4.0-py3-none-any.whl", hash = "sha256:27349d6839038fe3b56c641ea2a8703df065c1d605fdee67275857c0a82122b4"}, + {file = "scramp-1.4.0.tar.gz", hash = "sha256:d27d768408c6fc025a0e567eed84325b0aaf24364c81ea5974e8334ae3c4fda3"}, ] selenium = [ {file = "selenium-3.141.0-py2.py3-none-any.whl", hash = "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c"}, @@ -3677,12 +3723,12 @@ spdx-license-list = [ {file = "spdx_license_list-0.5.2.tar.gz", hash = "sha256:952996f72ab807972dc2278bb9b91e5294767211e51f09aad9c0e2ff5b82a31b"}, ] sphinx = [ - {file = "Sphinx-3.5.3-py3-none-any.whl", hash = "sha256:3f01732296465648da43dec8fb40dc451ba79eb3e2cc5c6d79005fd98197107d"}, - {file = "Sphinx-3.5.3.tar.gz", hash = "sha256:ce9c228456131bab09a3d7d10ae58474de562a6f79abb3dc811ae401cf8c1abc"}, + {file = "Sphinx-3.5.4-py3-none-any.whl", hash = "sha256:2320d4e994a191f4b4be27da514e46b3d6b420f2ff895d064f52415d342461e8"}, + {file = "Sphinx-3.5.4.tar.gz", hash = "sha256:19010b7b9fa0dc7756a6e105b2aacd3a80f798af3c25c273be64d7beeb482cb1"}, ] sphinx-autodoc-typehints = [ - {file = "sphinx-autodoc-typehints-1.11.1.tar.gz", hash = "sha256:244ba6d3e2fdb854622f643c7763d6f95b6886eba24bec28e86edf205e4ddb20"}, - {file = "sphinx_autodoc_typehints-1.11.1-py3-none-any.whl", hash = "sha256:da049791d719f4c9813642496ee4764203e317f0697eb75446183fa2a68e3f77"}, + {file = "sphinx-autodoc-typehints-1.12.0.tar.gz", hash = "sha256:193617d9dbe0847281b1399d369e74e34cd959c82e02c7efde077fca908a9f52"}, + {file = "sphinx_autodoc_typehints-1.12.0-py3-none-any.whl", hash = "sha256:5e81776ec422dd168d688ab60f034fccfafbcd94329e9537712c93003bddc04a"}, ] sphinxcontrib-applehelp = [ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, @@ -3744,47 +3790,47 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tqdm = [ - {file = "tqdm-4.59.0-py2.py3-none-any.whl", hash = "sha256:9fdf349068d047d4cfbe24862c425883af1db29bcddf4b0eeb2524f6fbdb23c7"}, - {file = "tqdm-4.59.0.tar.gz", hash = "sha256:d666ae29164da3e517fcf125e41d4fe96e5bb375cd87ff9763f6b38b5592fe33"}, + {file = "tqdm-4.60.0-py2.py3-none-any.whl", hash = "sha256:daec693491c52e9498632dfbe9ccfc4882a557f5fa08982db1b4d3adbe0887c3"}, + {file = "tqdm-4.60.0.tar.gz", hash = "sha256:ebdebdb95e3477ceea267decfc0784859aa3df3e27e22d23b83e9b272bf157ae"}, ] traitlets = [ {file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"}, {file = "traitlets-5.0.5.tar.gz", hash = "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396"}, ] twilio = [ - {file = "twilio-6.55.0.tar.gz", hash = "sha256:766555e9f3bdfe9eb2fad9e2efa701f6f7644337a3f6b31a660293d2fbd54331"}, + {file = "twilio-6.57.0.tar.gz", hash = "sha256:3a9a0e3882897185bf3a997408a43662bcfdec84250a701d06aff5b89bbaa9a1"}, ] typed-ast = [ - {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, - {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, - {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"}, - {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"}, - {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"}, - {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"}, - {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"}, - {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"}, - {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"}, - {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"}, - {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"}, - {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"}, - {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"}, - {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"}, - {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"}, - {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"}, - {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"}, - {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"}, - {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"}, - {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"}, - {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"}, - {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"}, - {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"}, - {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"}, - {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"}, - {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"}, - {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"}, - {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"}, - {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"}, - {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, + {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, + {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, + {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, + {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, + {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, + {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, + {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, + {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, + {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] typing-extensions = [ {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, diff --git a/pyproject.toml b/pyproject.toml index f2cafb552b79679de75626f5bdd588ff9048f16c..b5d24d7e844e23779a94c2c374579d1c873c49ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,14 +34,13 @@ secondary = true [tool.poetry.dependencies] python = "^3.7" -Django = "^3.1.7" -django-any-js = "^1.0" +Django = "^3.2" +django-any-js = "^1.1" django-debug-toolbar = "^3.2" -django-middleware-global-request = "^0.1.2" django-menu-generator-ng = "^1.2.3" django-tables2 = "^2.1" Pillow = "^8.0" -django-phonenumber-field = {version = "<5.1", extras = ["phonenumbers"]} +django-phonenumber-field = {version = "<5.2", extras = ["phonenumbers"]} django-sass-processor = "^1.0" libsass = "^0.20.0" colour = "^0.1.5" @@ -90,7 +89,7 @@ psutil = "^5.7.0" celery-progress = "^0.1.0" django-cachalot = "^2.3.2" django-prometheus = "^2.1.0" -importlib-metadata = {version = "^3.0.0", python = "<3.9"} +importlib-metadata = {version = "^4.0.0", python = "<3.9"} django-model-utils = "^4.0.0" bs4 = "^0.0.1" django-uwsgi-ng = "^1.1.0" @@ -100,6 +99,7 @@ django-oauth-toolkit = "^1.5.0" django-redis = "^4.12.1" django-storages = {version = "^1.11.1", optional = true} boto3 = {version = "^1.17.33", optional = true} +django-cleanup = "^5.1.0" [tool.poetry.extras] ldap = ["django-auth-ldap"]