mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-07-09 12:30:15 +02:00
Persistent User Settings
This commit is contained in:
parent
e54f72091f
commit
1aad3e4512
@ -13,8 +13,12 @@ from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
|
||||
from allianceauth.authentication.models import State, get_guest_state,\
|
||||
CharacterOwnership, UserProfile, OwnershipRecord
|
||||
from allianceauth.authentication.models import (
|
||||
State,
|
||||
get_guest_state,
|
||||
CharacterOwnership,
|
||||
UserProfile,
|
||||
OwnershipRecord)
|
||||
from allianceauth.hooks import get_hooks
|
||||
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\
|
||||
EveAllianceInfo, EveFactionInfo
|
||||
|
45
allianceauth/authentication/middleware.py
Normal file
45
allianceauth/authentication/middleware.py
Normal file
@ -0,0 +1,45 @@
|
||||
from django.conf import settings
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UserSettingsMiddleware(MiddlewareMixin):
|
||||
def process_response(self, request, response):
|
||||
"""Django Middleware: User Settings."""
|
||||
|
||||
# Intercept the built in django /setlang/ view and also save it to Database.
|
||||
# Note the annoymous user check, only logged in users will ever hit the DB here
|
||||
if request.path == '/i18n/setlang/' and not request.user.is_anonymous:
|
||||
try:
|
||||
request.user.profile.language = request.POST['language']
|
||||
request.user.profile.save()
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
# Only act during the login flow, _after_ user is activated (step 2: post-sso)
|
||||
elif request.path == '/sso/login' and not request.user.is_anonymous:
|
||||
# Set the Language Cookie, if it doesnt match the DB
|
||||
# Null = hasnt been set by the user ever, dont act.
|
||||
try:
|
||||
if request.user.profile.language != request.LANGUAGE_CODE and request.user.profile.language is not None:
|
||||
response.set_cookie(key=settings.LANGUAGE_COOKIE_NAME,
|
||||
value=request.user.profile.language,
|
||||
max_age=settings.LANGUAGE_COOKIE_AGE)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
# Set our Night mode flag from the DB
|
||||
# Null = hasnt been set by the user ever, dont act.
|
||||
#
|
||||
# Night mode intercept is not needed in this middleware.
|
||||
# is saved direct to DB in NightModeRedirectView
|
||||
try:
|
||||
if request.user.profile.night_mode is not None:
|
||||
request.session["NIGHT_MODE"] = request.user.profile.night_mode
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
return response
|
@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.0.2 on 2022-02-26 03:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0019_merge_20211026_0919'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='language',
|
||||
field=models.CharField(blank=True, choices=[('en', 'English'), ('de', 'German'), ('es', 'Spanish'), ('zh-hans', 'Chinese Simplified'), ('ru', 'Russian'), ('ko', 'Korean'), ('fr', 'French'), ('ja', 'Japanese'), ('it', 'Italian')], default='', max_length=10, verbose_name='Language'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='night_mode',
|
||||
field=models.BooleanField(blank=True, null=True, verbose_name='Night Mode'),
|
||||
),
|
||||
]
|
@ -5,6 +5,7 @@ from django.db import models, transaction
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo
|
||||
from allianceauth.notifications import notify
|
||||
from django.conf import settings
|
||||
|
||||
from .managers import CharacterOwnershipManager, StateManager
|
||||
|
||||
@ -62,9 +63,39 @@ class UserProfile(models.Model):
|
||||
class Meta:
|
||||
default_permissions = ('change',)
|
||||
|
||||
user = models.OneToOneField(User, related_name='profile', on_delete=models.CASCADE)
|
||||
main_character = models.OneToOneField(EveCharacter, blank=True, null=True, on_delete=models.SET_NULL)
|
||||
state = models.ForeignKey(State, on_delete=models.SET_DEFAULT, default=get_guest_state_pk)
|
||||
user = models.OneToOneField(
|
||||
User,
|
||||
related_name='profile',
|
||||
on_delete=models.CASCADE)
|
||||
main_character = models.OneToOneField(
|
||||
EveCharacter,
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL)
|
||||
state = models.ForeignKey(
|
||||
State,
|
||||
on_delete=models.SET_DEFAULT,
|
||||
default=get_guest_state_pk)
|
||||
LANGUAGE_CHOICES = [
|
||||
('en', _('English')),
|
||||
('de', _('German')),
|
||||
('es', _('Spanish')),
|
||||
('zh-hans', _('Chinese Simplified')),
|
||||
('ru', _('Russian')),
|
||||
('ko', _('Korean')),
|
||||
('fr', _('French')),
|
||||
('ja', _('Japanese')),
|
||||
('it', _('Italian')),
|
||||
]
|
||||
language = models.CharField(
|
||||
_("Language"), max_length=10,
|
||||
choices=LANGUAGE_CHOICES,
|
||||
blank=True,
|
||||
default='')
|
||||
night_mode = models.BooleanField(
|
||||
_("Night Mode"),
|
||||
blank=True,
|
||||
null=True)
|
||||
|
||||
def assign_state(self, state=None, commit=True):
|
||||
if not state:
|
||||
@ -93,8 +124,6 @@ class UserProfile(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return str(self.user)
|
||||
|
||||
|
||||
class CharacterOwnership(models.Model):
|
||||
class Meta:
|
||||
default_permissions = ('change', 'delete')
|
||||
|
@ -1,6 +1,11 @@
|
||||
import logging
|
||||
|
||||
from .models import CharacterOwnership, UserProfile, get_guest_state, State, OwnershipRecord
|
||||
from .models import (
|
||||
CharacterOwnership,
|
||||
UserProfile,
|
||||
get_guest_state,
|
||||
State,
|
||||
OwnershipRecord)
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Q
|
||||
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
|
||||
@ -71,7 +76,7 @@ def reassess_on_profile_save(sender, instance, created, *args, **kwargs):
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_required_models(sender, instance, created, *args, **kwargs):
|
||||
# ensure all users have a model
|
||||
# ensure all users have our Sub-Models
|
||||
if created:
|
||||
logger.debug(f'User {instance} created. Creating default UserProfile.')
|
||||
UserProfile.objects.get_or_create(user=instance)
|
||||
|
@ -1,4 +1,17 @@
|
||||
from django.db.models.signals import (
|
||||
m2m_changed,
|
||||
post_save,
|
||||
pre_delete,
|
||||
pre_save
|
||||
)
|
||||
from django.urls import reverse
|
||||
from unittest import mock
|
||||
|
||||
MODULE_PATH = 'allianceauth.authentication'
|
||||
|
||||
|
||||
def patch(target, *args, **kwargs):
|
||||
return mock.patch(f'{MODULE_PATH}{target}', *args, **kwargs)
|
||||
|
||||
|
||||
def get_admin_change_view_url(obj: object) -> str:
|
||||
|
175
allianceauth/authentication/tests/test_middleware.py
Normal file
175
allianceauth/authentication/tests/test_middleware.py
Normal file
@ -0,0 +1,175 @@
|
||||
from unittest import mock
|
||||
from allianceauth.authentication.middleware import UserSettingsMiddleware
|
||||
from unittest.mock import Mock
|
||||
from django.http import HttpResponse
|
||||
|
||||
from django.test.testcases import TestCase
|
||||
|
||||
|
||||
class TestUserSettingsMiddlewareSaveLang(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.middleware = UserSettingsMiddleware(HttpResponse)
|
||||
self.request = Mock()
|
||||
self.request.headers = {
|
||||
"User-Agent": "AUTOMATED TEST"
|
||||
}
|
||||
self.request.path = '/i18n/setlang/'
|
||||
self.request.POST = {
|
||||
'language': 'fr'
|
||||
}
|
||||
self.request.user.profile.language = 'de'
|
||||
self.request.user.is_anonymous = False
|
||||
self.response = Mock()
|
||||
self.response.content = 'hello world'
|
||||
|
||||
def test_middleware_passthrough(self):
|
||||
"""
|
||||
Simply tests the middleware runs cleanly
|
||||
"""
|
||||
response = self.middleware.process_response(
|
||||
self.request,
|
||||
self.response
|
||||
)
|
||||
self.assertEqual(self.response, response)
|
||||
|
||||
def test_middleware_save_language_false_anonymous(self):
|
||||
"""
|
||||
Ensures the middleware wont change the usersettings
|
||||
of a non-existent (anonymous) user
|
||||
"""
|
||||
self.request.user.is_anonymous = True
|
||||
response = self.middleware.process_response(
|
||||
self.request,
|
||||
self.response
|
||||
)
|
||||
self.assertEqual(self.request.user.profile.language, 'de')
|
||||
self.assertFalse(self.request.user.profile.save.called)
|
||||
self.assertEqual(self.request.user.profile.save.call_count, 0)
|
||||
|
||||
def test_middleware_save_language_new(self):
|
||||
"""
|
||||
does the middleware change a language not set in the DB
|
||||
"""
|
||||
self.request.user.profile.language = None
|
||||
response = self.middleware.process_response(
|
||||
self.request,
|
||||
self.response
|
||||
)
|
||||
self.assertEqual(self.request.user.profile.language, 'fr')
|
||||
self.assertTrue(self.request.user.profile.save.called)
|
||||
self.assertEqual(self.request.user.profile.save.call_count, 1)
|
||||
|
||||
def test_middleware_save_language_changed(self):
|
||||
"""
|
||||
Tests the middleware will change a language setting
|
||||
"""
|
||||
response = self.middleware.process_response(
|
||||
self.request,
|
||||
self.response
|
||||
)
|
||||
self.assertEqual(self.request.user.profile.language, 'fr')
|
||||
self.assertTrue(self.request.user.profile.save.called)
|
||||
self.assertEqual(self.request.user.profile.save.call_count, 1)
|
||||
|
||||
|
||||
class TestUserSettingsMiddlewareLoginFlow(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.middleware = UserSettingsMiddleware(HttpResponse)
|
||||
self.request = Mock()
|
||||
self.request.headers = {
|
||||
"User-Agent": "AUTOMATED TEST"
|
||||
}
|
||||
self.request.path = '/sso/login'
|
||||
self.request.session = {
|
||||
'NIGHT_MODE': False
|
||||
}
|
||||
self.request.LANGUAGE_CODE = 'en'
|
||||
self.request.user.profile.language = 'de'
|
||||
self.request.user.profile.night_mode = True
|
||||
self.request.user.is_anonymous = False
|
||||
self.response = Mock()
|
||||
self.response.content = 'hello world'
|
||||
|
||||
def test_middleware_passthrough(self):
|
||||
"""
|
||||
Simply tests the middleware runs cleanly
|
||||
"""
|
||||
middleware_response = self.middleware.process_response(
|
||||
self.request,
|
||||
self.response
|
||||
)
|
||||
self.assertEqual(self.response, middleware_response)
|
||||
|
||||
def test_middleware_sets_language_cookie_true_no_cookie(self):
|
||||
"""
|
||||
tests the middleware will set a cookie, while none is set
|
||||
"""
|
||||
self.request.LANGUAGE_CODE = None
|
||||
middleware_response = self.middleware.process_response(
|
||||
self.request,
|
||||
self.response
|
||||
)
|
||||
self.assertTrue(middleware_response.set_cookie.called)
|
||||
self.assertEqual(middleware_response.set_cookie.call_count, 1)
|
||||
args, kwargs = middleware_response.set_cookie.call_args
|
||||
self.assertEqual(kwargs['value'], 'de')
|
||||
|
||||
def test_middleware_sets_language_cookie_true_wrong_cookie(self):
|
||||
"""
|
||||
tests the middleware will set a cookie, while a different value is set
|
||||
"""
|
||||
middleware_response = self.middleware.process_response(
|
||||
self.request,
|
||||
self.response
|
||||
)
|
||||
self.assertTrue(middleware_response.set_cookie.called)
|
||||
self.assertEqual(middleware_response.set_cookie.call_count, 1)
|
||||
args, kwargs = middleware_response.set_cookie.call_args
|
||||
self.assertEqual(kwargs['value'], 'de')
|
||||
|
||||
def test_middleware_sets_language_cookie_false_anonymous(self):
|
||||
"""
|
||||
ensures the middleware wont set a value for a non existent user (anonymous)
|
||||
"""
|
||||
self.request.user.is_anonymous = True
|
||||
middleware_response = self.middleware.process_response(
|
||||
self.request,
|
||||
self.response
|
||||
)
|
||||
self.assertFalse = middleware_response.set_cookie.called
|
||||
self.assertEqual(middleware_response.set_cookie.call_count, 0)
|
||||
|
||||
def test_middleware_sets_language_cookie_false_already_set(self):
|
||||
"""
|
||||
tests the middleware skips setting the cookie, if its already set correctly
|
||||
"""
|
||||
self.request.user.profile.language = 'en'
|
||||
middleware_response = self.middleware.process_response(
|
||||
self.request,
|
||||
self.response
|
||||
)
|
||||
self.assertFalse = middleware_response.set_cookie.called
|
||||
self.assertEqual(middleware_response.set_cookie.call_count, 0)
|
||||
|
||||
def test_middleware_sets_night_mode_not_set(self):
|
||||
"""
|
||||
tests the middleware will set night_mode if not set
|
||||
"""
|
||||
self.request.session = {}
|
||||
response = self.middleware.process_response(
|
||||
self.request,
|
||||
self.response
|
||||
)
|
||||
self.assertEqual(self.request.session["NIGHT_MODE"], True)
|
||||
|
||||
def test_middleware_sets_night_mode_set(self):
|
||||
"""
|
||||
tests the middleware will set night_mode if set.
|
||||
"""
|
||||
response = self.middleware.process_response(
|
||||
self.request,
|
||||
self.response
|
||||
)
|
||||
self.assertEqual(self.request.session["NIGHT_MODE"], True)
|
94
allianceauth/authentication/tests/test_signals.py
Normal file
94
allianceauth/authentication/tests/test_signals.py
Normal file
@ -0,0 +1,94 @@
|
||||
from allianceauth.authentication.models import User, UserProfile
|
||||
from allianceauth.eveonline.models import (
|
||||
EveCharacter,
|
||||
EveCorporationInfo,
|
||||
EveAllianceInfo
|
||||
)
|
||||
from django.db.models.signals import (
|
||||
pre_save,
|
||||
post_save,
|
||||
pre_delete,
|
||||
m2m_changed
|
||||
)
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
from django.test.testcases import TestCase
|
||||
from unittest.mock import Mock
|
||||
from . import patch
|
||||
|
||||
|
||||
class TestUserProfileSignals(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
state = AuthUtils.get_member_state()
|
||||
|
||||
self.char = EveCharacter.objects.create(
|
||||
character_id='1234',
|
||||
character_name='test character',
|
||||
corporation_id='2345',
|
||||
corporation_name='test corp',
|
||||
corporation_ticker='tickr',
|
||||
alliance_id='3456',
|
||||
alliance_name='alliance name',
|
||||
)
|
||||
|
||||
self.alliance = EveAllianceInfo.objects.create(
|
||||
alliance_id='3456',
|
||||
alliance_name='alliance name',
|
||||
alliance_ticker='TIKR',
|
||||
executor_corp_id='2345',
|
||||
)
|
||||
|
||||
self.corp = EveCorporationInfo.objects.create(
|
||||
corporation_id='2345',
|
||||
corporation_name='corp name',
|
||||
corporation_ticker='TIKK',
|
||||
member_count=10,
|
||||
alliance=self.alliance,
|
||||
)
|
||||
|
||||
state.member_alliances.add(self.alliance)
|
||||
state.member_corporations.add(self.corp)
|
||||
|
||||
self.member = AuthUtils.create_user('test user')
|
||||
self.member.profile.main_character = self.char
|
||||
self.member.profile.save()
|
||||
|
||||
@patch('.signals.create_required_models')
|
||||
def test_create_required_models_triggered_true(
|
||||
self, create_required_models):
|
||||
"""
|
||||
Create a User object here,
|
||||
to generate UserProfile models
|
||||
"""
|
||||
post_save.connect(create_required_models, sender=User)
|
||||
AuthUtils.create_user('test_create_required_models_triggered')
|
||||
self.assertTrue = create_required_models.called
|
||||
self.assertEqual(create_required_models.call_count, 1)
|
||||
|
||||
user = User.objects.get(username='test_create_required_models_triggered')
|
||||
self.assertIsNot(UserProfile.objects.get(user=user), False)
|
||||
|
||||
@patch('.signals.create_required_models')
|
||||
def test_create_required_models_triggered_false(
|
||||
self, create_required_models):
|
||||
"""
|
||||
Only call a User object Update here,
|
||||
which does not need to generate UserProfile models
|
||||
"""
|
||||
post_save.connect(create_required_models, sender=User)
|
||||
char = EveCharacter.objects.create(
|
||||
character_id='1266',
|
||||
character_name='test character2',
|
||||
corporation_id='2345',
|
||||
corporation_name='test corp',
|
||||
corporation_ticker='tickr',
|
||||
alliance_id='3456',
|
||||
alliance_name='alliance name',
|
||||
)
|
||||
self.member.profile.main_character = char
|
||||
self.member.profile.save()
|
||||
|
||||
self.assertTrue = create_required_models.called
|
||||
self.assertEqual(create_required_models.call_count, 0)
|
||||
self.assertIsNot(UserProfile.objects.get(user=self.member), False)
|
@ -68,6 +68,7 @@ BASE_DIR = os.path.dirname(PROJECT_DIR)
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'allianceauth.authentication.middleware.UserSettingsMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
|
@ -3,12 +3,23 @@ from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import redirect
|
||||
from django.contrib import messages
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NightModeRedirectView(View):
|
||||
SESSION_VAR = "NIGHT_MODE"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
request.session[self.SESSION_VAR] = not self.night_mode_state(request)
|
||||
if not request.user.is_anonymous:
|
||||
try:
|
||||
request.user.profile.night_mode = request.session[self.SESSION_VAR]
|
||||
request.user.profile.save()
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
return HttpResponseRedirect(request.GET.get("next", "/"))
|
||||
|
||||
@classmethod
|
||||
|
Loading…
x
Reference in New Issue
Block a user