reenabled admin area; outfitted apps.lyric.models w. AbstractBaseUser instead of custom user class; many other fns & several models updated to accomodate, such as set_unusable_password() method to base user model; reset staging db to prepare for refreshed lyric migrations to accomodate for retrofitted pw field
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Disco DeDisco
2026-02-19 20:31:29 -05:00
parent d26196a7f1
commit 025a59938b
13 changed files with 114 additions and 11 deletions

View File

@@ -1,3 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import Token, User
# Register your models here.
admin.site.register(User)
admin.site.register(Token)

View File

@@ -13,7 +13,7 @@ class PasswordlessAuthenticationBackend:
try: try:
return User.objects.get(email=token.email) return User.objects.get(email=token.email)
except User.DoesNotExist: except User.DoesNotExist:
return User.objects.create(email=token.email) return User.objects.create_user(email=token.email)
def get_user(self, user_id): def get_user(self, user_id):
try: try:

View File

@@ -1,4 +1,4 @@
# Generated by Django 6.0 on 2026-02-08 01:19 # Generated by Django 6.0 on 2026-02-20 00:48
import uuid import uuid
from django.db import migrations, models from django.db import migrations, models
@@ -15,9 +15,16 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='User', name='User',
fields=[ fields=[
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('email', models.EmailField(max_length=254, unique=True)), ('email', models.EmailField(max_length=254, unique=True)),
('is_staff', models.BooleanField(default=False)),
('is_superuser', models.BooleanField(default=False)),
], ],
options={
'abstract': False,
},
), ),
migrations.CreateModel( migrations.CreateModel(
name='Token', name='Token',

View File

@@ -1,16 +1,38 @@
import uuid import uuid
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.db import models from django.db import models
class UserManager(BaseUserManager):
def create_user(self, email):
user = self.model(email=email)
user.set_unusable_password()
user.save(using=self._db)
return user
def create_superuser(self, email, password):
user = self.model(email=email, is_staff=True, is_superuser=True)
user.set_password(password)
user.save(using=self._db)
return user
class Token(models.Model): class Token(models.Model):
email = models.EmailField() email = models.EmailField()
uid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) uid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
class User(models.Model): class User(AbstractBaseUser):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
email = models.EmailField(unique=True) email = models.EmailField(unique=True)
is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False)
objects = UserManager()
REQUIRED_FIELDS = [] REQUIRED_FIELDS = []
USERNAME_FIELD = "email" USERNAME_FIELD = "email"
def has_perm(self, perm, obj=None):
return self.is_superuser
is_authenticated = True def has_module_perms(self, app_label):
is_anonymous =False return self.is_superuser

View File

@@ -11,6 +11,7 @@ class UserModelTest(TestCase):
def test_user_is_valid_with_email_only(self): def test_user_is_valid_with_email_only(self):
user = User(email="a@b.cde") user = User(email="a@b.cde")
user.set_unusable_password()
user.full_clean() # should not raise user.full_clean() # should not raise
def test_id_is_primary_key(self): def test_id_is_primary_key(self):
@@ -23,3 +24,19 @@ class TokenModelTest(TestCase):
token2 = Token.objects.create(email="v@w.xyz") token2 = Token.objects.create(email="v@w.xyz")
self.assertNotEqual(token1.pk, token2.pk) self.assertNotEqual(token1.pk, token2.pk)
self.assertIsInstance(token1.pk, uuid.UUID) self.assertIsInstance(token1.pk, uuid.UUID)
class UserManagerTest(TestCase):
def test_create_superuser_sets_is_staff_and_is_superuser(self):
user = User.objects.create_superuser(
email="admin@example.com",
password="correct-password",
)
self.assertTrue(user.is_staff)
self.assertTrue(user.is_superuser)
def test_create_superuser_sets_usable_password(self):
user = User.objects.create_superuser(
email="admin@example.com",
password="correct-password",
)
self.assertTrue(user.check_password("correct-password"))

View File

@@ -2,6 +2,7 @@ from django.http import HttpRequest
from django.test import SimpleTestCase from django.test import SimpleTestCase
from apps.lyric.authentication import PasswordlessAuthenticationBackend from apps.lyric.authentication import PasswordlessAuthenticationBackend
from apps.lyric.models import User
class SimpleAuthenticateTest(SimpleTestCase): class SimpleAuthenticateTest(SimpleTestCase):
@@ -15,3 +16,16 @@ class SimpleAuthenticateTest(SimpleTestCase):
result = PasswordlessAuthenticationBackend().authenticate(HttpRequest()) result = PasswordlessAuthenticationBackend().authenticate(HttpRequest())
self.assertIsNone(result) self.assertIsNone(result)
class UserPermissionsTest(SimpleTestCase):
def test_superuser_has_perm(self):
user = User(is_superuser=True)
self.assertTrue(user.has_perm("any.permission"))
def test_superuser_has_module_perms(self):
user = User(is_superuser=True)
self.assertTrue(user.has_module_perms("any_app"))
def test_non_superuser_has_no_perm(self):
user = User(is_superuser=False)
self.assertFalse(user.has_perm("any.permission"))

View File

@@ -39,7 +39,7 @@ else:
INSTALLED_APPS = [ INSTALLED_APPS = [
# Django apps # Django apps
# 'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
@@ -123,6 +123,7 @@ AUTH_PASSWORD_VALIDATORS = [
AUTH_USER_MODEL = "lyric.User" AUTH_USER_MODEL = "lyric.User"
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"apps.lyric.authentication.PasswordlessAuthenticationBackend", "apps.lyric.authentication.PasswordlessAuthenticationBackend",
] ]

View File

@@ -1,10 +1,10 @@
# from django.contrib import admin from django.contrib import admin
from django.http import HttpResponse from django.http import HttpResponse
from django.urls import include, path from django.urls import include, path
from apps.dashboard import views as dash_views from apps.dashboard import views as dash_views
urlpatterns = [ urlpatterns = [
# path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('', dash_views.home_page, name='home'), path('', dash_views.home_page, name='home'),
path('apps/dashboard/', include('apps.dashboard.urls')), path('apps/dashboard/', include('apps.dashboard.urls')),
path('apps/lyric/', include('apps.lyric.urls')), path('apps/lyric/', include('apps.lyric.urls')),

View File

@@ -1,5 +1,6 @@
import subprocess import subprocess
USER = "discoman" USER = "discoman"

View File

@@ -1,5 +1,10 @@
from django.conf import settings from django.conf import settings
from django.contrib.auth import BACKEND_SESSION_KEY, SESSION_KEY, get_user_model from django.contrib.auth import (
BACKEND_SESSION_KEY,
HASH_SESSION_KEY,
SESSION_KEY,
get_user_model,
)
from django.contrib.sessions.backends.db import SessionStore from django.contrib.sessions.backends.db import SessionStore
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
@@ -18,6 +23,7 @@ def create_pre_authenticated_session(email):
user = User.objects.create(email=email) user = User.objects.create(email=email)
session = SessionStore() session = SessionStore()
session[SESSION_KEY] = str(user.pk) # Convert UUID to string for JSON serialization session[SESSION_KEY] = str(user.pk) # Convert UUID to string for JSON serialization
session[BACKEND_SESSION_KEY] = settings.AUTHENTICATION_BACKENDS[0] session[BACKEND_SESSION_KEY] = "apps.lyric.authentication.PasswordlessAuthenticationBackend"
session[HASH_SESSION_KEY] = user.get_session_auth_hash()
session.save() session.save()
return session.session_key return session.session_key

View File

@@ -0,0 +1,28 @@
from selenium.webdriver.common.by import By
from .base import FunctionalTest
from apps.lyric.models import User
class AdminLoginTest(FunctionalTest):
def setUp(self):
super().setUp()
self.superuser = User.objects.create_superuser(
email="admin@example.com",
password="correct-password",
)
def test_can_access_admin(self):
self.browser.get(self.live_server_url + "/admin/")
self.wait_for(lambda: self.browser.find_element(By.ID, "id_username"))
self.browser.find_element(By.ID, "id_username").send_keys("admin@example.com")
self.browser.find_element(By.ID, "id_password").send_keys("correct-password")
self.browser.find_element(By.CSS_SELECTOR, "input[type=submit]").click()
body = self.wait_for(
lambda: self.browser.find_element(By.TAG_NAME, "body")
)
self.assertIn("Site administration", body.text)
self.assertIn("Users", body.text)
self.assertIn("Tokens", body.text)

View File

@@ -4,6 +4,7 @@ from selenium.webdriver.common.keys import Keys
from .base import FunctionalTest from .base import FunctionalTest
from .list_page import ListPage from .list_page import ListPage
class LayoutAndStylingTest(FunctionalTest): class LayoutAndStylingTest(FunctionalTest):
def test_layout_and_styling(self): def test_layout_and_styling(self):
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)

View File

@@ -2,11 +2,14 @@ import re
from unittest.mock import patch from unittest.mock import patch
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from .base import FunctionalTest from .base import FunctionalTest
TEST_EMAIL = "discoman@example.com" TEST_EMAIL = "discoman@example.com"
SUBJECT = "A magic login link to your Dashboard" SUBJECT = "A magic login link to your Dashboard"
class LoginTest(FunctionalTest): class LoginTest(FunctionalTest):
@patch('apps.lyric.views.requests.post') @patch('apps.lyric.views.requests.post')
def test_login_using_magic_link(self, mock_post): def test_login_using_magic_link(self, mock_post):