Skip to content
Commits on Source (106)
......@@ -6,6 +6,39 @@ All notable changes to this project will be documented in this file.
The format is based on `Keep a Changelog`_,
and this project adheres to `Semantic Versioning`_.
`2.8`_ - 2022-03-11
-------------------
Added
~~~~~
* Add iconify icons
* Use identicons where avatars are missing.
* Display personal photos instead of avatars based on a site preference.
* Add an account menu in the top navbar.
* Create a reusable snippet for avatar content.
* Allow to configure if additional field is required
* Allow to configure description of additional fields
* Allow configuring regex for allowed usernames
* [Dev] Support scheduled notifications.
* Implement StaticContentWidget
* Allow to enable password change independently of password reset
Changed
~~~~~~~
* Added a `Retry` button to the server error page
Fixed
~~~~~
* The ``reset password`` button on the login site used to overflow the card on smaller devices.
Deprecated
~~~~~~~~~~
* Legacy material icon font will be removed in AlekSIS-Core 3.0
`2.7.4`_ - 2022-02-09
---------------------
......@@ -750,3 +783,4 @@ Fixed
.. _2.7.2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.7.2
.. _2.7.3: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.7.3
.. _2.7.4: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.7.4
.. _2.8: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.8
......@@ -8,13 +8,13 @@ MENUS = {
{
"name": _("Login"),
"url": settings.LOGIN_URL,
"icon": "lock_open",
"svg_icon": "mdi:login-variant",
"validators": ["menu_generator.validators.is_anonymous"],
},
{
"name": _("Sign up"),
"url": "account_signup",
"icon": "how_to_reg",
"svg_icon": "mdi:account-plus-outline",
"validators": [
"menu_generator.validators.is_anonymous",
("aleksis.core.util.predicates.permission_validator", "core.can_register"),
......@@ -23,7 +23,7 @@ MENUS = {
{
"name": _("Accept invitation"),
"url": "enter_invitation_code",
"icon": "vpn_key",
"svg_icon": "mdi:key-outline",
"validators": [
"menu_generator.validators.is_anonymous",
("aleksis.core.util.predicates.permission_validator", "core.invite_enabled"),
......@@ -32,7 +32,7 @@ MENUS = {
{
"name": _("Dashboard"),
"url": "index",
"icon": "home",
"svg_icon": "mdi:home-outline",
"validators": [
("aleksis.core.util.predicates.permission_validator", "core.view_dashboard_rule")
],
......@@ -40,7 +40,7 @@ MENUS = {
{
"name": _("Notifications"),
"url": "notifications",
"icon": "notifications",
"svg_icon": "mdi:bell-outline",
"badge": unread_notifications_badge,
"validators": [
(
......@@ -52,14 +52,14 @@ MENUS = {
{
"name": _("Account"),
"url": "#",
"icon": "person",
"svg_icon": "mdi:account-outline",
"root": True,
"validators": ["menu_generator.validators.is_authenticated"],
"submenu": [
{
"name": _("Stop impersonation"),
"url": "impersonate-stop",
"icon": "stop",
"svg_icon": "mdi:stop",
"validators": [
"menu_generator.validators.is_authenticated",
"aleksis.core.util.core_helpers.is_impersonate",
......@@ -68,13 +68,13 @@ MENUS = {
{
"name": _("Logout"),
"url": "logout",
"icon": "exit_to_app",
"svg_icon": "mdi:logout-variant",
"validators": ["menu_generator.validators.is_authenticated"],
},
{
"name": _("2FA"),
"url": "two_factor:profile",
"icon": "phonelink_lock",
"svg_icon": "mdi:two-factor-authentication",
"validators": [
"menu_generator.validators.is_authenticated",
],
......@@ -82,7 +82,7 @@ MENUS = {
{
"name": _("Change password"),
"url": "account_change_password",
"icon": "lock",
"svg_icon": "mdi:form-textbox-password",
"validators": [
"menu_generator.validators.is_authenticated",
(
......@@ -94,7 +94,7 @@ MENUS = {
{
"name": _("Me"),
"url": "person",
"icon": "insert_emoticon",
"svg_icon": "mdi:emoticon-outline",
"validators": [
"menu_generator.validators.is_authenticated",
"aleksis.core.util.core_helpers.has_person",
......@@ -103,7 +103,7 @@ MENUS = {
{
"name": _("Preferences"),
"url": "preferences_person",
"icon": "settings",
"svg_icon": "mdi:cog-outline",
"validators": [
"menu_generator.validators.is_authenticated",
"aleksis.core.util.core_helpers.has_person",
......@@ -112,7 +112,7 @@ MENUS = {
{
"name": _("Third-party accounts"),
"url": "socialaccount_connections",
"icon": "public",
"svg_icon": "mdi:earth",
"validators": [
"menu_generator.validators.is_authenticated",
"aleksis.core.util.core_helpers.has_person",
......@@ -121,7 +121,7 @@ MENUS = {
{
"name": _("Authorized applications"),
"url": "oauth2_provider:authorized-token-list",
"icon": "touch_app",
"svg_icon": "mdi:gesture-tap-hold",
"validators": [
"menu_generator.validators.is_authenticated",
"aleksis.core.util.core_helpers.has_person",
......@@ -132,7 +132,7 @@ MENUS = {
{
"name": _("Admin"),
"url": "#",
"icon": "security",
"svg_icon": "mdi:security",
"validators": [
("aleksis.core.util.predicates.permission_validator", "core.view_admin_menu"),
],
......@@ -140,7 +140,7 @@ MENUS = {
{
"name": _("Announcements"),
"url": "announcements",
"icon": "announcement",
"svg_icon": "mdi:message-alert-outline",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
......@@ -151,7 +151,7 @@ MENUS = {
{
"name": _("School terms"),
"url": "school_terms",
"icon": "date_range",
"svg_icon": "mdi:calendar-range-outline",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
......@@ -162,7 +162,7 @@ MENUS = {
{
"name": _("Dashboard widgets"),
"url": "dashboard_widgets",
"icon": "dashboard",
"svg_icon": "mdi:view-dashboard-outline",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
......@@ -173,7 +173,7 @@ MENUS = {
{
"name": _("Data management"),
"url": "data_management",
"icon": "view_list",
"svg_icon": "mdi:chart-donut",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
......@@ -184,7 +184,7 @@ MENUS = {
{
"name": _("System status"),
"url": "system_status",
"icon": "power_settings_new",
"svg_icon": "mdi:power-settings",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
......@@ -195,7 +195,7 @@ MENUS = {
{
"name": _("Configuration"),
"url": "preferences_site",
"icon": "settings",
"svg_icon": "mdi:tune",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
......@@ -206,13 +206,13 @@ MENUS = {
{
"name": _("Data checks"),
"url": "check_data",
"icon": "done_all",
"svg_icon": "mdi:list-status",
"validators": ["menu_generator.validators.is_superuser"],
},
{
"name": _("Manage permissions"),
"url": "manage_user_global_permissions",
"icon": "shield",
"svg_icon": "mdi:shield-outline",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
......@@ -223,7 +223,7 @@ MENUS = {
{
"name": _("Backend Admin"),
"url": "admin:index",
"icon": "settings",
"svg_icon": "mdi:database-cog-outline",
"validators": [
"menu_generator.validators.is_superuser",
],
......@@ -231,7 +231,7 @@ MENUS = {
{
"name": _("OAuth2 Applications"),
"url": "oauth2_applications",
"icon": "touch_app",
"svg_icon": "mdi:gesture-tap-hold",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
......@@ -244,7 +244,7 @@ MENUS = {
{
"name": _("People"),
"url": "#",
"icon": "people",
"svg_icon": "mdi:account-group-outline",
"root": True,
"validators": [
("aleksis.core.util.predicates.permission_validator", "core.view_people_menu_rule")
......@@ -253,7 +253,7 @@ MENUS = {
{
"name": _("Persons"),
"url": "persons",
"icon": "person",
"svg_icon": "mdi:account-outline",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
......@@ -264,7 +264,7 @@ MENUS = {
{
"name": _("Groups"),
"url": "groups",
"icon": "group",
"svg_icon": "mdi:account-multiple-outline",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
......@@ -275,7 +275,7 @@ MENUS = {
{
"name": _("Group types"),
"url": "group_types",
"icon": "category",
"svg_icon": "mdi:shape-outline",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
......@@ -286,7 +286,7 @@ MENUS = {
{
"name": _("Groups and child groups"),
"url": "groups_child_groups",
"icon": "group_add",
"svg_icon": "mdi:account-multiple-plus-outline",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
......@@ -297,7 +297,7 @@ MENUS = {
{
"name": _("Additional fields"),
"url": "additional_fields",
"icon": "style",
"svg_icon": "mdi:palette-swatch-outline",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
......@@ -308,7 +308,7 @@ MENUS = {
{
"name": _("Invite person"),
"url": "invite_person",
"icon": "card_giftcard",
"svg_icon": "mdi:account-plus-outline",
"validators": [
"menu_generator.validators.is_authenticated",
("aleksis.core.util.predicates.permission_validator", "core.can_invite"),
......@@ -329,4 +329,78 @@ MENUS = {
],
},
],
"NAVBAR_ACCOUNT_MENU": [
{
"name": _("Stop impersonation"),
"url": "impersonate-stop",
"icon": "stop",
"validators": [
"menu_generator.validators.is_authenticated",
"aleksis.core.util.core_helpers.is_impersonate",
],
},
{
"name": _("Account"),
"url": "person",
"icon": "person",
"validators": [
"menu_generator.validators.is_authenticated",
"aleksis.core.util.core_helpers.has_person",
],
},
{
"name": _("Preferences"),
"url": "preferences_person",
"icon": "settings",
"validators": [
"menu_generator.validators.is_authenticated",
"aleksis.core.util.core_helpers.has_person",
],
},
{
"name": _("2FA"),
"url": "two_factor:profile",
"icon": "phonelink_lock",
"validators": [
"menu_generator.validators.is_authenticated",
],
},
{
"name": _("Change password"),
"url": "account_change_password",
"icon": "lock",
"validators": [
"menu_generator.validators.is_authenticated",
(
"aleksis.core.util.predicates.permission_validator",
"core.can_change_password",
),
],
},
{
"name": _("Third-party accounts"),
"url": "socialaccount_connections",
"icon": "public",
"validators": [
"menu_generator.validators.is_authenticated",
"aleksis.core.util.core_helpers.has_person",
],
},
{
"name": _("Authorized applications"),
"url": "oauth2_provider:authorized-token-list",
"icon": "touch_app",
"validators": [
"menu_generator.validators.is_authenticated",
"aleksis.core.util.core_helpers.has_person",
],
},
{
"divider": True,
"name": _("Logout"),
"url": "logout",
"icon": "exit_to_app",
"validators": ["menu_generator.validators.is_authenticated"],
},
],
}
# Generated by Django 3.2.12 on 2022-02-18 21:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0035_preference_model_unique'),
]
operations = [
migrations.AddField(
model_name='additionalfield',
name='help_text',
field=models.TextField(blank=True, verbose_name='Help text / description'),
),
migrations.AddField(
model_name='additionalfield',
name='required',
field=models.BooleanField(default=False, verbose_name='Required'),
),
]
# Generated by Django 3.2.12 on 2022-02-23 18:03
import ckeditor.fields
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0036_additionalfields_helptext_required'),
]
operations = [
migrations.CreateModel(
name='StaticContentWidget',
fields=[
('dashboardwidget_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.dashboardwidget')),
('content', ckeditor.fields.RichTextField(verbose_name='Content')),
],
options={
'verbose_name': 'Static content widget',
'verbose_name_plural': 'Static content widgets',
},
bases=('core.dashboardwidget',),
),
]
# Generated by Django 3.2.12 on 2022-02-23 19:33
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('core', '0037_add_static_content_widget'),
]
operations = [
migrations.AddField(
model_name='notification',
name='send_at',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Send notification at'),
preserve_default=False,
),
]
# flake8: noqa: DJ01
import base64
import hmac
from datetime import date, datetime, timedelta
from typing import Any, Iterable, List, Optional, Sequence, Union
......@@ -24,9 +25,12 @@ from django.utils.functional import classproperty
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
import customidenticon
import jsonstore
from cachalot.api import cachalot_disabled
from cache_memoize import cache_memoize
from celery.result import AsyncResult
from ckeditor.fields import RichTextField
from django_celery_results.models import TaskResult
from django_cte import CTEQuerySet, With
from dynamic_preferences.models import PerInstancePreferenceModel
......@@ -314,7 +318,12 @@ class Person(ExtensibleModel):
@property
def initials(self):
return f"{self.first_name[0]}{self.last_name[0]}".upper()
initials = ""
if self.first_name:
initials += self.first_name[0]
if self.last_name:
initials += self.last_name[0]
return initials.upper() or "?"
user_info_tracker = FieldTracker(fields=("first_name", "last_name", "email", "user_id"))
......@@ -334,6 +343,20 @@ class Person(ExtensibleModel):
q = q.union(group.child_groups_recursive)
return q
@property
@cache_memoize(60 * 60)
def identicon_url(self):
identicon = customidenticon.create(self.full_name, border=35)
base64_data = base64.b64encode(identicon).decode("ascii")
return f"data:image/png;base64,{base64_data}"
@property
def avatar_url(self):
if self.avatar:
return self.avatar.url
else:
return self.identicon_url
def save(self, *args, **kwargs):
# Determine all fields that were changed since last load
changed = self.user_info_tracker.changed()
......@@ -435,6 +458,8 @@ class AdditionalField(ExtensibleModel):
field_type = models.CharField(
verbose_name=_("Type of field"), choices=FIELD_CHOICES, max_length=50
)
required = models.BooleanField(verbose_name=_("Required"), default=False)
help_text = models.TextField(verbose_name=_("Help text / description"), blank=True)
def __str__(self) -> str:
return self.title
......@@ -716,6 +741,8 @@ class Notification(ExtensibleModel, TimeStampedModel):
description = models.TextField(max_length=500, verbose_name=_("Description"))
link = models.URLField(blank=True, verbose_name=_("Link"))
send_at = models.DateTimeField(default=timezone.now, verbose_name=_("Send notification at"))
read = models.BooleanField(default=False, verbose_name=_("Read"))
sent = models.BooleanField(default=False, verbose_name=_("Sent"))
......@@ -724,10 +751,14 @@ class Notification(ExtensibleModel, TimeStampedModel):
def save(self, **kwargs):
super().save(**kwargs)
if not self.sent:
send_notification(self.pk, resend=True)
self.sent = True
super().save(**kwargs)
if not self.sent and self.send_at <= timezone.now():
self.send()
super().save(**kwargs)
def send(self, resend: bool = False) -> Optional[AsyncResult]:
"""Send the notification to the recipient."""
if not self.sent or resend:
return send_notification.delay(self.pk, resend=True)
class Meta:
verbose_name = _("Notification")
......@@ -965,6 +996,19 @@ class ExternalLinkWidget(DashboardWidget):
verbose_name_plural = _("External link widgets")
class StaticContentWidget(DashboardWidget):
template = "core/dashboard_widget/static_content_widget.html"
content = RichTextField(verbose_name=_("Content"))
def get_context(self, request):
return {"title": self.title, "content": self.content}
class Meta:
verbose_name = _("Static content widget")
verbose_name_plural = _("Static content widgets")
class DashboardWidgetOrder(ExtensibleModel):
widget = models.ForeignKey(
DashboardWidget, on_delete=models.CASCADE, verbose_name=_("Dashboard widget")
......
......@@ -271,6 +271,14 @@ class AllowPasswordChange(BooleanPreference):
verbose_name = _("Allow users to change their passwords")
@site_preferences_registry.register
class AllowPasswordReset(BooleanPreference):
section = auth
name = "allow_password_reset"
default = True
verbose_name = _("Allow users to reset their passwords")
@site_preferences_registry.register
class SignupEnabled(BooleanPreference):
section = auth
......@@ -279,6 +287,14 @@ class SignupEnabled(BooleanPreference):
verbose_name = _("Enable signup")
@site_preferences_registry.register
class AllowedUsernameRegex(StringPreference):
section = auth
name = "allowed_username_regex"
default = ".+"
verbose_name = _("Regular expression for allowed usernames")
@site_preferences_registry.register
class InviteEnabled(BooleanPreference):
section = auth
......@@ -422,6 +438,16 @@ class PersonChangeNotificationContact(StringPreference):
required = False
@site_preferences_registry.register
class PersonPreferPhoto(BooleanPreference):
"""Preference, whether personal photos should be displayed instead of avatars."""
section = account
name = "person_prefer_photo"
default = False
verbose_name = _("Prefer personal photos over avatars")
@site_preferences_registry.register
class PDFFileExpirationDuration(IntegerPreference):
"""PDF file expiration duration."""
......
......@@ -147,6 +147,7 @@ INSTALLED_APPS = [
"django_filters",
"oauth2_provider",
"rest_framework",
"dj_iconify.apps.DjIconifyConfig",
]
merge_app_settings("INSTALLED_APPS", INSTALLED_APPS, True)
......@@ -362,6 +363,9 @@ ACCOUNT_SIGNUP_EMAIL_ENTER_TWICE = True
# Enforce uniqueness of email addresses
ACCOUNT_UNIQUE_EMAIL = _settings.get("auth.login.registration.unique_email", True)
# Configurable username validators
ACCOUNT_USERNAME_VALIDATORS = "aleksis.core.util.auth_helpers.custom_username_validators"
# Configuration for django-invitations
# Use custom account adapter
......@@ -562,6 +566,8 @@ YARN_INSTALLED_APPS = [
"sortablejs",
"@sentry/tracing",
"luxon",
"@iconify/iconify",
"@iconify/json",
]
merge_app_settings("YARN_INSTALLED_APPS", YARN_INSTALLED_APPS, True)
......@@ -595,6 +601,7 @@ ANY_JS = {
"Sentry": {"js_url": JS_URL + "/@sentry/tracing/build/bundle.tracing.js"},
"cleavejs": {"js_url": JS_URL + "/cleave.js/dist/cleave.min.js"},
"luxon": {"js_url": JS_URL + "/luxon/build/global/luxon.min.js"},
"iconify": {"js_url": JS_URL + "/@iconify/iconify/dist/iconify.min.js"},
}
merge_app_settings("ANY_JS", ANY_JS, True)
......@@ -614,6 +621,8 @@ SASS_PROCESSOR_INCLUDE_DIRS = [
os.path.join(STATIC_ROOT, "public"),
]
ICONIFY_JSON_ROOT = os.path.join(JS_ROOT, "@iconify", "json")
ADMINS = _settings.get(
"contact.admins", [(AUTH_INITIAL_SUPERUSER["username"], AUTH_INITIAL_SUPERUSER["email"])]
)
......
......@@ -121,6 +121,10 @@ $(document).ready(function () {
// Initialize dropdown [MAT]
$('.dropdown-trigger').dropdown();
$('.navbar-dropdown-trigger').dropdown({
"coverTrigger": false,
"constrainWidth": false,
});
// If JS is activated, the language form will be auto-submitted
$('.language-field select').change(function () {
......@@ -188,6 +192,6 @@ $(document).ready(function () {
const channel = new BroadcastChannel("cache-or-not");
channel.addEventListener("message", event => {
if ((event.data) && !($("#cache-alert").length)) {
$("main").prepend('<div id="cache-alert" class="alert warning"><p><i class="material-icons left">warning</i>' + gettext("This page may contain outdated information since there is no internet connection.") + '</p> </div>')
$("main").prepend('<div id="cache-alert" class="alert warning"><p><i class="material-icons iconify left" data-icon="mdi:alert-outline"></i>' + gettext("This page may contain outdated information since there is no internet connection.") + '</p> </div>')
}
});
......@@ -9,11 +9,11 @@ const STYLE_CLASSES = {
};
const ICONS = {
10: 'info',
20: 'info',
25: 'check_circle',
30: 'warning',
40: 'error',
10: 'mdi:information',
20: 'mdi:information',
25: 'mdi:check-circle',
30: 'mdi:alert-outline',
40: 'mdi:alert-octagon-outline',
};
function setProgress(progress) {
......@@ -21,7 +21,7 @@ function setProgress(progress) {
}
function renderMessageBox(level, text) {
return '<div class="alert ' + STYLE_CLASSES[level] + '"><p><i class="material-icons left">' + ICONS[level] + '</i>' + text + '</p></div>';
return '<div class="alert ' + STYLE_CLASSES[level] + '"><p><i class="material-icons iconify left" data-icon="' + ICONS[level] + '"></i>' + text + '</p></div>';
}
function customProgress(progressBarElement, progressBarMessageElement, progress) {
......@@ -44,7 +44,7 @@ function customProgress(progressBarElement, progressBarMessageElement, progress)
function customSuccess(progressBarElement, progressBarMessageElement) {
setProgress(100);
$("#result-alert").addClass("success");
$("#result-icon").text("check_circle");
$("#result-icon").attr("data-icon", "mdi:check-circle-outline");
$("#result-text").text(OPTIONS.success);
$("#result-box").show();
$("#result-button").show();
......@@ -57,7 +57,7 @@ function customSuccess(progressBarElement, progressBarMessageElement) {
function customError(progressBarElement, progressBarMessageElement) {
setProgress(100);
$("#result-alert").addClass("error");
$("#result-icon").text("error");
$("#result-icon").attr("data-icon", "mdi:alert-octagon-outline");
$("#result-text").text(OPTIONS.error);
$("#result-box").show();
}
......
......@@ -77,6 +77,9 @@ header, main, footer {
.materialize-circle {
@extend .circle;
}
.collection .collection-item.avatar > .materialize-circle > .materialize-circle {
left: 0;
}
/**********/
/* HEADER */
......@@ -211,25 +214,6 @@ div#search-results {
}
// Sidenav trigger
header a.sidenav-trigger {
position: absolute;
left: 7.5%;
top: 0;
height: 64px;
font-size: 38px;
float: none;
text-align: center;
color: white;
z-index: 2;
}
// Footer
.footer-icon {
......@@ -445,41 +429,17 @@ th.orderable > a {
height: inherit;
}
th.orderable > a::after {
@extend .material-icons;
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: 'liga';
float: right;
content: "unfold_more";
th.orderable {
background: no-repeat right center;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M12,18.17L8.83,15L7.42,16.41L12,21L16.59,16.41L15.17,15M12,5.83L15.17,9L16.58,7.59L12,3L7.41,7.59L8.83,9L12,5.83Z' /%3E%3C/svg%3E");
}
th.orderable.asc > a {
color: inherit;
&::after {
content: "expand_less";
}
th.orderable.asc {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M7.41,15.41L12,10.83L16.59,15.41L18,14L12,8L6,14L7.41,15.41Z' /%3E%3C/svg%3E");
}
th.orderable.desc > a {
color: inherit;
&::after {
content: "expand_more";
}
th.orderable.desc {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z' /%3E%3C/svg%3E");
}
/*+++++++*/
......@@ -828,8 +788,8 @@ $person-logo-size: 20vh;
& img {
border-radius: 50%;
width: 20vh;
height: 20vh;
width: 100%;
height: 100%;
object-fit: cover;
}
}
......@@ -844,6 +804,64 @@ $person-logo-size: 20vh;
user-select: none;
cursor: default;
border-radius: 50%;
height: unset;
}
.nav-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
> a {
position: static!important;
transform: none!important;
}
& .nav-spacer {
width: 60px;
}
& ul.account-nav {
display: flex;
margin-inline: 7.5px;
& > li > a {
padding: 0 7.5px;
}
}
}
.nav-wrapper .navbar-dropdown-trigger {
cursor: pointer;
height: 100%;
display: grid;
}
.navbar-dropdown-trigger .clip-circle {
margin: auto;
width: $navbar-height*0.75;
height: $navbar-height*0.75;
cursor: pointer;
&.no-image, &.no-image > i.material-icons {
font-size: calc(#{$navbar-height} * 0.75 * 0.5);
color: #6f6f6f;
background: #f2f2f2;
line-height: $navbar-height*0.75;
width: $navbar-height*0.75;
cursor: pointer;
}
}
i.material-icons.new-notification {
position: relative;
&:after {
content: "";
position: absolute;
width: 12px;
height: 12px;
bottom: 27%;
right: -4%;
background-color: $secondary-color;
border-radius: 50%;
}
}
#hero-bg {
......@@ -937,3 +955,22 @@ $person-logo-size: 20vh;
@extend .application-circle;
object-fit: cover;
}
svg.iconify {
@extend i;
}
.btn .iconify.material-icons, .btn-flat .iconify.material-icons{
height: $button-height;
}
// Login Page
.login-card-action {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
row-gap: 0.7rem;
& *:last-child {
grid-column: -2;
text-align: center;
}
}
......@@ -4,6 +4,7 @@ from django.conf import settings
from django.core import management
from .celery import app
from .util.notifications import _send_due_notifications as _send_due_notifications
from .util.notifications import send_notification as _send_notification
......@@ -48,3 +49,9 @@ def clear_oauth_tokens():
from oauth2_provider.models import clear_expired # noqa
return clear_expired()
@app.task(run_every=timedelta(minutes=5))
def send_notifications():
"""Send due notifications to users."""
_send_due_notifications()
......@@ -6,7 +6,7 @@
<div class="container">
<div class="card red">
<div class="card-content white-text">
<i class="material-icons small left">error_outline</i>
<i class="material-icons iconify small left" data-icon="mdi:alert-octagon-outline"></i>
<span class="card-title">
{% if exception %}
{{ exception }}
......
......@@ -6,7 +6,7 @@
<div class="container">
<div class="card red">
<div class="card-content white-text">
<i class="material-icons small left">error_outline</i>
<i class="material-icons iconify small left" data-icon="mdi:alert-octagon-outline"></i>
<span class="card-title">{{ exception }}</span>
<p>
{% blocktrans %}
......
......@@ -6,7 +6,7 @@
<div class="container">
<div class="card red">
<div class="card-content white-text">
<i class="material-icons small left">error_outline</i>
<i class="material-icons iconify small left" data-icon="mdi:alert-octagon-outline"></i>
<span class="card-title">{% trans "Error" %} (500): {% blocktrans %}An unexpected error has
occured.{% endblocktrans %}</span>
<p>
......@@ -16,6 +16,10 @@
{% endblocktrans %}
</p>
{% include "core/partials/admins_list.html" %}
<a href="javascript:window.location.reload()" class="btn green waves-effect waves-light">
<i class="material-icons left">refresh</i>
{% trans "Retry" %}
</a>
</div>
</div>
</div>
......
......@@ -6,7 +6,7 @@
<div class="container">
<div class="card red">
<div class="card-content white-text">
<i class="material-icons small left">error_outline</i>
<i class="material-icons iconify small left" data-icon="mdi:alert-octagon-outline"></i>
<span class="card-title">{% blocktrans %}The maintenance mode is currently enabled. Please try again
later.{% endblocktrans %}</span>
<p>
......
......@@ -10,7 +10,7 @@
<div class="card red">
<div class="card-content white-text">
<div class="card-title">
<i class="material-icons small left">error_outline</i>
<i class="material-icons iconify small left" data-icon="mdi:alert-octagon-outline"></i>
{% blocktrans %}Account inactive.{% endblocktrans %}
</div>
<p>
......
......@@ -15,13 +15,13 @@
{% csrf_token %}
{% form form=form %}{% endform %}
{% trans "Confirm" as caption %}
{% include "core/partials/save_button.html" with caption=caption icon="how_to_reg" %}
{% include "core/partials/save_button.html" with caption=caption icon="mdi:account-plus-outline" %}
</form>
{% else %}
{% url "account_email" as email_url %}
<div class="alert warning">
<p>
<i class="material-icons left">warning</i>
<i class="material-icons iconify left" data-icon="mdi:alert-outline"></i>
{% blocktrans %}This e-mail confirmation link expired or is invalid. Please <a href="{{ email_url }}">issue a new e-mail confirmation request</a>.{% endblocktrans %}
</p>
</div>
......
......@@ -8,7 +8,7 @@
{% block content %}
<div class="alert warning">
<p>
<i class="material-icons left">warning</i>
<i class="material-icons iconify left" data-icon="mdi:alert-outline"></i>
{% trans "Forgot your current password? Click here to reset it:" %} <a href="{% url 'account_reset_password' %}">{% trans "Forgot Password?" %}</a>.
</p>
</div>
......@@ -17,7 +17,7 @@
{% csrf_token %}
{% form form=form %}{% endform %}
{% trans "Change password" as caption %}
{% include "core/partials/save_button.html" with caption=caption icon="priority_high" %}
{% include "core/partials/save_button.html" with caption=caption icon="mdi:exclamation" %}
</form>
{% endblock %}
......@@ -9,7 +9,7 @@
<div class="container">
<div class="card red">
<div class="card-content white-text">
<div class="material-icons small left">error_outline</div>
<div class="material-icons iconify small left" data-icon="mdi:alert-octagon-outline"></div>
<span class="card-title">{% blocktrans %}Changing of password disabled.{% endblocktrans %}</span>
<p>
{% blocktrans %}
......