Persistent User Settings

This commit is contained in:
Ariel Rin 2022-02-26 06:19:38 +00:00
parent e54f72091f
commit 1aad3e4512
11 changed files with 410 additions and 10 deletions

View File

@ -13,8 +13,12 @@ from django.utils.html import format_html
from django.urls import reverse from django.urls import reverse
from django.utils.text import slugify from django.utils.text import slugify
from allianceauth.authentication.models import State, get_guest_state,\ from allianceauth.authentication.models import (
CharacterOwnership, UserProfile, OwnershipRecord State,
get_guest_state,
CharacterOwnership,
UserProfile,
OwnershipRecord)
from allianceauth.hooks import get_hooks from allianceauth.hooks import get_hooks
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\ from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\
EveAllianceInfo, EveFactionInfo EveAllianceInfo, EveFactionInfo

View 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

View File

@ -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'),
),
]

View File

@ -5,6 +5,7 @@ from django.db import models, transaction
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo
from allianceauth.notifications import notify from allianceauth.notifications import notify
from django.conf import settings
from .managers import CharacterOwnershipManager, StateManager from .managers import CharacterOwnershipManager, StateManager
@ -62,9 +63,39 @@ class UserProfile(models.Model):
class Meta: class Meta:
default_permissions = ('change',) default_permissions = ('change',)
user = models.OneToOneField(User, related_name='profile', on_delete=models.CASCADE) user = models.OneToOneField(
main_character = models.OneToOneField(EveCharacter, blank=True, null=True, on_delete=models.SET_NULL) User,
state = models.ForeignKey(State, on_delete=models.SET_DEFAULT, default=get_guest_state_pk) 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): def assign_state(self, state=None, commit=True):
if not state: if not state:
@ -93,8 +124,6 @@ class UserProfile(models.Model):
def __str__(self): def __str__(self):
return str(self.user) return str(self.user)
class CharacterOwnership(models.Model): class CharacterOwnership(models.Model):
class Meta: class Meta:
default_permissions = ('change', 'delete') default_permissions = ('change', 'delete')

View File

@ -1,6 +1,11 @@
import logging 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.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed 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) @receiver(post_save, sender=User)
def create_required_models(sender, instance, created, *args, **kwargs): def create_required_models(sender, instance, created, *args, **kwargs):
# ensure all users have a model # ensure all users have our Sub-Models
if created: if created:
logger.debug(f'User {instance} created. Creating default UserProfile.') logger.debug(f'User {instance} created. Creating default UserProfile.')
UserProfile.objects.get_or_create(user=instance) UserProfile.objects.get_or_create(user=instance)

View File

@ -1,4 +1,17 @@
from django.db.models.signals import (
m2m_changed,
post_save,
pre_delete,
pre_save
)
from django.urls import reverse 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: def get_admin_change_view_url(obj: object) -> str:

View 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)

View 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)

View File

@ -40,7 +40,7 @@ def update_character(character_id: int) -> None:
def run_model_update(): def run_model_update():
"""Update all alliances, corporations and characters from ESI""" """Update all alliances, corporations and characters from ESI"""
# update existing corp models #update existing corp models
for corp in EveCorporationInfo.objects.all().values('corporation_id'): for corp in EveCorporationInfo.objects.all().values('corporation_id'):
update_corp.apply_async(args=[corp['corporation_id']], priority=TASK_PRIORITY) update_corp.apply_async(args=[corp['corporation_id']], priority=TASK_PRIORITY)

View File

@ -68,6 +68,7 @@ BASE_DIR = os.path.dirname(PROJECT_DIR)
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'allianceauth.authentication.middleware.UserSettingsMiddleware',
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',

View File

@ -3,12 +3,23 @@ from django.http import HttpResponseRedirect
from django.shortcuts import redirect from django.shortcuts import redirect
from django.contrib import messages from django.contrib import messages
import logging
logger = logging.getLogger(__name__)
class NightModeRedirectView(View): class NightModeRedirectView(View):
SESSION_VAR = "NIGHT_MODE" SESSION_VAR = "NIGHT_MODE"
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
request.session[self.SESSION_VAR] = not self.night_mode_state(request) 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", "/")) return HttpResponseRedirect(request.GET.get("next", "/"))
@classmethod @classmethod