mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-12-06 21:01:42 +01:00
Merge branch 'hide-menu' into 'master'
Draft: [ADD] User setting to keep the sidebar menu minimized See merge request allianceauth/allianceauth!1769
This commit is contained in:
commit
23dd987892
@ -52,4 +52,10 @@ class UserSettingsMiddleware(MiddlewareMixin):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
|
||||||
|
# Minimize Menu
|
||||||
|
try:
|
||||||
|
request.session["MINIMIZE_SIDEBAR"] = request.user.profile.minimize_sidebar
|
||||||
|
except Exception as e:
|
||||||
|
pass # We don't care that an anonymous user has no profile (not logged in)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 4.2.25 on 2025-10-14 22:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentication", "0024_alter_userprofile_language"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="userprofile",
|
||||||
|
name="minimize_sidebar",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Keep the sidebar menu minimized",
|
||||||
|
verbose_name="Minimize Sidebar Menu",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -97,7 +97,8 @@ class UserProfile(models.Model):
|
|||||||
on_delete=models.SET_DEFAULT,
|
on_delete=models.SET_DEFAULT,
|
||||||
default=get_guest_state_pk)
|
default=get_guest_state_pk)
|
||||||
language = models.CharField(
|
language = models.CharField(
|
||||||
_("Language"), max_length=10,
|
_("Language"),
|
||||||
|
max_length=10,
|
||||||
choices=Language.choices,
|
choices=Language.choices,
|
||||||
blank=True,
|
blank=True,
|
||||||
default='')
|
default='')
|
||||||
@ -112,6 +113,12 @@ class UserProfile(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
help_text="Bootstrap 5 Themes from https://bootswatch.com/ or Community Apps"
|
help_text="Bootstrap 5 Themes from https://bootswatch.com/ or Community Apps"
|
||||||
)
|
)
|
||||||
|
minimize_sidebar = models.BooleanField(
|
||||||
|
_("Minimize Sidebar Menu"),
|
||||||
|
default=False,
|
||||||
|
help_text=_("Keep the sidebar menu minimized")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def assign_state(self, state=None, commit=True):
|
def assign_state(self, state=None, commit=True):
|
||||||
if not state:
|
if not state:
|
||||||
|
|||||||
@ -88,6 +88,7 @@ class TestUserSettingsMiddlewareLoginFlow(TestCase):
|
|||||||
self.request.LANGUAGE_CODE = 'en'
|
self.request.LANGUAGE_CODE = 'en'
|
||||||
self.request.user.profile.language = 'de'
|
self.request.user.profile.language = 'de'
|
||||||
self.request.user.profile.night_mode = True
|
self.request.user.profile.night_mode = True
|
||||||
|
self.request.user.profile.minimize_sidebar = False
|
||||||
self.request.user.is_anonymous = False
|
self.request.user.is_anonymous = False
|
||||||
self.response = Mock()
|
self.response = Mock()
|
||||||
self.response.content = 'hello world'
|
self.response.content = 'hello world'
|
||||||
@ -173,3 +174,26 @@ class TestUserSettingsMiddlewareLoginFlow(TestCase):
|
|||||||
self.response
|
self.response
|
||||||
)
|
)
|
||||||
self.assertEqual(self.request.session["NIGHT_MODE"], True)
|
self.assertEqual(self.request.session["NIGHT_MODE"], True)
|
||||||
|
|
||||||
|
def test_middleware_set_mimimize_sidebar(self):
|
||||||
|
"""
|
||||||
|
tests the middleware will always set minimize_sidebar to False (default)
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self.middleware.process_response(
|
||||||
|
self.request,
|
||||||
|
self.response
|
||||||
|
)
|
||||||
|
self.assertEqual(self.request.session["MINIMIZE_SIDEBAR"], False)
|
||||||
|
|
||||||
|
def test_middleware_minimize_sidebar_when_set(self):
|
||||||
|
"""
|
||||||
|
tests the middleware will set mimimize_sidebar to True from DB
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.request.user.profile.minimize_sidebar = True
|
||||||
|
response = self.middleware.process_response(
|
||||||
|
self.request,
|
||||||
|
self.response
|
||||||
|
)
|
||||||
|
self.assertEqual(self.request.session["MINIMIZE_SIDEBAR"], True)
|
||||||
|
|||||||
@ -72,6 +72,31 @@
|
|||||||
|
|
||||||
{% theme_select %}
|
{% theme_select %}
|
||||||
|
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><h6 class="dropdown-header">{% translate "Sidebar" %}</h6></li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<form class="dropdown-item" action="{% url 'minimize_sidebar' %}?next={{ request.path|urlencode }}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
id="toggle-sidebar"
|
||||||
|
onchange="this.form.submit()"
|
||||||
|
{% if request.session.MINIMIZE_SIDEBAR %}checked{% endif %}
|
||||||
|
>
|
||||||
|
<label class="form-check-label" for="toggle-sidebar">
|
||||||
|
{% translate "Minimize Sidebar" %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if user.is_superuser %}
|
{% if user.is_superuser %}
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><h6 class="dropdown-header">{% translate "Super User" %}</h6></li>
|
<li><h6 class="dropdown-header">{% translate "Super User" %}</h6></li>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
{% load menu_menu_items %}
|
{% load menu_menu_items %}
|
||||||
|
|
||||||
<div class="col-auto px-0">
|
<div class="col-auto px-0">
|
||||||
<div class="collapse collapse-horizontal" tabindex="-1" id="sidebar">
|
<div class="collapse collapse-horizontal {% if user.is_authenticated and not request.is_mobile_device and not request.session.MINIMIZE_SIDEBAR %}show{% endif %}" tabindex="-1" id="sidebar">
|
||||||
<div>
|
<div>
|
||||||
<div class="nav-padding navbar-dark text-bg-dark px-0 d-flex flex-column overflow-hidden vh-100 {% if not user.is_authenticated %}position-relative{% endif %}">
|
<div class="nav-padding navbar-dark text-bg-dark px-0 d-flex flex-column overflow-hidden vh-100 {% if not user.is_authenticated %}position-relative{% endif %}">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
|
|||||||
65
allianceauth/middleware.py
Normal file
65
allianceauth/middleware.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
Alliance Auth Middleware
|
||||||
|
"""
|
||||||
|
|
||||||
|
from user_agents import parse
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceDetectionMiddleware:
|
||||||
|
"""
|
||||||
|
Middleware to detect the type of device making the request.
|
||||||
|
Sets flags on the request object for easy access in views and templates.
|
||||||
|
|
||||||
|
Flags include:
|
||||||
|
- is_mobile: True if the device is a mobile phone.
|
||||||
|
- is_tablet: True if the device is a tablet.
|
||||||
|
- is_mobile_device: True if the device is either a mobile phone or a tablet.
|
||||||
|
- is_touch_capable: True if the device has touch capabilities.
|
||||||
|
- is_pc: True if the device is a desktop or laptop computer.
|
||||||
|
- is_bot: True if the device is identified as a bot or crawler.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
"""
|
||||||
|
Initialize the middleware with the get_response callable.
|
||||||
|
|
||||||
|
:param get_response:
|
||||||
|
:type get_response:
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
"""
|
||||||
|
Process the incoming request to determine if it's from a mobile device.
|
||||||
|
|
||||||
|
This method is called when the middleware is invoked. It inspects the
|
||||||
|
`user-agent` header of the incoming HTTP request to determine the type
|
||||||
|
of client making the request (e.g., mobile, tablet, PC, bot, etc.).
|
||||||
|
Flags are set on the `request` object to indicate the client type.
|
||||||
|
|
||||||
|
:param request: The HTTP request object.
|
||||||
|
:type request: HttpRequest
|
||||||
|
:return: The HTTP response object after processing the request.
|
||||||
|
:rtype: HttpResponse
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Retrieve the user-agent string from the request headers
|
||||||
|
user_agent_string = request.headers.get("user-agent", "")
|
||||||
|
|
||||||
|
# Parse the user-agent string to extract client information
|
||||||
|
user_agent = parse(user_agent_string)
|
||||||
|
|
||||||
|
# Set flags on the request object based on the client type
|
||||||
|
request.is_mobile = user_agent.is_mobile # True if the client is a mobile phone
|
||||||
|
request.is_tablet = user_agent.is_tablet # True if the client is a tablet
|
||||||
|
request.is_mobile_device = user_agent.is_mobile or user_agent.is_tablet # True if mobile phone or tablet
|
||||||
|
request.is_touch_capable = user_agent.is_touch_capable # True if the client supports touch input
|
||||||
|
request.is_pc = user_agent.is_pc # True if the client is a PC
|
||||||
|
request.is_bot = user_agent.is_bot # True if the client is a bot
|
||||||
|
|
||||||
|
# Pass the request to the next middleware or view and get the response
|
||||||
|
response = self.get_response(request)
|
||||||
|
|
||||||
|
# Return the processed response
|
||||||
|
return response
|
||||||
@ -88,6 +88,7 @@ MIDDLEWARE = [
|
|||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
"allianceauth.authentication.middleware.UserSettingsMiddleware",
|
"allianceauth.authentication.middleware.UserSettingsMiddleware",
|
||||||
|
"allianceauth.middleware.DeviceDetectionMiddleware",
|
||||||
"django.middleware.locale.LocaleMiddleware",
|
"django.middleware.locale.LocaleMiddleware",
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
|||||||
@ -1,23 +1,6 @@
|
|||||||
$(document).ready(() => {
|
$(document).ready(() => {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const sidebar = document.getElementById('sidebar');
|
|
||||||
const sidebarKey = `sidebar_${sidebar.id}`;
|
|
||||||
|
|
||||||
sidebar.addEventListener('shown.bs.collapse', (event) => {
|
|
||||||
if (event.target.id === sidebar.id) {
|
|
||||||
localStorage.removeItem(sidebarKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
sidebar.addEventListener('hidden.bs.collapse', (event) => {
|
|
||||||
if (event.target.id === sidebar.id) {
|
|
||||||
localStorage.setItem(sidebarKey, 'closed');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
sidebar.classList.toggle('show', localStorage.getItem(sidebarKey) !== 'closed');
|
|
||||||
|
|
||||||
const activeChildMenuItem = document.querySelector('ul#sidebar-menu ul.collapse a.active');
|
const activeChildMenuItem = document.querySelector('ul#sidebar-menu ul.collapse a.active');
|
||||||
|
|
||||||
if (activeChildMenuItem) {
|
if (activeChildMenuItem) {
|
||||||
|
|||||||
@ -80,7 +80,10 @@ urlpatterns = [
|
|||||||
path('night/', views.NightModeRedirectView.as_view(), name='nightmode'),
|
path('night/', views.NightModeRedirectView.as_view(), name='nightmode'),
|
||||||
|
|
||||||
# Theme Change
|
# Theme Change
|
||||||
path('theme/', views.ThemeRedirectView.as_view(), name='theme')
|
path('theme/', views.ThemeRedirectView.as_view(), name='theme'),
|
||||||
|
|
||||||
|
# Minimize Menu
|
||||||
|
path('minimize-sidebar/', views.MinimizeSidebarRedirectView.as_view(), name='minimize_sidebar')
|
||||||
]
|
]
|
||||||
|
|
||||||
url_hooks = get_hooks("url_hook")
|
url_hooks = get_hooks("url_hook")
|
||||||
|
|||||||
@ -48,6 +48,29 @@ class ThemeRedirectView(View):
|
|||||||
|
|
||||||
return HttpResponseRedirect(request.GET.get("next", "/"))
|
return HttpResponseRedirect(request.GET.get("next", "/"))
|
||||||
|
|
||||||
|
class MinimizeSidebarRedirectView(View):
|
||||||
|
SESSION_VAR = "MINIMIZE_SIDEBAR"
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
request.session[self.SESSION_VAR] = not self.minimize_sidebar_state(request)
|
||||||
|
if not request.user.is_anonymous:
|
||||||
|
try:
|
||||||
|
request.user.profile.minimize_sidebar = request.session[self.SESSION_VAR]
|
||||||
|
request.user.profile.save()
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
|
||||||
|
return HttpResponseRedirect(request.GET.get("next", "/"))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def minimize_sidebar_state(cls, request):
|
||||||
|
try:
|
||||||
|
return request.session.get(cls.SESSION_VAR, False)
|
||||||
|
except AttributeError:
|
||||||
|
# Session is middleware
|
||||||
|
# Sometimes request wont have a session attribute
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# TODO: error views should be renamed to a proper function name when possible
|
# TODO: error views should be renamed to a proper function name when possible
|
||||||
|
|
||||||
|
|||||||
@ -61,11 +61,14 @@ dependencies = [
|
|||||||
"passlib",
|
"passlib",
|
||||||
"pydiscourse",
|
"pydiscourse",
|
||||||
"python-slugify>=1.2",
|
"python-slugify>=1.2",
|
||||||
|
"pyyaml",
|
||||||
"redis>=4",
|
"redis>=4",
|
||||||
"requests>=2.9.1",
|
"requests>=2.9.1",
|
||||||
"requests-oauthlib",
|
"requests-oauthlib",
|
||||||
"semantic-version",
|
"semantic-version",
|
||||||
"slixmpp<1.9",
|
"slixmpp<1.9",
|
||||||
|
"ua-parser",
|
||||||
|
"user-agents",
|
||||||
]
|
]
|
||||||
optional-dependencies.docs = [
|
optional-dependencies.docs = [
|
||||||
"myst-parser",
|
"myst-parser",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user