Skip to content
Snippets Groups Projects
Verified Commit bbf879a1 authored by Nik | Klampfradler's avatar Nik | Klampfradler
Browse files

[black] Auto-reformat code

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