diff --git a/biscuit/core/anonymizers.py b/biscuit/core/anonymizers.py index 03d4f3bf3a8597a3a61a9683878eae1a9f1637ea..9a063a819a0f5dac62353c5e2a5dfd1a6ca088a3 100644 --- a/biscuit/core/anonymizers.py +++ b/biscuit/core/anonymizers.py @@ -18,6 +18,9 @@ class PersonAnonymizer(BaseAnonymizer): ('phone_number', ''), ('mobile_number', ''), ('email', faker.email), - ('date_of_birth', lambda **kwargs: faker.date_of_birth(minimum_age=8, maximum_age=66, **kwargs)), - ('photo', '') + ( + 'date_of_birth', + lambda **kwargs: faker.date_of_birth(minimum_age=8, maximum_age=66, **kwargs), + ), + ('photo', ''), ] diff --git a/biscuit/core/cronjobs.py b/biscuit/core/cronjobs.py index 229e017cf840a2eabfa9b624aa47e28cac636686..4c9d81ebb412fd986830f0bbef9dd03e196d2bb4 100644 --- a/biscuit/core/cronjobs.py +++ b/biscuit/core/cronjobs.py @@ -7,7 +7,9 @@ class Backup(CronJobBase): RUN_AT_TIMES = settings.DBBACKUP_CRON_TIMES RETRY_AFTER_FAILURE_MINS = 5 - schedule = Schedule(run_at_times=RUN_AT_TIMES, retry_after_failure_mins=RETRY_AFTER_FAILURE_MINS) + schedule = Schedule( + run_at_times=RUN_AT_TIMES, retry_after_failure_mins=RETRY_AFTER_FAILURE_MINS + ) code = 'biscuit.core.Backup' def do(self): diff --git a/biscuit/core/decorators.py b/biscuit/core/decorators.py index 732f120958924f683f05482bd57661cc503f2882..7c0feb30ef05ea88431bc41ef210ceecd1003921 100644 --- a/biscuit/core/decorators.py +++ b/biscuit/core/decorators.py @@ -3,6 +3,5 @@ from django.contrib.auth.decorators import user_passes_test def admin_required(function: Callable = None) -> Callable: - actual_decorator = user_passes_test( - lambda u: u.is_active and u.is_superuser) + actual_decorator = user_passes_test(lambda u: u.is_active and u.is_superuser) return actual_decorator(function) diff --git a/biscuit/core/forms.py b/biscuit/core/forms.py index c7e813fdec59f29777da3670fb1aeee377850b3f..119730401b574eebef1657b3700c34f16d4b4f28 100644 --- a/biscuit/core/forms.py +++ b/biscuit/core/forms.py @@ -10,9 +10,7 @@ class PersonAccountForm(forms.ModelForm): class Meta: model = Person fields = ['last_name', 'first_name', 'user'] - widgets = { - 'user': Select2Widget - } + widgets = {'user': Select2Widget} new_user = forms.CharField(required=False) @@ -26,51 +24,74 @@ class PersonAccountForm(forms.ModelForm): if self.cleaned_data.get('new_user', None): if self.cleaned_data.get('user', None): - self.add_error('new_user', _( - 'You cannot set a new username when also selecting an existing user.')) + self.add_error( + 'new_user', + _('You cannot set a new username when also selecting an existing user.'), + ) elif User.objects.filter(username=self.cleaned_data['new_user']).exists(): self.add_error('new_user', _('This username is already in use.')) else: - new_user_obj = User.objects.create_user(self.cleaned_data['new_user'], - self.instance.email, - first_name=self.instance.first_name, - last_name=self.instance.last_name) + new_user_obj = User.objects.create_user( + self.cleaned_data['new_user'], + self.instance.email, + first_name=self.instance.first_name, + last_name=self.instance.last_name, + ) self.cleaned_data['user'] = new_user_obj PersonsAccountsFormSet = forms.modelformset_factory( - Person, form=PersonAccountForm, max_num=0, extra=0) + Person, form=PersonAccountForm, max_num=0, extra=0 +) class EditPersonForm(forms.ModelForm): class Meta: model = Person - fields = ['user', 'is_active', 'first_name', 'last_name', 'additional_name', 'short_name', 'street', 'housenumber', - 'postal_code', 'place', 'phone_number', 'mobile_number', 'email', 'date_of_birth', 'sex', 'photo', 'photo_cropping'] - widgets = { - 'user': Select2Widget - } + fields = [ + 'user', + 'is_active', + 'first_name', + 'last_name', + 'additional_name', + 'short_name', + 'street', + 'housenumber', + 'postal_code', + 'place', + 'phone_number', + 'mobile_number', + 'email', + 'date_of_birth', + 'sex', + 'photo', + 'photo_cropping', + ] + widgets = {'user': Select2Widget} new_user = forms.CharField( - required=False, - label=_('New user'), - help_text=_('Create a new account')) + required=False, label=_('New user'), help_text=_('Create a new account') + ) def clean(self) -> None: User = get_user_model() if self.cleaned_data.get('new_user', None): if self.cleaned_data.get('user', None): - self.add_error('new_user', _( - 'You cannot set a new username when also selecting an existing user.')) + self.add_error( + 'new_user', + _('You cannot set a new username when also selecting an existing user.'), + ) elif User.objects.filter(username=self.cleaned_data['new_user']).exists(): self.add_error('new_user', _('This username is already in use.')) else: - new_user_obj = User.objects.create_user(self.cleaned_data['new_user'], - self.instance.email, - first_name=self.instance.first_name, - last_name=self.instance.last_name) + new_user_obj = User.objects.create_user( + self.cleaned_data['new_user'], + self.instance.email, + first_name=self.instance.first_name, + last_name=self.instance.last_name, + ) self.cleaned_data['user'] = new_user_obj @@ -80,11 +101,26 @@ class EditGroupForm(forms.ModelForm): model = Group fields = ['name', 'short_name', 'members', 'owners', 'parent_groups'] widgets = { - 'members': ModelSelect2MultipleWidget(search_fields=['first_name__icontains', 'last_name__icontains', 'short_name__icontains']), - 'owners': ModelSelect2MultipleWidget(search_fields=['first_name__icontains', 'last_name__icontains', 'short_name__icontains']), - 'parent_groups': ModelSelect2MultipleWidget(search_fields=['name__icontains', 'short_name__icontains']), + 'members': ModelSelect2MultipleWidget( + search_fields=[ + 'first_name__icontains', + 'last_name__icontains', + 'short_name__icontains', + ] + ), + 'owners': ModelSelect2MultipleWidget( + search_fields=[ + 'first_name__icontains', + 'last_name__icontains', + 'short_name__icontains', + ] + ), + 'parent_groups': ModelSelect2MultipleWidget( + search_fields=['name__icontains', 'short_name__icontains'] + ), } + class EditSchoolForm(forms.ModelForm): class Meta: model = School diff --git a/biscuit/core/menus.py b/biscuit/core/menus.py index a3ba09e29e7a459c7b1a25a20c35cb425c4dcb17..f4afc55773b106cb26b4fcc408be9ebc061f4149 100644 --- a/biscuit/core/menus.py +++ b/biscuit/core/menus.py @@ -11,102 +11,116 @@ MENUS = { { 'name': _('Stop impersonation'), 'url': 'impersonate-stop', - 'validators': ['menu_generator.validators.is_authenticated', 'biscuit.core.util.core_helpers.is_impersonate'] + 'validators': [ + 'menu_generator.validators.is_authenticated', + 'biscuit.core.util.core_helpers.is_impersonate', + ], }, { 'name': _('Login'), 'url': settings.LOGIN_URL, - 'validators': ['menu_generator.validators.is_anonymous'] + 'validators': ['menu_generator.validators.is_anonymous'], }, { 'name': _('Logout'), 'url': 'logout', - 'validators': ['menu_generator.validators.is_authenticated'] + 'validators': ['menu_generator.validators.is_authenticated'], }, { 'name': _('Two factor auth'), 'url': 'two_factor:profile', - 'validators': ['menu_generator.validators.is_authenticated', lambda request: 'two_factor' in settings.INSTALLED_APPS] - } - ] + 'validators': [ + 'menu_generator.validators.is_authenticated', + lambda request: 'two_factor' in settings.INSTALLED_APPS, + ], + }, + ], }, { 'name': _('Admin'), 'url': '#', - 'validators': ['menu_generator.validators.is_authenticated', 'menu_generator.validators.is_superuser'], + 'validators': [ + 'menu_generator.validators.is_authenticated', + 'menu_generator.validators.is_superuser', + ], 'submenu': [ { 'name': _('Data management'), 'url': 'data_management', - 'validators': ['menu_generator.validators.is_authenticated', 'menu_generator.validators.is_superuser'] + 'validators': [ + 'menu_generator.validators.is_authenticated', + 'menu_generator.validators.is_superuser', + ], }, { 'name': _('System status'), 'url': 'system_status', - 'validators': ['menu_generator.validators.is_authenticated', 'menu_generator.validators.is_superuser'] + 'validators': [ + 'menu_generator.validators.is_authenticated', + 'menu_generator.validators.is_superuser', + ], }, { 'name': _('Impersonation'), 'url': 'impersonate-list', - 'validators': ['menu_generator.validators.is_authenticated', 'menu_generator.validators.is_superuser'] + 'validators': [ + 'menu_generator.validators.is_authenticated', + 'menu_generator.validators.is_superuser', + ], }, { 'name': _('Manage school'), 'url': 'school_management', - 'validators': ['menu_generator.validators.is_authenticated', 'menu_generator.validators.is_superuser'] - } - ] + 'validators': [ + 'menu_generator.validators.is_authenticated', + 'menu_generator.validators.is_superuser', + ], + }, + ], }, { 'name': _('People'), 'url': '#', 'root': True, - 'validators': ['menu_generator.validators.is_authenticated', 'biscuit.core.util.core_helpers.has_person'], + 'validators': [ + 'menu_generator.validators.is_authenticated', + 'biscuit.core.util.core_helpers.has_person', + ], 'submenu': [ { 'name': _('Persons'), 'url': 'persons', - 'validators': ['menu_generator.validators.is_authenticated'] + 'validators': ['menu_generator.validators.is_authenticated'], }, { 'name': _('Groups'), 'url': 'groups', - 'validators': ['menu_generator.validators.is_authenticated'] + 'validators': ['menu_generator.validators.is_authenticated'], }, { 'name': _('Persons and accounts'), 'url': 'persons_accounts', - 'validators': ['menu_generator.validators.is_authenticated', 'menu_generator.validators.is_superuser'] - } - ] - } + 'validators': [ + 'menu_generator.validators.is_authenticated', + 'menu_generator.validators.is_superuser', + ], + }, + ], + }, ], 'FOOTER_MENU_CORE': [ { 'name': _('BiscuIT Software'), 'url': '#', 'submenu': [ - { - 'name': _('Website'), - 'url': 'https://biscuit.edugit.org/' - }, - { - 'name': 'Teckids e.V.', - 'url': 'https://www.teckids.org/' - } - ] + {'name': _('Website'), 'url': 'https://biscuit.edugit.org/'}, + {'name': 'Teckids e.V.', 'url': 'https://www.teckids.org/'}, + ], }, ], - 'DATA_MANAGEMENT_MENU': [ - ], + 'DATA_MANAGEMENT_MENU': [], 'SCHOOL_MANAGEMENT_MENU': [ - { - 'name': _('Edit school information'), - 'url': 'edit_school_information', - }, - { - 'name': _('Edit school term'), - 'url': 'edit_school_term', - } + {'name': _('Edit school information'), 'url': 'edit_school_information',}, + {'name': _('Edit school term'), 'url': 'edit_school_term',}, ], } diff --git a/biscuit/core/mixins.py b/biscuit/core/mixins.py index 9ec842a5ea99b793c688a63a9a2e8c1d5d9203b4..a5d535ac2cedcbd76e25e8c042d235fbc15b28e8 100644 --- a/biscuit/core/mixins.py +++ b/biscuit/core/mixins.py @@ -67,6 +67,7 @@ class ExtensibleModel(object): cls._safe_add(func, func.__name__) + class CRUDMixin(models.Model): class Meta: abstract = True @@ -78,8 +79,5 @@ class CRUDMixin(models.Model): content_type = ContentType.objects.get_for_model(self) return CRUDEvent.objects.filter( - object_id=self.pk, - content_type=content_type - ).select_related( - 'user' - ) + object_id=self.pk, content_type=content_type + ).select_related('user') diff --git a/biscuit/core/models.py b/biscuit/core/models.py index ae3f5950a570f17b8e8cec7ad96be167bca2d79a..a659136d97cc584643eae3cc715166870b678159 100644 --- a/biscuit/core/models.py +++ b/biscuit/core/models.py @@ -21,6 +21,7 @@ class ThemeSettings(dbsettings.Group): colour_light = dbsettings.StringValue(default='#f8f9fa') colour_dark = dbsettings.StringValue(default='#343a40') + theme_settings = ThemeSettings('Global theme settings') @@ -32,8 +33,11 @@ class School(models.Model): """ name = models.CharField(verbose_name=_('Name'), max_length=30) - name_official = models.CharField(verbose_name=_('Official name'), max_length=200, help_text=_( - 'Official name of the school, e.g. as given by supervisory authority')) + name_official = models.CharField( + verbose_name=_('Official name'), + max_length=200, + help_text=_('Official name of the school, e.g. as given by supervisory authority'), + ) logo = ImageCropField(verbose_name=_('School logo'), blank=True, null=True) logo_cropping = ImageRatioField('logo', '600x600', size_warning=True) @@ -51,13 +55,10 @@ class SchoolTerm(models.Model): be linked to. """ - caption = models.CharField(verbose_name=_('Visible caption of the term'), - max_length=30) + caption = models.CharField(verbose_name=_('Visible caption of the term'), max_length=30) - date_start = models.DateField(verbose_name=_( - 'Effective start date of term'), null=True) - date_end = models.DateField(verbose_name=_( - 'Effective end date of term'), null=True) + date_start = models.DateField(verbose_name=_('Effective start date of term'), null=True) + date_end = models.DateField(verbose_name=_('Effective end date of term'), null=True) current = models.NullBooleanField(default=None, unique=True) @@ -75,54 +76,51 @@ class Person(models.Model, ExtensibleModel): class Meta: ordering = ['last_name', 'first_name'] - SEX_CHOICES = [ - ('f', _('female')), - ('m', _('male')) - ] + SEX_CHOICES = [('f', _('female')), ('m', _('male'))] user = models.OneToOneField( - get_user_model(), on_delete=models.SET_NULL, blank=True, null=True, - related_name='person') - is_active = models.BooleanField( - verbose_name=_('Is person active?'), default=True) + get_user_model(), on_delete=models.SET_NULL, blank=True, null=True, related_name='person' + ) + is_active = models.BooleanField(verbose_name=_('Is person active?'), default=True) first_name = models.CharField(verbose_name=_('First name'), max_length=30) last_name = models.CharField(verbose_name=_('Last name'), max_length=30) - additional_name = models.CharField(verbose_name=_( - 'Additional name(s)'), max_length=30, blank=True) + additional_name = models.CharField( + verbose_name=_('Additional name(s)'), max_length=30, blank=True + ) - short_name = models.CharField(verbose_name=_( - 'Short name'), max_length=5, blank=True, null=True, unique=True) + short_name = models.CharField( + verbose_name=_('Short name'), max_length=5, blank=True, null=True, unique=True + ) - street = models.CharField(verbose_name=_( - 'Street'), max_length=30, blank=True) - housenumber = models.CharField(verbose_name=_( - 'Street number'), max_length=10, blank=True) - postal_code = models.CharField(verbose_name=_( - 'Postal code'), max_length=5, blank=True) - place = models.CharField(verbose_name=_( - 'Place'), max_length=30, blank=True) + street = models.CharField(verbose_name=_('Street'), max_length=30, blank=True) + housenumber = models.CharField(verbose_name=_('Street number'), max_length=10, blank=True) + postal_code = models.CharField(verbose_name=_('Postal code'), max_length=5, blank=True) + place = models.CharField(verbose_name=_('Place'), max_length=30, blank=True) phone_number = PhoneNumberField(verbose_name=_('Home phone'), blank=True) - mobile_number = PhoneNumberField( - verbose_name=_('Mobile phone'), blank=True) + mobile_number = PhoneNumberField(verbose_name=_('Mobile phone'), blank=True) email = models.EmailField(verbose_name=_('E-mail address'), blank=True) - date_of_birth = models.DateField( - verbose_name=_('Date of birth'), blank=True, null=True) - sex = models.CharField(verbose_name=_( - 'Sex'), max_length=1, choices=SEX_CHOICES, blank=True) + date_of_birth = models.DateField(verbose_name=_('Date of birth'), blank=True, null=True) + sex = models.CharField(verbose_name=_('Sex'), max_length=1, choices=SEX_CHOICES, blank=True) photo = ImageCropField(verbose_name=_('Photo'), blank=True, null=True) photo_cropping = ImageRatioField('photo', '600x800', size_warning=True) - import_ref = models.CharField(verbose_name=_( - 'Reference ID of import source'), max_length=64, - blank=True, null=True, editable=False, unique=True) + import_ref = models.CharField( + verbose_name=_('Reference ID of import source'), + max_length=64, + blank=True, + null=True, + editable=False, + unique=True, + ) - guardians = models.ManyToManyField('self', verbose_name=_('Guardians / Parents'), - symmetrical=False, related_name='children') + guardians = models.ManyToManyField( + 'self', verbose_name=_('Guardians / Parents'), symmetrical=False, related_name='children' + ) primary_group = models.ForeignKey('Group', models.SET_NULL, null=True) @@ -143,8 +141,7 @@ class Person(models.Model, ExtensibleModel): if it can't find one. """ - group, created = Group.objects.get_or_create(short_name=value, - defaults={'name': value}) + group, created = Group.objects.get_or_create(short_name=value, defaults={'name': value}) self.primary_group = group @property @@ -163,16 +160,19 @@ class Group(models.Model, ExtensibleModel): class Meta: ordering = ['short_name', 'name'] - name = models.CharField(verbose_name=_( - 'Long name of group'), max_length=60, unique=True) - short_name = models.CharField(verbose_name=_( - 'Short name of group'), max_length=16, unique=True) + name = models.CharField(verbose_name=_('Long name of group'), max_length=60, unique=True) + short_name = models.CharField(verbose_name=_('Short name of group'), max_length=16, unique=True) members = models.ManyToManyField('Person', related_name='member_of') owners = models.ManyToManyField('Person', related_name='owner_of') - parent_groups = models.ManyToManyField('self', related_name='child_groups', - symmetrical=False, verbose_name=_('Parent groups'), blank=True) + parent_groups = models.ManyToManyField( + 'self', + related_name='child_groups', + symmetrical=False, + verbose_name=_('Parent groups'), + blank=True, + ) def __str__(self) -> str: return '%s (%s)' % (self.name, self.short_name) diff --git a/biscuit/core/settings.py b/biscuit/core/settings.py index f70853445f219c1492daacc0ac4fdd904e41a217..66441e7143637d4f45d57cb76639599f23bd76ed 100644 --- a/biscuit/core/settings.py +++ b/biscuit/core/settings.py @@ -20,7 +20,7 @@ for directory in DIRS_FOR_DYNACONF: _settings = LazySettings( ENVVAR_PREFIX_FOR_DYNACONF=ENVVAR_PREFIX_FOR_DYNACONF, - SETTINGS_FILE_FOR_DYNACONF=SETTINGS_FILE_FOR_DYNACONF + SETTINGS_FILE_FOR_DYNACONF=SETTINGS_FILE_FOR_DYNACONF, ) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -36,7 +36,7 @@ DEBUG_TOOLBAR_CONFIG = { 'RENDER_PANELS': True, 'SHOW_COLLAPSED': True, 'JQUERY_URL': '', - 'SHOW_TOOLBAR_CALLBACK': 'biscuit.core.util.core_helpers.dt_show_toolbar' + 'SHOW_TOOLBAR_CALLBACK': 'biscuit.core.util.core_helpers.dt_show_toolbar', } ALLOWED_HOSTS = _settings.get('http.allowed_hosts', []) @@ -75,7 +75,7 @@ INSTALLED_APPS = [ 'otp_yubikey', 'biscuit.core', 'impersonate', - 'two_factor' + 'two_factor', ] INSTALLED_APPS += get_app_packages() @@ -84,7 +84,7 @@ STATICFILES_FINDERS = [ 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'django_yarnpkg.finders.NodeModulesFinder', - 'sass_processor.finders.CssFinder' + 'sass_processor.finders.CssFinder', ] @@ -121,7 +121,7 @@ TEMPLATES = [ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', 'maintenance_mode.context_processors.maintenance_mode', - 'settings_context_processor.context_processors.settings' + 'settings_context_processor.context_processors.settings', ], }, }, @@ -148,7 +148,7 @@ DATABASES = { 'PASSWORD': _settings.get('database.password', None), 'HOST': _settings.get('database.host', '127.0.0.1'), 'PORT': _settings.get('database.port', '5432'), - 'ATOMIC_REQUESTS': True + 'ATOMIC_REQUESTS': True, } } @@ -156,7 +156,7 @@ if _settings.get('caching.memcached.enabled', True): CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', - 'LOCATION': _settings.get('caching.memcached.address', '127.0.0.1:11211') + 'LOCATION': _settings.get('caching.memcached.address', '127.0.0.1:11211'), } } @@ -164,18 +164,10 @@ if _settings.get('caching.memcached.enabled', True): # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',}, + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',}, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',}, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',}, ] # Authentication backends are dynamically populated @@ -200,7 +192,7 @@ if _settings.get('ldap.uri', None): AUTH_LDAP_USER_SEARCH = LDAPSearch( _settings.get('ldap.users.base'), ldap.SCOPE_SUBTREE, - _settings.get('ldap.users.filter', '(uid=%(user)s)') + _settings.get('ldap.users.filter', '(uid=%(user)s)'), ) # Mapping of LDAP attributes to Django model fields @@ -241,42 +233,33 @@ STATIC_ROOT = _settings.get('static.root', os.path.join(BASE_DIR, 'static')) MEDIA_ROOT = _settings.get('media.root', os.path.join(BASE_DIR, 'media')) NODE_MODULES_ROOT = _settings.get('node_modules.root', os.path.join(BASE_DIR, 'node_modules')) -YARN_INSTALLED_APPS = [ - 'bootstrap', - 'font-awesome', - 'jquery', - 'popper.js', - 'datatables', - 'select2' -] +YARN_INSTALLED_APPS = ['bootstrap', 'font-awesome', 'jquery', 'popper.js', 'datatables', 'select2'] JS_URL = _settings.get('js_assets.url', STATIC_URL) -JS_ROOT = _settings.get('js_assets.root', NODE_MODULES_ROOT+'/node_modules') +JS_ROOT = _settings.get('js_assets.root', NODE_MODULES_ROOT + '/node_modules') -FONT_AWESOME = {'url': JS_URL+'/font-awesome/css/font-awesome.min.css'} +FONT_AWESOME = {'url': JS_URL + '/font-awesome/css/font-awesome.min.css'} BOOTSTRAP4 = { - 'css_url': JS_URL+'/bootstrap/dist//css/bootstrap.min.css', - 'javascript_url': JS_URL+'/bootstrap/dist/js/bootstrap.min.js', - 'jquery_url': JS_URL+'/jquery/dist/jquery.min.js', - 'popper_url': JS_URL+'/popper.js/dist/umd/popper.min.js', + 'css_url': JS_URL + '/bootstrap/dist//css/bootstrap.min.css', + 'javascript_url': JS_URL + '/bootstrap/dist/js/bootstrap.min.js', + 'jquery_url': JS_URL + '/jquery/dist/jquery.min.js', + 'popper_url': JS_URL + '/popper.js/dist/umd/popper.min.js', 'include_jquery': True, 'include_popper': True, - 'javascript_in_head': True + 'javascript_in_head': True, } -SELECT2_CSS = JS_URL+'/select2/dist/css/select2.min.css' -SELECT2_JS = JS_URL+'/select2/dist/js/select2.min.js' -SELECT2_I18N_PATH = JS_URL+'/select2/dist/js/i18n' +SELECT2_CSS = JS_URL + '/select2/dist/css/select2.min.css' +SELECT2_JS = JS_URL + '/select2/dist/js/select2.min.js' +SELECT2_I18N_PATH = JS_URL + '/select2/dist/js/i18n' ANY_JS = { - 'DataTables': { - 'js_url': JS_URL+'/datatables/media/js/jquery.dataTables.min.js' - }, + 'DataTables': {'js_url': JS_URL + '/datatables/media/js/jquery.dataTables.min.js'}, 'DataTables-Bootstrap4': { - 'css_url': JS_URL+'/datatables/media/css/dataTables.bootstrap4.min.css', - 'js_url': JS_URL+'/datatables/media/js/dataTables.bootstrap4.min.js' - } + 'css_url': JS_URL + '/datatables/media/css/dataTables.bootstrap4.min.css', + 'js_url': JS_URL + '/datatables/media/js/dataTables.bootstrap4.min.js', + }, } SASS_PROCESSOR_AUTO_INCLUDE = False @@ -284,9 +267,7 @@ SASS_PROCESSOR_CUSTOM_FUNCTIONS = { 'get-colour': 'biscuit.core.util.sass_helpers.get_colour', 'get-theme-setting': 'biscuit.core.util.sass_helpers.get_theme_setting', } -SASS_PROCESSOR_INCLUDE_DIRS = [ - _settings.get('bootstrap.sass_path', JS_ROOT+'/bootstrap/scss/') -] +SASS_PROCESSOR_INCLUDE_DIRS = [_settings.get('bootstrap.sass_path', JS_ROOT + '/bootstrap/scss/')] ADMINS = _settings.get('contact.admins', []) SERVER_EMAIL = _settings.get('contact.from', 'root@localhost') @@ -307,30 +288,25 @@ TEMPLATE_VISIBLE_SETTINGS = ['ADMINS', 'DEBUG'] MAINTENANCE_MODE = _settings.get('maintenance.enabled', None) MAINTENANCE_MODE_IGNORE_IP_ADDRESSES = _settings.get( - 'maintenance.ignore_ips', _settings.get('maintenance.internal_ips', [])) + 'maintenance.ignore_ips', _settings.get('maintenance.internal_ips', []) +) MAINTENANCE_MODE_GET_CLIENT_IP_ADDRESS = 'ipware.ip.get_ip' MAINTENANCE_MODE_IGNORE_SUPERUSER = True -MAINTENANCE_MODE_STATE_FILE_PATH = _settings.get('maintenance.statefile', 'maintenance_mode_state.txt') +MAINTENANCE_MODE_STATE_FILE_PATH = _settings.get( + 'maintenance.statefile', 'maintenance_mode_state.txt' +) -IMPERSONATE = { - 'USE_HTTP_REFERER': True, - 'REQUIRE_SUPERUSER': True, - 'ALLOW_SUPERUSER': True -} +IMPERSONATE = {'USE_HTTP_REFERER': True, 'REQUIRE_SUPERUSER': True, 'ALLOW_SUPERUSER': True} DJANGO_TABLES2_TEMPLATE = "django_tables2/bootstrap4.html" DBBACKUP_STORAGE = _settings.get('backup.storage', 'django.core.files.storage.FileSystemStorage') -DBBACKUP_STORAGE_OPTIONS = { - 'location': _settings.get('backup.location', '/var/backups/biscuit') -} +DBBACKUP_STORAGE_OPTIONS = {'location': _settings.get('backup.location', '/var/backups/biscuit')} DBBACKUP_CLEANUP_KEEP = _settings.get('backup.keep.database', 10) DBBACKUP_CLEANUP_KEEP_MEDIA = _settings.get('backup.keep.media', 10) DBBACKUP_CRON_TIMES = _settings.get('backup.times', None) or ['03:57'] -CRON_CLASSES = [ - 'biscuit.core.cronjobs.Backup' -] +CRON_CLASSES = ['biscuit.core.cronjobs.Backup'] ANONYMIZE_ENABLED = _settings.get('maintenance.anonymisable', True) @@ -343,7 +319,10 @@ if _settings.get('2fa.sms.enabled', False): TWO_FACTOR_SMS_GATEWAY = 'two_factor.gateways.twilio.gateway.Twilio' if _settings.get('2fa.twilio.sid', None): - MIDDLEWARE.insert(MIDDLEWARE.index('django_otp.middleware.OTPMiddleware')+1, 'two_factor.middleware.threadlocals.ThreadLocals') + MIDDLEWARE.insert( + MIDDLEWARE.index('django_otp.middleware.OTPMiddleware') + 1, + 'two_factor.middleware.threadlocals.ThreadLocals', + ) TWILIO_SID = _settings.get('2fa.twilio.sid') TWILIO_TOKEN = _settings.get('2fa.twilio.token') TWILIO_CALLER_ID = _settings.get('2fa.twilio.callerid') diff --git a/biscuit/core/tests/browser/test_selenium.py b/biscuit/core/tests/browser/test_selenium.py index fcdde21653e644fc90461e5e52f4a3a506ffd7b1..21705ba508ee81825c7cbfecadbd6c09676fb9ab 100644 --- a/biscuit/core/tests/browser/test_selenium.py +++ b/biscuit/core/tests/browser/test_selenium.py @@ -7,9 +7,12 @@ from django.test.selenium import SeleniumTestCase, SeleniumTestCaseBase from django.urls import reverse SeleniumTestCaseBase.external_host = os.environ.get('TEST_HOST', '') or None -SeleniumTestCaseBase.browsers = list(filter(bool, os.environ.get('TEST_SELENIUM_BROWSERS', '').split(','))) +SeleniumTestCaseBase.browsers = list( + filter(bool, os.environ.get('TEST_SELENIUM_BROWSERS', '').split(',')) +) SeleniumTestCaseBase.selenium_hub = os.environ.get('TEST_SELENIUM_HUB', '') or None + class SeleniumTests(SeleniumTestCase): serialized_rollback = True @@ -18,7 +21,9 @@ class SeleniumTests(SeleniumTestCase): screenshot_path = os.environ.get('TEST_SCREENSHOT_PATH', None) if screenshot_path: os.makedirs(os.path.join(screenshot_path, cls.browser), exist_ok=True) - return cls.selenium.save_screenshot(os.path.join(screenshot_path, cls.browser, filename)) + return cls.selenium.save_screenshot( + os.path.join(screenshot_path, cls.browser, filename) + ) else: return False @@ -47,9 +52,7 @@ class SeleniumTests(SeleniumTestCase): 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(), "Next")]' - ).click() + self.selenium.find_element_by_xpath('//button[contains(text(), "Next")]').click() self._screenshot('login_default_superuser_submitted.png') # Should redirect away from login page and not put up an alert about wrong credentials diff --git a/biscuit/core/tests/models/test_person.py b/biscuit/core/tests/models/test_person.py index e454d3b60709498564c1c005242694b08ad805bb..328f459b3b5859ac422a08cb61acbe1ea31715c5 100644 --- a/biscuit/core/tests/models/test_person.py +++ b/biscuit/core/tests/models/test_person.py @@ -5,9 +5,6 @@ from biscuit.core.models import Person @pytest.mark.django_db def test_full_name(): - _person = Person.objects.create( - first_name='Jane', - last_name='Doe' - ) + _person = Person.objects.create(first_name='Jane', last_name='Doe') assert _person.full_name == 'Doe, Jane' diff --git a/biscuit/core/tests/templatetags/test_data_helpers.py b/biscuit/core/tests/templatetags/test_data_helpers.py index 096903947c8c21870ad5c05fcffb5b0a8e522a12..22281a18462088f3bceef6f94a84f8f60200612f 100644 --- a/biscuit/core/tests/templatetags/test_data_helpers.py +++ b/biscuit/core/tests/templatetags/test_data_helpers.py @@ -1,21 +1,25 @@ from biscuit.core.templatetags.data_helpers import get_dict + def test_get_dict_object(): class _Foo(object): bar = 12 assert _Foo.bar == get_dict(_Foo, 'bar') + def test_get_dict_dict(): _foo = {'bar': 12} assert _foo['bar'] == get_dict(_foo, 'bar') + def test_get_dict_list(): _foo = [10, 11, 12] assert _foo[2] == get_dict(_foo, 2) + def test_get_dict_invalid(): _foo = 12 diff --git a/biscuit/core/tests/views/test_account.py b/biscuit/core/tests/views/test_account.py index 3bffcf17e9e226ac04230d367219e3cc5942a835..6a5971720ab70e680e8dcc5688e27f6483705fbc 100644 --- a/biscuit/core/tests/views/test_account.py +++ b/biscuit/core/tests/views/test_account.py @@ -3,6 +3,7 @@ import pytest from django.conf import settings from django.urls import reverse + @pytest.mark.django_db def test_index_not_logged_in(client): response = client.get('/') @@ -10,6 +11,7 @@ def test_index_not_logged_in(client): assert response.status_code == 200 assert reverse(settings.LOGIN_URL) in response.content.decode('utf-8') + @pytest.mark.django_db def test_login(client, django_user_model): username = 'foo' @@ -23,6 +25,7 @@ def test_login(client, django_user_model): assert response.status_code == 200 assert reverse(settings.LOGIN_URL) not in response.content.decode('utf-8') + @pytest.mark.django_db def test_index_not_linked_to_person(client, django_user_model): username = 'foo' @@ -36,6 +39,7 @@ def test_index_not_linked_to_person(client, django_user_model): assert response.status_code == 200 assert 'You are not linked to a person' in response.content.decode('utf-8') + @pytest.mark.django_db def test_logout(client, django_user_model): username = 'foo' diff --git a/biscuit/core/urls.py b/biscuit/core/urls.py index 0ecfbb6f34d064a32170999f3886327100a6da9d..3b172dc5e7558df97b05cd02df1b1da35bc6d9d0 100644 --- a/biscuit/core/urls.py +++ b/biscuit/core/urls.py @@ -22,22 +22,19 @@ urlpatterns = [ path('persons', views.persons, name='persons'), path('persons/accounts', views.persons_accounts, name='persons_accounts'), path('person', views.person, name='person'), - path('person/<int:id_>', views.person, - {'template': 'full'}, name='person_by_id'), - path('person/<int:id_>/card', views.person, - {'template': 'card'}, name='person_by_id_card'), + path('person/<int:id_>', views.person, {'template': 'full'}, name='person_by_id'), + path('person/<int:id_>/card', views.person, {'template': 'card'}, name='person_by_id_card'), path('person/<int:id_>/edit', views.edit_person, name='edit_person_by_id'), path('groups', views.groups, name='groups'), path('group/create', views.edit_group, name='create_group'), - path('group/<int:id_>', views.group, - {'template': 'full'}, name='group_by_id'), + path('group/<int:id_>', views.group, {'template': 'full'}, name='group_by_id'), path('group/<int:id_>/edit', views.edit_group, name='edit_group_by_id'), path('', views.index, name='index'), path('maintenance-mode/', include('maintenance_mode.urls')), path('impersonate/', include('impersonate.urls')), path('__i18n__/', include('django.conf.urls.i18n')), path('select2/', include('django_select2.urls')), - path('settings/', include('dbsettings.urls')) + path('settings/', include('dbsettings.urls')), ] # Serve static files from STATIC_ROOT to make it work with runserver @@ -47,6 +44,7 @@ urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # Add URLs for optional features if hasattr(settings, 'TWILIO_ACCOUNT_SID'): from two_factor.gateways.twilio.urls import urlpatterns as tf_twilio_urls # noqa + urlpatterns += [path('', include(tf_twilio_urls))] # Serve javascript-common if in development @@ -58,5 +56,4 @@ for app_config in apps.app_configs.values(): if not app_config.name.startswith('biscuit.apps.'): continue - urlpatterns.append(path('app/%s/' % app_config.label, - include('%s.urls' % app_config.name))) + urlpatterns.append(path('app/%s/' % app_config.label, include('%s.urls' % app_config.name))) diff --git a/biscuit/core/util/apps.py b/biscuit/core/util/apps.py index 306bb1f701c0419f29610d84766d06ce777f6cd9..9b732d651caae3412e002717e57c518c515bbdae 100644 --- a/biscuit/core/util/apps.py +++ b/biscuit/core/util/apps.py @@ -11,7 +11,9 @@ class AppConfig(django.apps.AppConfig): # Run model extension code try: - import_module('.'.join(self.__class__.__module__.split('.')[:-1] + ['model_extensions'])) + import_module( + '.'.join(self.__class__.__module__.split('.')[:-1] + ['model_extensions']) + ) except ImportError: # ImportErrors are non-fatal because model extensions are optional. pass diff --git a/biscuit/core/util/core_helpers.py b/biscuit/core/util/core_helpers.py index 03ec0f6391811d62d22b002d4765816f74119115..4089026c884b03abe83a468e0949853581bcdf39 100644 --- a/biscuit/core/util/core_helpers.py +++ b/biscuit/core/util/core_helpers.py @@ -19,6 +19,7 @@ def dt_show_toolbar(request: HttpRequest) -> bool: return False + def get_app_packages() -> Sequence[str]: """ Find all packages within the biscuit.apps namespace. """ diff --git a/biscuit/core/util/messages.py b/biscuit/core/util/messages.py index 7b847885567b989100b89de4c830d73f4346bb70..e3c93dbb0301ea3eff1c2c7f4b18556d33c09789 100644 --- a/biscuit/core/util/messages.py +++ b/biscuit/core/util/messages.py @@ -5,7 +5,9 @@ from django.contrib import messages from django.http import HttpRequest -def add_message(request: Optional[HttpRequest], level: int, message: str, **kwargs) -> Optional[Any]: +def add_message( + request: Optional[HttpRequest], level: int, message: str, **kwargs +) -> Optional[Any]: if request: return messages.add_message(request, level, message, **kwargs) else: diff --git a/biscuit/core/views.py b/biscuit/core/views.py index 585d049ae72bd65c0a8ffd16bba88056850f2f04..d3eb0e7752f964afc1d5ae630e65835dbe6f65ca 100644 --- a/biscuit/core/views.py +++ b/biscuit/core/views.py @@ -9,7 +9,13 @@ from django.utils.translation import ugettext_lazy as _ from django_cron.models import CronJobLog from .decorators import admin_required -from .forms import PersonsAccountsFormSet, EditPersonForm, EditGroupForm, EditSchoolForm, EditTermForm +from .forms import ( + PersonsAccountsFormSet, + EditPersonForm, + EditGroupForm, + EditSchoolForm, + EditTermForm, +) from .models import Person, Group, School from .tables import PersonsTable, GroupsTable from .util import messages @@ -181,9 +187,9 @@ def data_management(request: HttpRequest) -> HttpResponse: def system_status(request: HttpRequest) -> HttpResponse: context = {} - context['backups'] = CronJobLog.objects.filter( - code='biscuit.core.Backup' - ).order_by('-end_time')[:10] + context['backups'] = CronJobLog.objects.filter(code='biscuit.core.Backup').order_by( + '-end_time' + )[:10] return render(request, 'core/system_status.html', context)