Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Contributor backend 21 #144

Merged
merged 56 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
75456ff
avoid importing models
SKairinos Nov 4, 2024
4c8e557
fix issues
SKairinos Nov 4, 2024
77bd91c
fix type imports
SKairinos Nov 4, 2024
4922af6
fix: type as vars
SKairinos Nov 5, 2024
84f4895
fix: type imports
SKairinos Nov 5, 2024
e380049
fix: imports
SKairinos Nov 5, 2024
c3312e6
fix: imports
SKairinos Nov 5, 2024
1dd44d0
base request
SKairinos Nov 5, 2024
e0a3061
fix: types
SKairinos Nov 5, 2024
40076c1
ignore duplicate code
SKairinos Nov 5, 2024
67e6b17
disable duplicate code
SKairinos Nov 5, 2024
9c0463d
split request objects
SKairinos Nov 5, 2024
623f995
session param
SKairinos Nov 5, 2024
a3fc0c7
disable duplicate code
SKairinos Nov 5, 2024
7342906
fix: abstract api request factory
SKairinos Nov 5, 2024
fb58a80
import base api request factory
SKairinos Nov 5, 2024
b5242a9
split model list serializer
SKairinos Nov 5, 2024
d19d1ab
abstract model view and serializer
SKairinos Nov 5, 2024
0f695c9
abstract model list
SKairinos Nov 5, 2024
f9294cd
import BaseModelListSerializer
SKairinos Nov 5, 2024
2492870
disable missing-function-docstring
SKairinos Nov 5, 2024
77925a7
init request
SKairinos Nov 6, 2024
b805632
fix: init request
SKairinos Nov 6, 2024
27df3ee
abstract model serializer test case
SKairinos Nov 6, 2024
48c9b46
fix types
SKairinos Nov 6, 2024
19cbfda
fix linting issues
SKairinos Nov 6, 2024
5a85de9
split code
SKairinos Nov 6, 2024
5b1d3bd
fix: abstract api test case and client
SKairinos Nov 6, 2024
0f503e9
# pylint: disable-next=too-many-ancestors
SKairinos Nov 6, 2024
33d68a9
split code
SKairinos Nov 6, 2024
b058505
abstract user and session
SKairinos Nov 6, 2024
11cc4fc
fix: type hints
SKairinos Nov 6, 2024
2664b2e
fix types
SKairinos Nov 6, 2024
a351a67
fix types
SKairinos Nov 6, 2024
4c5857a
disable too-many-ancestors
SKairinos Nov 7, 2024
04966b5
fix linting
SKairinos Nov 7, 2024
ab235bf
abstract model view set test case and client
SKairinos Nov 7, 2024
045deef
import base classes
SKairinos Nov 7, 2024
3b0409e
fix: session def
SKairinos Nov 7, 2024
40e7be7
mypy ignore
SKairinos Nov 7, 2024
b633a24
remove id field
SKairinos Nov 7, 2024
6226609
abstract is authenticated
SKairinos Nov 7, 2024
691d372
fix: comment out check
SKairinos Nov 7, 2024
58a5d58
delete unnecessary code
SKairinos Nov 7, 2024
687d587
fix pre setup
SKairinos Nov 7, 2024
f092832
disable no-member
SKairinos Nov 7, 2024
b7e603e
model serializer type arg
SKairinos Nov 7, 2024
5848e30
AnyBaseModelViewSet
SKairinos Nov 7, 2024
68df522
AnyBaseModelViewSet
SKairinos Nov 7, 2024
623b1a2
fix type hints
SKairinos Nov 7, 2024
9fc68d3
base login view and form
SKairinos Nov 7, 2024
882de73
fix: import
SKairinos Nov 7, 2024
4675ee2
get arg helper
SKairinos Nov 7, 2024
655604e
delete unused var
SKairinos Nov 7, 2024
3a7c0fa
migrate on app startup
SKairinos Nov 11, 2024
19c790b
feedback
SKairinos Nov 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions codeforlife/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import multiprocessing
import typing as t

from django.core.management import call_command

Check warning on line 9 in codeforlife/app.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/app.py#L9

Added line #L9 was not covered by tests
from gunicorn.app.base import BaseApplication # type: ignore[import-untyped]


Expand All @@ -19,6 +20,8 @@
"""

def __init__(self, app: t.Callable):
call_command("migrate", interactive=False)

Check warning on line 23 in codeforlife/app.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/app.py#L23

Added line #L23 was not covered by tests

self.options = {
"bind": "0.0.0.0:8080",
# https://docs.gunicorn.org/en/stable/design.html#how-many-workers
Expand Down
81 changes: 81 additions & 0 deletions codeforlife/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""
© Ocado Group
Created on 07/11/2024 at 15:08:33(+00:00).
"""

import typing as t

from django import forms
from django.contrib.auth import authenticate
from django.core.exceptions import ValidationError
from django.core.handlers.wsgi import WSGIRequest

from .models import AbstractBaseUser
from .types import get_arg

AnyAbstractBaseUser = t.TypeVar("AnyAbstractBaseUser", bound=AbstractBaseUser)


class BaseLoginForm(forms.Form, t.Generic[AnyAbstractBaseUser]):
"""Base login form that all other login forms must inherit."""

user: AnyAbstractBaseUser

@classmethod
def get_user_class(cls) -> t.Type[AnyAbstractBaseUser]:
"""Get the user class."""
return get_arg(cls, 0)

Check warning on line 27 in codeforlife/forms.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/forms.py#L27

Added line #L27 was not covered by tests

def __init__(self, request: WSGIRequest, *args, **kwargs):
self.request = request
super().__init__(*args, **kwargs)

Check warning on line 31 in codeforlife/forms.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/forms.py#L30-L31

Added lines #L30 - L31 were not covered by tests

def clean(self):
"""Authenticates a user.

Raises:
ValidationError: If there are form errors.
ValidationError: If the user's credentials were incorrect.
ValidationError: If the user's account is deactivated.

Returns:
The cleaned form data.
"""

if self.errors:
raise ValidationError(

Check warning on line 46 in codeforlife/forms.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/forms.py#L45-L46

Added lines #L45 - L46 were not covered by tests
"Found form errors. Skipping authentication.",
code="form_errors",
)

user = authenticate(

Check warning on line 51 in codeforlife/forms.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/forms.py#L51

Added line #L51 was not covered by tests
self.request,
**{key: self.cleaned_data[key] for key in self.fields.keys()}
)
if user is None:
raise ValidationError(

Check warning on line 56 in codeforlife/forms.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/forms.py#L55-L56

Added lines #L55 - L56 were not covered by tests
self.get_invalid_login_error_message(),
code="invalid_login",
)
if not isinstance(user, self.get_user_class()):
raise ValidationError(

Check warning on line 61 in codeforlife/forms.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/forms.py#L60-L61

Added lines #L60 - L61 were not covered by tests
"Incorrect user class.",
code="incorrect_user_class",
)
if not user.is_active:
raise ValidationError(

Check warning on line 66 in codeforlife/forms.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/forms.py#L65-L66

Added lines #L65 - L66 were not covered by tests
"User is not active",
code="user_not_active",
)

self.user = user

Check warning on line 71 in codeforlife/forms.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/forms.py#L71

Added line #L71 was not covered by tests

return self.cleaned_data

Check warning on line 73 in codeforlife/forms.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/forms.py#L73

Added line #L73 was not covered by tests

def get_invalid_login_error_message(self) -> str:
"""Returns the error message if the user failed to login.

Raises:
NotImplementedError: If message is not set.
"""
raise NotImplementedError()

Check warning on line 81 in codeforlife/forms.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/forms.py#L81

Added line #L81 was not covered by tests
3 changes: 3 additions & 0 deletions codeforlife/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
Created on 19/01/2024 at 15:20:45(+00:00).
"""

from .abstract_base_session import AbstractBaseSession
from .abstract_base_user import AbstractBaseUser
from .base import *
from .base_session_store import BaseSessionStore
78 changes: 78 additions & 0 deletions codeforlife/models/abstract_base_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""
© Ocado Group
Created on 06/11/2024 at 16:44:56(+00:00).
"""

import typing as t

from django.contrib.sessions.base_session import (
AbstractBaseSession as _AbstractBaseSession,
)
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from .abstract_base_user import AbstractBaseUser

# pylint: disable=duplicate-code
if t.TYPE_CHECKING:
from django_stubs_ext.db.models import TypedModelMeta

Check warning on line 19 in codeforlife/models/abstract_base_session.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/models/abstract_base_session.py#L19

Added line #L19 was not covered by tests

from .base_session_store import BaseSessionStore

Check warning on line 21 in codeforlife/models/abstract_base_session.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/models/abstract_base_session.py#L21

Added line #L21 was not covered by tests
else:
TypedModelMeta = object

AnyAbstractBaseUser = t.TypeVar("AnyAbstractBaseUser", bound=AbstractBaseUser)
# pylint: enable=duplicate-code


class AbstractBaseSession(_AbstractBaseSession):
"""
Base session class to be inherited by all session classes.
https://docs.djangoproject.com/en/3.2/topics/http/sessions/#example
"""

pk: str # type: ignore[assignment]

user_id: int

# pylint: disable-next=missing-class-docstring,too-few-public-methods
class Meta(TypedModelMeta):
abstract = True
verbose_name = _("session")
verbose_name_plural = _("sessions")

@property
def is_expired(self):
"""Whether or not this session has expired."""
return self.expire_date < timezone.now()

@property
def store(self):
"""A store instance for this session."""
return self.get_session_store_class()(self.session_key)

Check warning on line 53 in codeforlife/models/abstract_base_session.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/models/abstract_base_session.py#L53

Added line #L53 was not covered by tests

@classmethod
def get_session_store_class(cls) -> t.Type["BaseSessionStore"]:
raise NotImplementedError

Check warning on line 57 in codeforlife/models/abstract_base_session.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/models/abstract_base_session.py#L57

Added line #L57 was not covered by tests

@staticmethod
def init_user_field(user_class: t.Type[AnyAbstractBaseUser]):
"""Initializes the user field that relates a session to a user.

Example:
class Session(AbstractBaseSession):
user = AbstractBaseSession.init_user_field(User)

Args:
user_class: The user model to associate sessions to.

Returns:
A one-to-one field that relates to the provided user model.
"""
return models.OneToOneField(
user_class,
null=True,
blank=True,
on_delete=models.CASCADE,
)
58 changes: 58 additions & 0 deletions codeforlife/models/abstract_base_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
© Ocado Group
Created on 06/11/2024 at 16:38:15(+00:00).
"""

import typing as t
from functools import cached_property

from django.apps import apps
from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser as _AbstractBaseUser
from django.utils.translation import gettext_lazy as _

if t.TYPE_CHECKING:
from django_stubs_ext.db.models import TypedModelMeta

Check warning on line 15 in codeforlife/models/abstract_base_user.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/models/abstract_base_user.py#L15

Added line #L15 was not covered by tests

from .abstract_base_session import AbstractBaseSession

Check warning on line 17 in codeforlife/models/abstract_base_user.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/models/abstract_base_user.py#L17

Added line #L17 was not covered by tests
else:
TypedModelMeta = object


class AbstractBaseUser(_AbstractBaseUser):
"""
Base user class to be inherited by all user classes.
https://docs.djangoproject.com/en/3.2/topics/auth/customizing/#using-a-custom-user-model-when-starting-a-project
"""

pk: int
session: "AbstractBaseSession"

# pylint: disable-next=missing-class-docstring,too-few-public-methods
class Meta(TypedModelMeta):
abstract = True
verbose_name = _("user")
verbose_name_plural = _("users")

@cached_property
def _session_class(self):
return t.cast(

Check warning on line 39 in codeforlife/models/abstract_base_user.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/models/abstract_base_user.py#L39

Added line #L39 was not covered by tests
t.Type["AbstractBaseSession"],
apps.get_model(
app_label=(
t.cast(str, settings.SESSION_ENGINE)
.lower()
.removesuffix(".models.session")
.split(".")[-1]
),
model_name="session",
),
)

@property
def is_authenticated(self):
"""A flag designating if this contributor has authenticated."""
try:
return self.is_active and not self.session.is_expired
except self._session_class.DoesNotExist:
return False

Check warning on line 58 in codeforlife/models/abstract_base_user.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/models/abstract_base_user.py#L57-L58

Added lines #L57 - L58 were not covered by tests
11 changes: 4 additions & 7 deletions codeforlife/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,20 @@

import typing as t

from django.db.models import Manager
from django.db.models import Model as _Model

if t.TYPE_CHECKING:
from django_stubs_ext.db.models import TypedModelMeta
else:
TypedModelMeta = object

Id = t.TypeVar("Id")

class Model(_Model):
"""Base for all models."""

class Model(_Model, t.Generic[Id]):
"""A base class for all Django models."""
objects: Manager[t.Self]

id: Id
pk: Id

# pylint: disable-next=missing-class-docstring,too-few-public-methods
class Meta(TypedModelMeta):
abstract = True

Expand Down
91 changes: 91 additions & 0 deletions codeforlife/models/base_session_store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""
© Ocado Group
Created on 06/11/2024 at 17:31:32(+00:00).
"""

import typing as t

from django.contrib.auth import SESSION_KEY
from django.contrib.sessions.backends.db import SessionStore
from django.utils import timezone

from ..types import get_arg

if t.TYPE_CHECKING:
from .abstract_base_session import AbstractBaseSession
from .abstract_base_user import AbstractBaseUser

Check warning on line 16 in codeforlife/models/base_session_store.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/models/base_session_store.py#L15-L16

Added lines #L15 - L16 were not covered by tests

AnyAbstractBaseSession = t.TypeVar(

Check warning on line 18 in codeforlife/models/base_session_store.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/models/base_session_store.py#L18

Added line #L18 was not covered by tests
"AnyAbstractBaseSession", bound=AbstractBaseSession
)
AnyAbstractBaseUser = t.TypeVar(

Check warning on line 21 in codeforlife/models/base_session_store.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/models/base_session_store.py#L21

Added line #L21 was not covered by tests
"AnyAbstractBaseUser", bound=AbstractBaseUser
)
else:
AnyAbstractBaseSession = t.TypeVar("AnyAbstractBaseSession")
AnyAbstractBaseUser = t.TypeVar("AnyAbstractBaseUser")


class BaseSessionStore(
SessionStore,
t.Generic[AnyAbstractBaseSession, AnyAbstractBaseUser],
):
"""
Base session store class to be inherited by all session store classes.
https://docs.djangoproject.com/en/3.2/topics/http/sessions/#example
"""

@classmethod
def get_model_class(cls) -> t.Type[AnyAbstractBaseSession]:
return get_arg(cls, 0)

@classmethod
def get_user_class(cls) -> t.Type[AnyAbstractBaseUser]:
"""Get the user class."""
return get_arg(cls, 1)

def associate_session_to_user(
self, session: AnyAbstractBaseSession, user_id: int
):
"""Associate an anon session to a user.

Args:
session: The anon session.
user_id: The user to associate.
"""
objects = self.get_user_class().objects # type: ignore[attr-defined]
session.user = objects.get(id=user_id) # type: ignore[attr-defined]

def create_model_instance(self, data):
try:
user_id = int(data.get(SESSION_KEY))
except (ValueError, TypeError):
# Create an anon session.
return super().create_model_instance(data)

model_class = self.get_model_class()

try:
session = model_class.objects.get(
user_id=user_id, # type: ignore[misc]
)
except model_class.DoesNotExist:
session = model_class.objects.get(session_key=self.session_key)
self.associate_session_to_user(
t.cast(AnyAbstractBaseSession, session), user_id
)

session.session_data = self.encode(data)

return session

@classmethod
def clear_expired(cls, user_id=None):
session_query = cls.get_model_class().objects.filter(

Check warning on line 84 in codeforlife/models/base_session_store.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/models/base_session_store.py#L84

Added line #L84 was not covered by tests
expire_date__lt=timezone.now()
)

if user_id is not None:
session_query = session_query.filter(user_id=user_id)

Check warning on line 89 in codeforlife/models/base_session_store.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/models/base_session_store.py#L88-L89

Added lines #L88 - L89 were not covered by tests

session_query.delete()

Check warning on line 91 in codeforlife/models/base_session_store.py

View check run for this annotation

Codecov / codecov/patch

codeforlife/models/base_session_store.py#L91

Added line #L91 was not covered by tests
8 changes: 8 additions & 0 deletions codeforlife/request/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""
© Ocado Group
Created on 05/11/2024 at 14:40:32(+00:00).
"""

from .drf import BaseRequest, Request
from .http import BaseHttpRequest, HttpRequest
from .wsgi import BaseWSGIRequest, WSGIRequest
Loading