renamed django app folder to backend

This commit is contained in:
Simon
2024-08-03 21:58:22 +02:00
parent 1d07386a06
commit a5b492fecd
124 changed files with 2 additions and 2 deletions

0
backend/user/__init__.py Normal file
View File

45
backend/user/admin.py Normal file
View File

@@ -0,0 +1,45 @@
"""custom admin classes"""
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django_celery_beat import models as BeatModels
from user.models import Account
class HomeAdmin(BaseUserAdmin):
"""register in admin page"""
list_display = ("name", "is_staff", "is_superuser")
list_filter = ("is_superuser",)
fieldsets = (
(None, {"fields": ("is_staff", "is_superuser", "password")}),
("Personal info", {"fields": ("name",)}),
("Groups", {"fields": ("groups",)}),
("Permissions", {"fields": ("user_permissions",)}),
)
add_fieldsets = (
(
None,
{"fields": ("is_staff", "is_superuser", "password1", "password2")},
),
("Personal info", {"fields": ("name",)}),
("Groups", {"fields": ("groups",)}),
("Permissions", {"fields": ("user_permissions",)}),
)
search_fields = ("name",)
ordering = ("name",)
filter_horizontal = ()
admin.site.register(Account, HomeAdmin)
admin.site.unregister(
[
BeatModels.ClockedSchedule,
BeatModels.CrontabSchedule,
BeatModels.IntervalSchedule,
BeatModels.PeriodicTask,
BeatModels.SolarSchedule,
]
)

View File

@@ -0,0 +1,75 @@
# Generated by Django 5.0.7 on 2024-07-22 19:26
import user.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
]
operations = [
migrations.CreateModel(
name="Account",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
("name", models.CharField(max_length=150, unique=True)),
("is_staff", models.BooleanField(default=False)),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.group",
verbose_name="groups",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.permission",
verbose_name="user permissions",
),
),
],
options={
"abstract": False,
},
managers=[
("objects", user.models.AccountManager()),
],
),
]

View File

54
backend/user/models.py Normal file
View File

@@ -0,0 +1,54 @@
"""custom models"""
from django.contrib.auth.models import (
AbstractBaseUser,
BaseUserManager,
PermissionsMixin,
)
from django.db import models
class AccountManager(BaseUserManager):
"""manage user creation methods"""
use_in_migrations = True
def _create_user(self, name, password, **extra_fields):
"""create regular user private"""
values = [name, password]
field_value_map = dict(zip(self.model.REQUIRED_FIELDS, values))
for field_name, value in field_value_map.items():
if not value:
raise ValueError(f"The {field_name} value must be set")
user = self.model(name=name, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, name, password):
"""create regular user public"""
return self._create_user(name, password)
def create_superuser(self, name, password, **extra_fields):
"""create super user"""
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
return self._create_user(name, password, **extra_fields)
class Account(AbstractBaseUser, PermissionsMixin):
"""handle account creation"""
name = models.CharField(max_length=150, unique=True)
is_staff = models.BooleanField(default=False)
objects = AccountManager()
USERNAME_FIELD = "name"
REQUIRED_FIELDS = ["password"]

View File

@@ -0,0 +1,20 @@
"""serializer for account model"""
from rest_framework import serializers
from user.models import Account
class AccountSerializer(serializers.ModelSerializer):
"""serialize account"""
class Meta:
model = Account
fields = (
"id",
"name",
"is_superuser",
"is_staff",
"groups",
"user_permissions",
"last_login",
)

View File

View File

@@ -0,0 +1,10 @@
from django.conf import settings
from django.contrib.auth.middleware import PersistentRemoteUserMiddleware
class HttpRemoteUserMiddleware(PersistentRemoteUserMiddleware):
"""This class allows authentication via HTTP_REMOTE_USER which is set for
example by certain SSO applications.
"""
header = settings.TA_AUTH_PROXY_USERNAME_HEADER

View File

@@ -0,0 +1,142 @@
"""
Functionality:
- read and write user config backed by ES
- encapsulate persistence of user properties
"""
from typing import TypedDict
from common.src.es_connect import ElasticWrap
from common.src.helper import get_stylesheets
class UserConfigType(TypedDict, total=False):
"""describes the user configuration"""
stylesheet: str
page_size: int
sort_by: str
sort_order: str
view_style_home: str
view_style_channel: str
view_style_downloads: str
view_style_playlist: str
grid_items: int
hide_watched: bool
show_ignored_only: bool
show_subed_only: bool
sponsorblock_id: str
class UserConfig:
"""Handle settings for an individual user"""
_DEFAULT_USER_SETTINGS = UserConfigType(
stylesheet="dark.css",
page_size=12,
sort_by="published",
sort_order="desc",
view_style_home="grid",
view_style_channel="list",
view_style_downloads="list",
view_style_playlist="grid",
grid_items=3,
hide_watched=False,
show_ignored_only=False,
show_subed_only=False,
sponsorblock_id=None,
)
VALID_STYLESHEETS = get_stylesheets()
VALID_VIEW_STYLE = ["grid", "list"]
VALID_SORT_ORDER = ["asc", "desc"]
VALID_SORT_BY = [
"published",
"downloaded",
"views",
"likes",
"duration",
"filesize",
]
VALID_GRID_ITEMS = range(3, 8)
def __init__(self, user_id: str):
self._user_id: str = user_id
self._config: UserConfigType = self.get_config()
def get_value(self, key: str):
"""Get the given key from the users configuration
Throws a KeyError if the requested Key is not a permitted value"""
if key not in self._DEFAULT_USER_SETTINGS:
raise KeyError(f"Unable to read config for unknown key '{key}'")
return self._config.get(key) or self._DEFAULT_USER_SETTINGS.get(key)
def set_value(self, key: str, value: str | bool | int):
"""Set or replace a configuration value for the user"""
self._validate(key, value)
old = self.get_value(key)
self._config[key] = value
# Upsert this property (creating a record if not exists)
es_payload = {"doc": {"config": {key: value}}, "doc_as_upsert": True}
es_document_path = f"ta_config/_update/user_{self._user_id}"
response, status = ElasticWrap(es_document_path).post(es_payload)
if status < 200 or status > 299:
raise ValueError(f"Failed storing user value {status}: {response}")
print(f"User {self._user_id} value '{key}' change: {old} -> {value}")
def _validate(self, key, value):
"""validate key and value"""
if not self._user_id:
raise ValueError("Unable to persist config for null user_id")
if key not in self._DEFAULT_USER_SETTINGS:
raise KeyError(
f"Unable to persist config for an unknown key '{key}'"
)
valid_values = {
"stylesheet": self.VALID_STYLESHEETS,
"sort_by": self.VALID_SORT_BY,
"sort_order": self.VALID_SORT_ORDER,
"view_style_home": self.VALID_VIEW_STYLE,
"view_style_channel": self.VALID_VIEW_STYLE,
"view_style_download": self.VALID_VIEW_STYLE,
"view_style_playlist": self.VALID_VIEW_STYLE,
"grid_items": self.VALID_GRID_ITEMS,
"page_size": int,
"hide_watched": bool,
"show_ignored_only": bool,
"show_subed_only": bool,
}
validation_value = valid_values.get(key)
if isinstance(validation_value, (list, range)):
if value not in validation_value:
raise ValueError(f"Invalid value for {key}: {value}")
elif validation_value == int:
if not isinstance(value, int):
raise ValueError(f"Invalid value for {key}: {value}")
elif validation_value == bool:
if not isinstance(value, bool):
raise ValueError(f"Invalid value for {key}: {value}")
def get_config(self) -> UserConfigType:
"""get config from ES or load from the application defaults"""
if not self._user_id:
# this is for a non logged-in user so use all the defaults
return {}
# Does this user have configuration stored in ES
es_document_path = f"ta_config/_doc/user_{self._user_id}"
response, status = ElasticWrap(es_document_path).get(print_error=False)
if status == 200 and "_source" in response.keys():
source = response.get("_source")
if "config" in source.keys():
return source.get("config")
# There is no config in ES
return {}

9
backend/user/urls.py Normal file
View File

@@ -0,0 +1,9 @@
"""all user API urls"""
from django.urls import path
from user import views
urlpatterns = [
path("login/", views.LoginApiView.as_view(), name="api-user-login"),
path("me/", views.UserConfigView.as_view(), name="api-user-me"),
]

77
backend/user/views.py Normal file
View File

@@ -0,0 +1,77 @@
"""all user api views"""
from common.views import ApiBaseView
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.response import Response
from user.models import Account
from user.serializers import AccountSerializer
from user.src.user_config import UserConfig
class UserConfigView(ApiBaseView):
"""resolves to /api/config/user/
GET: return current user config
POST: update user config
"""
def get(self, request):
"""get config"""
user_id = request.user.id
account = Account.objects.get(id=user_id)
serializer = AccountSerializer(account)
response = serializer.data.copy()
config = UserConfig(user_id).get_config()
response.update({"config": config})
return Response(response)
def post(self, request):
"""update config"""
user_id = request.user.id
data = request.data
user_conf = UserConfig(user_id)
for key, value in data.items():
try:
user_conf.set_value(key, value)
except ValueError as err:
message = {
"status": "Bad Request",
"message": f"failed updating {key} to '{value}', {err}",
}
return Response(message, status=400)
response = user_conf.get_config()
response.update({"user_id": user_id})
return Response(response)
class LoginApiView(ObtainAuthToken):
"""resolves to /api/user/login/
POST: return token and username after successful login
"""
def post(self, request, *args, **kwargs):
"""post data"""
# pylint: disable=no-member
serializer = self.serializer_class(
data=request.data, context={"request": request}
)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data["user"]
token, _ = Token.objects.get_or_create(user=user)
print(f"returning token for user with id {user.pk}")
return Response(
{
"token": token.key,
"user_id": user.pk,
"is_superuser": user.is_superuser,
"is_staff": user.is_staff,
"user_groups": [group.name for group in user.groups.all()],
}
)