From 7aa86ece274552f3d49d39f9bba11146c2c85617 Mon Sep 17 00:00:00 2001 From: Alex Parsons Date: Mon, 16 Dec 2024 20:58:52 +0000 Subject: [PATCH] Basic Django interface - Add supporting functions - Add basic User model --- src/twfy_tools/db/django_setup.py | 36 +++++++++++++ src/twfy_tools/db/model_helper.py | 52 ++++++++++++++++++ src/twfy_tools/db/models.py | 90 +++++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+) create mode 100644 src/twfy_tools/db/django_setup.py create mode 100644 src/twfy_tools/db/model_helper.py create mode 100644 src/twfy_tools/db/models.py diff --git a/src/twfy_tools/db/django_setup.py b/src/twfy_tools/db/django_setup.py new file mode 100644 index 0000000000..5e85882c8b --- /dev/null +++ b/src/twfy_tools/db/django_setup.py @@ -0,0 +1,36 @@ +""" +This is a simple minimal setup for using Django ORMs. +Import this when creating models and then the models can be used as normal. +""" + +import os + +import django +from django.conf import settings + +from twfy_tools.common.config import config + +# Allow use in notebooks +os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" + +if not settings.configured: + settings.configure( + DEBUG=True, + SECRET_KEY="your-secret-key", + ALLOWED_HOSTS=["*"], + INSTALLED_APPS=[ + "twfy_tools", + ], + DATABASES={ + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": config.TWFY_DB_NAME, + "USER": config.TWFY_DB_USER, + "PASSWORD": config.TWFY_DB_PASS, + "HOST": config.TWFY_DB_HOST, + "PORT": config.TWFY_DB_PORT, + } + }, + ) + +django.setup() diff --git a/src/twfy_tools/db/model_helper.py b/src/twfy_tools/db/model_helper.py new file mode 100644 index 0000000000..2f5d865baf --- /dev/null +++ b/src/twfy_tools/db/model_helper.py @@ -0,0 +1,52 @@ +from typing import Any, Callable, TypeVar + +from django.db import models + +from typing_extensions import ParamSpec, dataclass_transform + +FieldType = TypeVar( + "FieldType", + bound=models.Field, +) +P = ParamSpec("P") + + +def field( + model_class: Callable[P, FieldType], + null: bool = False, + *args: P.args, + **kwargs: P.kwargs, +) -> Any: + """ + Helper function for basic field creation. + So the type checker doesn't complain about the return type + and you can specify the specify type of the item as a typehint. + """ + if args: + raise ValueError("Positional arguments are not supported") + kwargs["null"] = null + if isinstance(model_class, type) and issubclass(model_class, models.Field): + return model_class(**kwargs) + else: + raise ValueError(f"Invalid model class {model_class}") + + +@dataclass_transform(kw_only_default=True, field_specifiers=(field,)) +class DataclassModelBase(models.base.ModelBase): + def __new__(cls, name: str, bases: tuple[type], dct: dict[str, Any], **kwargs: Any): + """ + Basic metaclass to make class keyword parameters into a Meta class. + """ + if kwargs: + dct["Meta"] = type("Meta", (dct.get("Meta", type),), kwargs) + return super().__new__(cls, name, bases, dct) + + +class DataclassModel(models.Model, metaclass=DataclassModelBase): + """ + Basic wrapper that adds tidier metaclass config, and dataclass + prompting. + """ + + +class UnManagedDataclassModel(DataclassModel, managed=False): ... diff --git a/src/twfy_tools/db/models.py b/src/twfy_tools/db/models.py new file mode 100644 index 0000000000..13bf732a2c --- /dev/null +++ b/src/twfy_tools/db/models.py @@ -0,0 +1,90 @@ +""" +This is a simple one file setup for using django's ORM models. +""" + +import datetime +from enum import IntEnum +from typing import Optional + +from django.db import models + +from twfy_tools.db import django_setup as django_setup + +from ..common.enum_backport import StrEnum +from .model_helper import UnManagedDataclassModel, field + +datetime_min = datetime.datetime(1, 1, 1, 0, 0, 0) + + +class UserLevels(StrEnum): + VIEWER = "Viewer" + USER = "User" + MODERATOR = "Moderator" + ADMINISTRATOR = "Administrator" + SUPERUSER = "Superuser" + + +class OptinValues(IntEnum): + OPTIN_SERVICE = 1 + OPTIN_STREAM = 2 + OPTIN_ORG = 4 + + +class User(UnManagedDataclassModel, db_table="users"): + user_id: Optional[int] = field(models.AutoField, primary_key=True) + firstname: str = field(models.CharField, max_length=255, default="") + lastname: str = field(models.CharField, max_length=255, default="") + email: str = field(models.CharField, max_length=255) + password: str = field(models.CharField, max_length=102, default="") + lastvisit: datetime.datetime = field(models.DateTimeField, default=datetime_min) + registrationtime: datetime.datetime = field( + models.DateTimeField, default=datetime_min + ) + registrationip: str = field(models.CharField, max_length=20, blank=True, null=True) + status: UserLevels = field( + models.CharField, + max_length=13, + blank=True, + null=True, + default=UserLevels.VIEWER, + ) + emailpublic: int = field(models.IntegerField, default=0) + optin: int = field(models.IntegerField, default=0) + deleted: int = field(models.IntegerField, default=0) + postcode: str = field(models.CharField, max_length=10, blank=True, null=True) + registrationtoken: str = field(models.CharField, max_length=24, default="") + confirmed: int = field(models.IntegerField, default=0) + url: str = field(models.CharField, max_length=255, blank=True, null=True) + api_key: str = field( + models.CharField, unique=True, max_length=24, blank=True, null=True + ) + facebook_id: str = field(models.CharField, max_length=24, blank=True, null=True) + facebook_token: str = field(models.CharField, max_length=200, blank=True, null=True) + + UserLevels = UserLevels + OptinValues = OptinValues + + def __str__(self): + return f"{self.status}: {self.email}" + + def get_optin_values(self) -> list[OptinValues]: + """ + Returns a list of OptinValues that match the user's optin value. + """ + matched_values: list[OptinValues] = [] + for value in OptinValues: + if self.optin & value: + matched_values.append(value) + return matched_values + + def add_optin(self, optin_value: OptinValues): + """ + Add an optin value to the user. + """ + self.optin |= optin_value + + def remove_optin(self, optin_value: OptinValues): + """ + Remove an optin value from the user. + """ + self.optin &= ~optin_value