update
This commit is contained in:
parent
15c634e588
commit
d2be7b1d05
36 changed files with 27 additions and 896 deletions
|
|
@ -1,13 +1,13 @@
|
||||||
from django.conf import settings
|
# from django.conf import settings
|
||||||
from rest_framework.routers import DefaultRouter
|
# from rest_framework.routers import DefaultRouter
|
||||||
from rest_framework.routers import SimpleRouter
|
# from rest_framework.routers import SimpleRouter
|
||||||
|
|
||||||
from lms.users.api.views import UserViewSet
|
# from lms.users.api.views import UserViewSet
|
||||||
|
|
||||||
router = DefaultRouter() if settings.DEBUG else SimpleRouter()
|
# router = DefaultRouter() if settings.DEBUG else SimpleRouter()
|
||||||
|
|
||||||
router.register("users", UserViewSet)
|
# router.register("users", UserViewSet)
|
||||||
|
|
||||||
|
|
||||||
app_name = "api"
|
# app_name = "api"
|
||||||
urlpatterns = router.urls
|
# urlpatterns = router.urls
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,6 @@ THIRD_PARTY_APPS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
LOCAL_APPS = [
|
LOCAL_APPS = [
|
||||||
"lms.users",
|
|
||||||
"lms.accounts",
|
"lms.accounts",
|
||||||
"lms.app",
|
"lms.app",
|
||||||
# Your stuff: custom apps go here
|
# Your stuff: custom apps go here
|
||||||
|
|
@ -351,12 +350,12 @@ REST_FRAMEWORK = {
|
||||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||||
"rest_framework.authentication.SessionAuthentication",
|
"rest_framework.authentication.SessionAuthentication",
|
||||||
"rest_framework.authentication.TokenAuthentication",
|
"rest_framework.authentication.TokenAuthentication",
|
||||||
'dj_rest_auth.jwt_auth.JWTCookieAuthentic ation',
|
'dj_rest_auth.jwt_auth.JWTCookieAuthentication',
|
||||||
),
|
),
|
||||||
"DEFAULT_PERMISSION_CLASSES": (
|
"DEFAULT_PERMISSION_CLASSES": (
|
||||||
"rest_framework.permissions.IsAuthenticated",
|
"rest_framework.permissions.IsAuthenticated",
|
||||||
),
|
),
|
||||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoS chema",
|
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||||
}
|
}
|
||||||
REST_AUTH = {
|
REST_AUTH = {
|
||||||
'LOGIN_SERIALIZER': 'lms.accounts.serializers.CustomLoginSerializer',
|
'LOGIN_SERIALIZER': 'lms.accounts.serializers.CustomLoginSerializer',
|
||||||
|
|
@ -373,7 +372,7 @@ SIMPLE_JWT = {
|
||||||
'ROTATE_REFRESH_TOKENS': True,
|
'ROTATE_REFRESH_TOKENS': True,
|
||||||
'BLACKLIST_AFTER_ROTATION': True,
|
'BLACKLIST_AFTER_ROTATION': True,
|
||||||
'ALGORITHM': 'HS256',
|
'ALGORITHM': 'HS256',
|
||||||
'SIGNING_KEY': SECRET_KEY,
|
# 'SIGNING_KEY': SECRET_KEY,
|
||||||
}
|
}
|
||||||
|
|
||||||
# django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup
|
# django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup
|
||||||
|
|
|
||||||
|
|
@ -81,3 +81,6 @@ INSTALLED_APPS += ["django_extensions"]
|
||||||
CELERY_TASK_EAGER_PROPAGATES = True
|
CELERY_TASK_EAGER_PROPAGATES = True
|
||||||
# Your stuff...
|
# Your stuff...
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
SIMPLE_JWT["SIGNING_KEY"]=SECRET_KEY
|
||||||
|
|
|
||||||
|
|
@ -204,3 +204,5 @@ SPECTACULAR_SETTINGS["SERVERS"] = [
|
||||||
]
|
]
|
||||||
# Your stuff...
|
# Your stuff...
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
SIMPLE_JWT["SIGNING_KEY"]=SECRET_KEY
|
||||||
|
|
@ -17,7 +17,6 @@ urlpatterns = [
|
||||||
# Django Admin, use {% url 'admin:index' %}
|
# Django Admin, use {% url 'admin:index' %}
|
||||||
path(settings.ADMIN_URL, admin.site.urls),
|
path(settings.ADMIN_URL, admin.site.urls),
|
||||||
# User management
|
# User management
|
||||||
path("users/", include("lms.users.urls", namespace="users")),
|
|
||||||
path("accounts/", include("allauth.urls")),
|
path("accounts/", include("allauth.urls")),
|
||||||
# Your stuff: custom urls includes go here
|
# Your stuff: custom urls includes go here
|
||||||
# ...
|
# ...
|
||||||
|
|
@ -45,7 +44,7 @@ urlpatterns += [
|
||||||
|
|
||||||
|
|
||||||
# API base url
|
# API base url
|
||||||
path("api/", include("config.api_router")),
|
# path("api/", include("config.api_router")),
|
||||||
# DRF auth token
|
# DRF auth token
|
||||||
path("api/auth-token/", obtain_auth_token),
|
path("api/auth-token/", obtain_auth_token),
|
||||||
path("api/schema/", SpectacularAPIView.as_view(), name="api-schema"),
|
path("api/schema/", SpectacularAPIView.as_view(), name="api-schema"),
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
from allauth.account.adapter import DefaultAccountAdapter
|
from allauth.account.adapter import DefaultAccountAdapter
|
||||||
from django.contrib.sites.models import Site
|
# from django.contrib.sites.models import Site
|
||||||
|
|
||||||
current_site = Site.objects.get_current()
|
# current_site = Site.objects.get_current()
|
||||||
site_domain = current_site.domain
|
site_domain = "current_site.domain"
|
||||||
|
|
||||||
class CustomAccountAdapter(DefaultAccountAdapter):
|
class CustomAccountAdapter(DefaultAccountAdapter):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from .models import *
|
||||||
|
from dj_rest_auth.serializers import LoginSerializer
|
||||||
|
from django.contrib.auth import authenticate
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from allauth.account.models import EmailAddress
|
||||||
|
from dj_rest_auth.registration.serializers import RegisterSerializer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from allauth.account.adapter import DefaultAccountAdapter
|
|
||||||
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
|
||||||
from allauth.socialaccount.models import SocialLogin
|
|
||||||
from django.http import HttpRequest
|
|
||||||
|
|
||||||
from lms.users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class AccountAdapter(DefaultAccountAdapter):
|
|
||||||
def is_open_for_signup(self, request: HttpRequest) -> bool:
|
|
||||||
return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)
|
|
||||||
|
|
||||||
|
|
||||||
class SocialAccountAdapter(DefaultSocialAccountAdapter):
|
|
||||||
def is_open_for_signup(
|
|
||||||
self,
|
|
||||||
request: HttpRequest,
|
|
||||||
sociallogin: SocialLogin,
|
|
||||||
) -> bool:
|
|
||||||
return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)
|
|
||||||
|
|
||||||
def populate_user(
|
|
||||||
self,
|
|
||||||
request: HttpRequest,
|
|
||||||
sociallogin: SocialLogin,
|
|
||||||
data: dict[str, typing.Any],
|
|
||||||
) -> User:
|
|
||||||
"""
|
|
||||||
Populates user information from social provider info.
|
|
||||||
|
|
||||||
See: https://docs.allauth.org/en/latest/socialaccount/advanced.html#creating-and-populating-user-instances
|
|
||||||
"""
|
|
||||||
user = super().populate_user(request, sociallogin, data)
|
|
||||||
if not user.name:
|
|
||||||
if name := data.get("name"):
|
|
||||||
user.name = name
|
|
||||||
elif first_name := data.get("first_name"):
|
|
||||||
user.name = first_name
|
|
||||||
if last_name := data.get("last_name"):
|
|
||||||
user.name += f" {last_name}"
|
|
||||||
return user
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
from allauth.account.decorators import secure_admin_login
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.contrib.auth import admin as auth_admin
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from .forms import UserAdminChangeForm
|
|
||||||
from .forms import UserAdminCreationForm
|
|
||||||
from .models import User
|
|
||||||
|
|
||||||
if settings.DJANGO_ADMIN_FORCE_ALLAUTH:
|
|
||||||
# Force the `admin` sign in process to go through the `django-allauth` workflow:
|
|
||||||
# https://docs.allauth.org/en/latest/common/admin.html#admin
|
|
||||||
admin.autodiscover()
|
|
||||||
admin.site.login = secure_admin_login(admin.site.login) # type: ignore[method-assign]
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(User)
|
|
||||||
class UserAdmin(auth_admin.UserAdmin):
|
|
||||||
form = UserAdminChangeForm
|
|
||||||
add_form = UserAdminCreationForm
|
|
||||||
fieldsets = (
|
|
||||||
(None, {"fields": ("email", "password")}),
|
|
||||||
(_("Personal info"), {"fields": ("name",)}),
|
|
||||||
(
|
|
||||||
_("Permissions"),
|
|
||||||
{
|
|
||||||
"fields": (
|
|
||||||
"is_active",
|
|
||||||
"is_staff",
|
|
||||||
"is_superuser",
|
|
||||||
"groups",
|
|
||||||
"user_permissions",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
|
||||||
)
|
|
||||||
list_display = ["email", "name", "is_superuser"]
|
|
||||||
search_fields = ["name"]
|
|
||||||
ordering = ["id"]
|
|
||||||
add_fieldsets = (
|
|
||||||
(
|
|
||||||
None,
|
|
||||||
{
|
|
||||||
"classes": ("wide",),
|
|
||||||
"fields": ("email", "password1", "password2"),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from lms.users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(serializers.ModelSerializer[User]):
|
|
||||||
class Meta:
|
|
||||||
model = User
|
|
||||||
fields = ["name", "url"]
|
|
||||||
|
|
||||||
extra_kwargs = {
|
|
||||||
"url": {"view_name": "api:user-detail", "lookup_field": "pk"},
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.mixins import ListModelMixin
|
|
||||||
from rest_framework.mixins import RetrieveModelMixin
|
|
||||||
from rest_framework.mixins import UpdateModelMixin
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.viewsets import GenericViewSet
|
|
||||||
|
|
||||||
from lms.users.models import User
|
|
||||||
|
|
||||||
from .serializers import UserSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(RetrieveModelMixin, ListModelMixin, UpdateModelMixin, GenericViewSet):
|
|
||||||
serializer_class = UserSerializer
|
|
||||||
queryset = User.objects.all()
|
|
||||||
lookup_field = "pk"
|
|
||||||
|
|
||||||
def get_queryset(self, *args, **kwargs):
|
|
||||||
assert isinstance(self.request.user.id, int)
|
|
||||||
return self.queryset.filter(id=self.request.user.id)
|
|
||||||
|
|
||||||
@action(detail=False)
|
|
||||||
def me(self, request):
|
|
||||||
serializer = UserSerializer(request.user, context={"request": request})
|
|
||||||
return Response(status=status.HTTP_200_OK, data=serializer.data)
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import contextlib
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class UsersConfig(AppConfig):
|
|
||||||
name = "lms.users"
|
|
||||||
verbose_name = _("Users")
|
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
with contextlib.suppress(ImportError):
|
|
||||||
import lms.users.signals # noqa: F401
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
|
|
||||||
def allauth_settings(request):
|
|
||||||
"""Expose some settings from django-allauth in templates."""
|
|
||||||
return {
|
|
||||||
"ACCOUNT_ALLOW_REGISTRATION": settings.ACCOUNT_ALLOW_REGISTRATION,
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
from allauth.account.forms import SignupForm
|
|
||||||
from allauth.socialaccount.forms import SignupForm as SocialSignupForm
|
|
||||||
from django.contrib.auth import forms as admin_forms
|
|
||||||
from django.forms import EmailField
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from .models import User
|
|
||||||
|
|
||||||
|
|
||||||
class UserAdminChangeForm(admin_forms.UserChangeForm):
|
|
||||||
class Meta(admin_forms.UserChangeForm.Meta): # type: ignore[name-defined]
|
|
||||||
model = User
|
|
||||||
field_classes = {"email": EmailField}
|
|
||||||
|
|
||||||
|
|
||||||
class UserAdminCreationForm(admin_forms.UserCreationForm):
|
|
||||||
"""
|
|
||||||
Form for User Creation in the Admin Area.
|
|
||||||
To change user signup, see UserSignupForm and UserSocialSignupForm.
|
|
||||||
"""
|
|
||||||
|
|
||||||
class Meta(admin_forms.UserCreationForm.Meta): # type: ignore[name-defined]
|
|
||||||
model = User
|
|
||||||
fields = ("email",)
|
|
||||||
field_classes = {"email": EmailField}
|
|
||||||
error_messages = {
|
|
||||||
"email": {"unique": _("This email has already been taken.")},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class UserSignupForm(SignupForm):
|
|
||||||
"""
|
|
||||||
Form that will be rendered on a user sign up section/screen.
|
|
||||||
Default fields will be added automatically.
|
|
||||||
Check UserSocialSignupForm for accounts created from social.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class UserSocialSignupForm(SocialSignupForm):
|
|
||||||
"""
|
|
||||||
Renders the form when user has signed up using social accounts.
|
|
||||||
Default fields will be added automatically.
|
|
||||||
See UserSignupForm otherwise.
|
|
||||||
"""
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from django.contrib.auth.hashers import make_password
|
|
||||||
from django.contrib.auth.models import UserManager as DjangoUserManager
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .models import User # noqa: F401
|
|
||||||
|
|
||||||
|
|
||||||
class UserManager(DjangoUserManager["User"]):
|
|
||||||
"""Custom manager for the User model."""
|
|
||||||
|
|
||||||
def _create_user(self, email: str, password: str | None, **extra_fields):
|
|
||||||
"""
|
|
||||||
Create and save a user with the given email and password.
|
|
||||||
"""
|
|
||||||
if not email:
|
|
||||||
msg = "The given email must be set"
|
|
||||||
raise ValueError(msg)
|
|
||||||
email = self.normalize_email(email)
|
|
||||||
user = self.model(email=email, **extra_fields)
|
|
||||||
user.password = make_password(password)
|
|
||||||
user.save(using=self._db)
|
|
||||||
return user
|
|
||||||
|
|
||||||
def create_user(self, email: str, password: str | None = None, **extra_fields): # type: ignore[override]
|
|
||||||
extra_fields.setdefault("is_staff", False)
|
|
||||||
extra_fields.setdefault("is_superuser", False)
|
|
||||||
return self._create_user(email, password, **extra_fields)
|
|
||||||
|
|
||||||
def create_superuser(self, email: str, password: str | None = None, **extra_fields): # type: ignore[override]
|
|
||||||
extra_fields.setdefault("is_staff", True)
|
|
||||||
extra_fields.setdefault("is_superuser", True)
|
|
||||||
|
|
||||||
if extra_fields.get("is_staff") is not True:
|
|
||||||
msg = "Superuser must have is_staff=True."
|
|
||||||
raise ValueError(msg)
|
|
||||||
if extra_fields.get("is_superuser") is not True:
|
|
||||||
msg = "Superuser must have is_superuser=True."
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
return self._create_user(email, password, **extra_fields)
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
import django.contrib.auth.models
|
|
||||||
import django.contrib.auth.validators
|
|
||||||
import django.utils.timezone
|
|
||||||
from django.db import migrations
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
import lms.users.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("auth", "0012_alter_user_first_name_max_length"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="User",
|
|
||||||
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",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"email",
|
|
||||||
models.EmailField(
|
|
||||||
unique=True, max_length=254, verbose_name="email address",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"is_staff",
|
|
||||||
models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
help_text="Designates whether the user can log into this admin site.",
|
|
||||||
verbose_name="staff status",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"is_active",
|
|
||||||
models.BooleanField(
|
|
||||||
default=True,
|
|
||||||
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
|
|
||||||
verbose_name="active",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"date_joined",
|
|
||||||
models.DateTimeField(
|
|
||||||
default=django.utils.timezone.now, verbose_name="date joined",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"name",
|
|
||||||
models.CharField(
|
|
||||||
blank=True, max_length=255, verbose_name="Name of User",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"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={
|
|
||||||
"verbose_name": "user",
|
|
||||||
"verbose_name_plural": "users",
|
|
||||||
"abstract": False,
|
|
||||||
},
|
|
||||||
managers=[
|
|
||||||
("objects", lms.users.models.UserManager()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
|
|
||||||
from typing import ClassVar
|
|
||||||
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
|
||||||
from django.db.models import CharField
|
|
||||||
from django.db.models import EmailField
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from .managers import UserManager
|
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
|
||||||
"""
|
|
||||||
Default custom user model for Learning Management System.
|
|
||||||
If adding fields that need to be filled at user signup,
|
|
||||||
check forms.SignupForm and forms.SocialSignupForms accordingly.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# First and last name do not cover name patterns around the globe
|
|
||||||
name = CharField(_("Name of User"), blank=True, max_length=255)
|
|
||||||
first_name = None # type: ignore[assignment]
|
|
||||||
last_name = None # type: ignore[assignment]
|
|
||||||
email = EmailField(_("email address"), unique=True)
|
|
||||||
username = None # type: ignore[assignment]
|
|
||||||
|
|
||||||
USERNAME_FIELD = "email"
|
|
||||||
REQUIRED_FIELDS = []
|
|
||||||
|
|
||||||
objects: ClassVar[UserManager] = UserManager()
|
|
||||||
|
|
||||||
def get_absolute_url(self) -> str:
|
|
||||||
"""Get URL for user's detail view.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: URL for user detail.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return reverse("users:detail", kwargs={"pk": self.id})
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
from celery import shared_task
|
|
||||||
|
|
||||||
from .models import User
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task()
|
|
||||||
def get_users_count():
|
|
||||||
"""A pointless Celery task to demonstrate usage."""
|
|
||||||
return User.objects.count()
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
from collections.abc import Sequence
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from factory import Faker
|
|
||||||
from factory import post_generation
|
|
||||||
from factory.django import DjangoModelFactory
|
|
||||||
|
|
||||||
from lms.users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class UserFactory(DjangoModelFactory[User]):
|
|
||||||
email = Faker("email")
|
|
||||||
name = Faker("name")
|
|
||||||
|
|
||||||
@post_generation
|
|
||||||
def password(self, create: bool, extracted: Sequence[Any], **kwargs): # noqa: FBT001
|
|
||||||
password = (
|
|
||||||
extracted
|
|
||||||
if extracted
|
|
||||||
else Faker(
|
|
||||||
"password",
|
|
||||||
length=42,
|
|
||||||
special_chars=True,
|
|
||||||
digits=True,
|
|
||||||
upper_case=True,
|
|
||||||
lower_case=True,
|
|
||||||
).evaluate(None, None, extra={"locale": None})
|
|
||||||
)
|
|
||||||
self.set_password(password)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _after_postgeneration(cls, instance, create, results=None):
|
|
||||||
"""Save again the instance if creating and at least one hook ran."""
|
|
||||||
if create and results and not cls._meta.skip_postgeneration_save:
|
|
||||||
# Some post-generation hooks ran, and may have modified us.
|
|
||||||
instance.save()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = User
|
|
||||||
django_get_or_create = ["email"]
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
import contextlib
|
|
||||||
from http import HTTPStatus
|
|
||||||
from importlib import reload
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
|
||||||
from django.urls import reverse
|
|
||||||
from pytest_django.asserts import assertRedirects
|
|
||||||
|
|
||||||
from lms.users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class TestUserAdmin:
|
|
||||||
def test_changelist(self, admin_client):
|
|
||||||
url = reverse("admin:users_user_changelist")
|
|
||||||
response = admin_client.get(url)
|
|
||||||
assert response.status_code == HTTPStatus.OK
|
|
||||||
|
|
||||||
def test_search(self, admin_client):
|
|
||||||
url = reverse("admin:users_user_changelist")
|
|
||||||
response = admin_client.get(url, data={"q": "test"})
|
|
||||||
assert response.status_code == HTTPStatus.OK
|
|
||||||
|
|
||||||
def test_add(self, admin_client):
|
|
||||||
url = reverse("admin:users_user_add")
|
|
||||||
response = admin_client.get(url)
|
|
||||||
assert response.status_code == HTTPStatus.OK
|
|
||||||
|
|
||||||
response = admin_client.post(
|
|
||||||
url,
|
|
||||||
data={
|
|
||||||
"email": "new-admin@example.com",
|
|
||||||
"password1": "My_R@ndom-P@ssw0rd",
|
|
||||||
"password2": "My_R@ndom-P@ssw0rd",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert response.status_code == HTTPStatus.FOUND
|
|
||||||
assert User.objects.filter(email="new-admin@example.com").exists()
|
|
||||||
|
|
||||||
def test_view_user(self, admin_client):
|
|
||||||
user = User.objects.get(email="admin@example.com")
|
|
||||||
url = reverse("admin:users_user_change", kwargs={"object_id": user.pk})
|
|
||||||
response = admin_client.get(url)
|
|
||||||
assert response.status_code == HTTPStatus.OK
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def _force_allauth(self, settings):
|
|
||||||
settings.DJANGO_ADMIN_FORCE_ALLAUTH = True
|
|
||||||
# Reload the admin module to apply the setting change
|
|
||||||
import lms.users.admin as users_admin
|
|
||||||
|
|
||||||
with contextlib.suppress(admin.sites.AlreadyRegistered): # type: ignore[attr-defined]
|
|
||||||
reload(users_admin)
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
@pytest.mark.usefixtures("_force_allauth")
|
|
||||||
def test_allauth_login(self, rf, settings):
|
|
||||||
request = rf.get("/fake-url")
|
|
||||||
request.user = AnonymousUser()
|
|
||||||
response = admin.site.login(request)
|
|
||||||
|
|
||||||
# The `admin` login view should redirect to the `allauth` login view
|
|
||||||
target_url = reverse(settings.LOGIN_URL) + "?next=" + request.path
|
|
||||||
assertRedirects(response, target_url, fetch_redirect_response=False)
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
from django.urls import resolve
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
from lms.users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_detail(user: User):
|
|
||||||
assert (
|
|
||||||
reverse("api:user-detail", kwargs={"pk": user.pk}) == f"/api/users/{user.pk}/"
|
|
||||||
)
|
|
||||||
assert resolve(f"/api/users/{user.pk}/").view_name == "api:user-detail"
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_list():
|
|
||||||
assert reverse("api:user-list") == "/api/users/"
|
|
||||||
assert resolve("/api/users/").view_name == "api:user-list"
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_me():
|
|
||||||
assert reverse("api:user-me") == "/api/users/me/"
|
|
||||||
assert resolve("/api/users/me/").view_name == "api:user-me"
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import pytest
|
|
||||||
from rest_framework.test import APIRequestFactory
|
|
||||||
|
|
||||||
from lms.users.api.views import UserViewSet
|
|
||||||
from lms.users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class TestUserViewSet:
|
|
||||||
@pytest.fixture
|
|
||||||
def api_rf(self) -> APIRequestFactory:
|
|
||||||
return APIRequestFactory()
|
|
||||||
|
|
||||||
def test_get_queryset(self, user: User, api_rf: APIRequestFactory):
|
|
||||||
view = UserViewSet()
|
|
||||||
request = api_rf.get("/fake-url/")
|
|
||||||
request.user = user
|
|
||||||
|
|
||||||
view.request = request
|
|
||||||
|
|
||||||
assert user in view.get_queryset()
|
|
||||||
|
|
||||||
def test_me(self, user: User, api_rf: APIRequestFactory):
|
|
||||||
view = UserViewSet()
|
|
||||||
request = api_rf.get("/fake-url/")
|
|
||||||
request.user = user
|
|
||||||
|
|
||||||
view.request = request
|
|
||||||
|
|
||||||
response = view.me(request) # type: ignore[call-arg, arg-type, misc]
|
|
||||||
|
|
||||||
assert response.data == {
|
|
||||||
"url": f"http://testserver/api/users/{user.pk}/",
|
|
||||||
"name": user.name,
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
"""Module for all Form Tests."""
|
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from lms.users.forms import UserAdminCreationForm
|
|
||||||
from lms.users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class TestUserAdminCreationForm:
|
|
||||||
"""
|
|
||||||
Test class for all tests related to the UserAdminCreationForm
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_username_validation_error_msg(self, user: User):
|
|
||||||
"""
|
|
||||||
Tests UserAdminCreation Form's unique validator functions correctly by testing:
|
|
||||||
1) A new user with an existing username cannot be added.
|
|
||||||
2) Only 1 error is raised by the UserCreation Form
|
|
||||||
3) The desired error message is raised
|
|
||||||
"""
|
|
||||||
|
|
||||||
# The user already exists,
|
|
||||||
# hence cannot be created.
|
|
||||||
form = UserAdminCreationForm(
|
|
||||||
{
|
|
||||||
"email": user.email,
|
|
||||||
"password1": user.password,
|
|
||||||
"password2": user.password,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert not form.is_valid()
|
|
||||||
assert len(form.errors) == 1
|
|
||||||
assert "email" in form.errors
|
|
||||||
assert form.errors["email"][0] == _("This email has already been taken.")
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from django.core.management import call_command
|
|
||||||
|
|
||||||
from lms.users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
class TestUserManager:
|
|
||||||
def test_create_user(self):
|
|
||||||
user = User.objects.create_user(
|
|
||||||
email="john@example.com",
|
|
||||||
password="something-r@nd0m!", # noqa: S106
|
|
||||||
)
|
|
||||||
assert user.email == "john@example.com"
|
|
||||||
assert not user.is_staff
|
|
||||||
assert not user.is_superuser
|
|
||||||
assert user.check_password("something-r@nd0m!")
|
|
||||||
assert user.username is None
|
|
||||||
|
|
||||||
def test_create_superuser(self):
|
|
||||||
user = User.objects.create_superuser(
|
|
||||||
email="admin@example.com",
|
|
||||||
password="something-r@nd0m!", # noqa: S106
|
|
||||||
)
|
|
||||||
assert user.email == "admin@example.com"
|
|
||||||
assert user.is_staff
|
|
||||||
assert user.is_superuser
|
|
||||||
assert user.username is None
|
|
||||||
|
|
||||||
def test_create_superuser_username_ignored(self):
|
|
||||||
user = User.objects.create_superuser(
|
|
||||||
email="test@example.com",
|
|
||||||
password="something-r@nd0m!", # noqa: S106
|
|
||||||
)
|
|
||||||
assert user.username is None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_createsuperuser_command():
|
|
||||||
"""Ensure createsuperuser command works with our custom manager."""
|
|
||||||
out = StringIO()
|
|
||||||
command_result = call_command(
|
|
||||||
"createsuperuser",
|
|
||||||
"--email",
|
|
||||||
"henry@example.com",
|
|
||||||
interactive=False,
|
|
||||||
stdout=out,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert command_result is None
|
|
||||||
assert out.getvalue() == "Superuser created successfully.\n"
|
|
||||||
user = User.objects.get(email="henry@example.com")
|
|
||||||
assert not user.has_usable_password()
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
from lms.users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_get_absolute_url(user: User):
|
|
||||||
assert user.get_absolute_url() == f"/users/{user.pk}/"
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
|
|
||||||
def test_swagger_accessible_by_admin(admin_client):
|
|
||||||
url = reverse("api-docs")
|
|
||||||
response = admin_client.get(url)
|
|
||||||
assert response.status_code == HTTPStatus.OK
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_swagger_ui_not_accessible_by_normal_user(client):
|
|
||||||
url = reverse("api-docs")
|
|
||||||
response = client.get(url)
|
|
||||||
assert response.status_code == HTTPStatus.FORBIDDEN
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_schema_generated_successfully(admin_client):
|
|
||||||
url = reverse("api-schema")
|
|
||||||
response = admin_client.get(url)
|
|
||||||
assert response.status_code == HTTPStatus.OK
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import pytest
|
|
||||||
from celery.result import EagerResult
|
|
||||||
|
|
||||||
from lms.users.tasks import get_users_count
|
|
||||||
from lms.users.tests.factories import UserFactory
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_count(settings):
|
|
||||||
"""A basic test to execute the get_users_count Celery task."""
|
|
||||||
batch_size = 3
|
|
||||||
UserFactory.create_batch(batch_size)
|
|
||||||
settings.CELERY_TASK_ALWAYS_EAGER = True
|
|
||||||
task_result = get_users_count.delay()
|
|
||||||
assert isinstance(task_result, EagerResult)
|
|
||||||
assert task_result.result == batch_size
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
from django.urls import resolve
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
from lms.users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
def test_detail(user: User):
|
|
||||||
assert reverse("users:detail", kwargs={"pk": user.pk}) == f"/users/{user.pk}/"
|
|
||||||
assert resolve(f"/users/{user.pk}/").view_name == "users:detail"
|
|
||||||
|
|
||||||
|
|
||||||
def test_update():
|
|
||||||
assert reverse("users:update") == "/users/~update/"
|
|
||||||
assert resolve("/users/~update/").view_name == "users:update"
|
|
||||||
|
|
||||||
|
|
||||||
def test_redirect():
|
|
||||||
assert reverse("users:redirect") == "/users/~redirect/"
|
|
||||||
assert resolve("/users/~redirect/").view_name == "users:redirect"
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
|
||||||
from django.contrib.messages.middleware import MessageMiddleware
|
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
|
||||||
from django.http import HttpRequest
|
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.test import RequestFactory
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from lms.users.forms import UserAdminChangeForm
|
|
||||||
from lms.users.models import User
|
|
||||||
from lms.users.tests.factories import UserFactory
|
|
||||||
from lms.users.views import UserRedirectView
|
|
||||||
from lms.users.views import UserUpdateView
|
|
||||||
from lms.users.views import user_detail_view
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
|
||||||
|
|
||||||
|
|
||||||
class TestUserUpdateView:
|
|
||||||
"""
|
|
||||||
TODO:
|
|
||||||
extracting view initialization code as class-scoped fixture
|
|
||||||
would be great if only pytest-django supported non-function-scoped
|
|
||||||
fixture db access -- this is a work-in-progress for now:
|
|
||||||
https://github.com/pytest-dev/pytest-django/pull/258
|
|
||||||
"""
|
|
||||||
|
|
||||||
def dummy_get_response(self, request: HttpRequest):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def test_get_success_url(self, user: User, rf: RequestFactory):
|
|
||||||
view = UserUpdateView()
|
|
||||||
request = rf.get("/fake-url/")
|
|
||||||
request.user = user
|
|
||||||
|
|
||||||
view.request = request
|
|
||||||
assert view.get_success_url() == f"/users/{user.pk}/"
|
|
||||||
|
|
||||||
def test_get_object(self, user: User, rf: RequestFactory):
|
|
||||||
view = UserUpdateView()
|
|
||||||
request = rf.get("/fake-url/")
|
|
||||||
request.user = user
|
|
||||||
|
|
||||||
view.request = request
|
|
||||||
|
|
||||||
assert view.get_object() == user
|
|
||||||
|
|
||||||
def test_form_valid(self, user: User, rf: RequestFactory):
|
|
||||||
view = UserUpdateView()
|
|
||||||
request = rf.get("/fake-url/")
|
|
||||||
|
|
||||||
# Add the session/message middleware to the request
|
|
||||||
SessionMiddleware(self.dummy_get_response).process_request(request)
|
|
||||||
MessageMiddleware(self.dummy_get_response).process_request(request)
|
|
||||||
request.user = user
|
|
||||||
|
|
||||||
view.request = request
|
|
||||||
|
|
||||||
# Initialize the form
|
|
||||||
form = UserAdminChangeForm()
|
|
||||||
form.cleaned_data = {}
|
|
||||||
form.instance = user
|
|
||||||
view.form_valid(form)
|
|
||||||
|
|
||||||
messages_sent = [m.message for m in messages.get_messages(request)]
|
|
||||||
assert messages_sent == [_("Information successfully updated")]
|
|
||||||
|
|
||||||
|
|
||||||
class TestUserRedirectView:
|
|
||||||
def test_get_redirect_url(self, user: User, rf: RequestFactory):
|
|
||||||
view = UserRedirectView()
|
|
||||||
request = rf.get("/fake-url")
|
|
||||||
request.user = user
|
|
||||||
|
|
||||||
view.request = request
|
|
||||||
assert view.get_redirect_url() == f"/users/{user.pk}/"
|
|
||||||
|
|
||||||
|
|
||||||
class TestUserDetailView:
|
|
||||||
def test_authenticated(self, user: User, rf: RequestFactory):
|
|
||||||
request = rf.get("/fake-url/")
|
|
||||||
request.user = UserFactory()
|
|
||||||
response = user_detail_view(request, pk=user.pk)
|
|
||||||
|
|
||||||
assert response.status_code == HTTPStatus.OK
|
|
||||||
|
|
||||||
def test_not_authenticated(self, user: User, rf: RequestFactory):
|
|
||||||
request = rf.get("/fake-url/")
|
|
||||||
request.user = AnonymousUser()
|
|
||||||
response = user_detail_view(request, pk=user.pk)
|
|
||||||
login_url = reverse(settings.LOGIN_URL)
|
|
||||||
|
|
||||||
assert isinstance(response, HttpResponseRedirect)
|
|
||||||
assert response.status_code == HTTPStatus.FOUND
|
|
||||||
assert response.url == f"{login_url}?next=/fake-url/"
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from .views import user_detail_view
|
|
||||||
from .views import user_redirect_view
|
|
||||||
from .views import user_update_view
|
|
||||||
|
|
||||||
app_name = "users"
|
|
||||||
urlpatterns = [
|
|
||||||
path("~redirect/", view=user_redirect_view, name="redirect"),
|
|
||||||
path("~update/", view=user_update_view, name="update"),
|
|
||||||
path("<int:pk>/", view=user_detail_view, name="detail"),
|
|
||||||
]
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.db.models import QuerySet
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.views.generic import DetailView
|
|
||||||
from django.views.generic import RedirectView
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
|
|
||||||
from lms.users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class UserDetailView(LoginRequiredMixin, DetailView):
|
|
||||||
model = User
|
|
||||||
slug_field = "id"
|
|
||||||
slug_url_kwarg = "id"
|
|
||||||
|
|
||||||
|
|
||||||
user_detail_view = UserDetailView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class UserUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
|
||||||
model = User
|
|
||||||
fields = ["name"]
|
|
||||||
success_message = _("Information successfully updated")
|
|
||||||
|
|
||||||
def get_success_url(self) -> str:
|
|
||||||
assert self.request.user.is_authenticated # type guard
|
|
||||||
return self.request.user.get_absolute_url()
|
|
||||||
|
|
||||||
def get_object(self, queryset: QuerySet | None=None) -> User:
|
|
||||||
assert self.request.user.is_authenticated # type guard
|
|
||||||
return self.request.user
|
|
||||||
|
|
||||||
|
|
||||||
user_update_view = UserUpdateView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class UserRedirectView(LoginRequiredMixin, RedirectView):
|
|
||||||
permanent = False
|
|
||||||
|
|
||||||
def get_redirect_url(self) -> str:
|
|
||||||
return reverse("users:detail", kwargs={"pk": self.request.user.pk})
|
|
||||||
|
|
||||||
|
|
||||||
user_redirect_view = UserRedirectView.as_view()
|
|
||||||
|
|
@ -24,6 +24,7 @@ django-redis==5.4.0 # https://github.com/jazzband/django-redis
|
||||||
# Django REST Framework
|
# Django REST Framework
|
||||||
djangorestframework==3.15.2 # https://github.com/encode/django-rest-framework
|
djangorestframework==3.15.2 # https://github.com/encode/django-rest-framework
|
||||||
dj-rest-auth
|
dj-rest-auth
|
||||||
|
djangorestframework-simplejwt
|
||||||
django-cors-headers==4.6.0 # https://github.com/adamchainz/django-cors-headers
|
django-cors-headers==4.6.0 # https://github.com/adamchainz/django-cors-headers
|
||||||
# DRF-spectacular for api documentation
|
# DRF-spectacular for api documentation
|
||||||
drf-spectacular==0.28.0 # https://github.com/tfranzel/drf-spectacular
|
drf-spectacular==0.28.0 # https://github.com/tfranzel/drf-spectacular
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue