Initial commit from ZIP

This commit is contained in:
kusz committed 2024-11-27 13:18:16 +01:00
commit 378d4692a9
454 files changed
+40367

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
from rest_framework.authentication import TokenAuthentication as BaseTokenAuthentication
class TokenAuthentication(BaseTokenAuthentication):
keyword = "Bearer"
class AuthenticationAppConfig(AppConfig):
name = "outbo.apps.authentication"
label = "authentication"
verbose_name = _("Authentication")
default_app_config = "outbo.apps.authentication.AuthenticationAppConfig"
+35
View File
@@ -0,0 +1,35 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import gettext_lazy as _
from outbo.apps.authentication.models import User
@admin.register(User)
class UserAdmin(BaseUserAdmin):
fieldsets = (
(None, {"fields": ("username", "password")}),
(
_("Personal info"),
{"fields": ("first_name", "last_name", "email", "photo", "phone_number")},
),
(_("Organization info"), {"fields": ("organization", "job_title", "is_observer", "is_trader", "category")}),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": ("username", "email", "password1", "password2", "is_observer", "is_trader", "category"),
},
),
)
list_display = ("username", "email", "first_name", "last_name")
list_filter = ()
search_fields = ("username", "first_name", "last_name", "email")
ordering = ("username",)
filter_horizontal = ()
autocomplete_fields = ["organization"]
def get_queryset(self, request):
return super().get_queryset(request).filter(is_superuser=False)
+41
View File
@@ -0,0 +1,41 @@
from django.contrib.auth.base_user import BaseUserManager
from outbo.apps.core.models import SoftDeletableManager
class UserManager(SoftDeletableManager, BaseUserManager):
def __init__(self, *, alive_only=False):
self.alive_only = alive_only
super().__init__()
def create_user(self, username, email, password=None, save=True, **kwargs):
if username is None:
raise TypeError("User must have a username.")
if email is None:
raise TypeError("User must have an email address.")
user = self.model(
username=username, email=self.normalize_email(email), **kwargs
)
user.set_password(password)
if save:
user.save()
return user
def create_superuser(self, username, email, password, save=True):
if password is None:
raise TypeError("Superuser must have a password.")
user = self.create_user(
username=username, email=email, password=password, save=False
)
user.is_superuser = True
user.is_staff = True
if save:
user.save()
return user
def regular_users(self):
return self.filter(is_staff=False)
@@ -0,0 +1,131 @@
# Generated by Django 2.2.7 on 2019-11-29 08:57
import django.contrib.auth.validators
from django.db import migrations, models
import outbo.apps.core.fields
import rules.contrib.models
class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0011_update_proxy_permissions"),
]
operations = [
migrations.CreateModel(
name="User",
fields=[
(
"id",
models.AutoField(
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",
),
),
(
"created_at",
outbo.apps.core.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="Created at"
),
),
(
"updated_at",
outbo.apps.core.fields.ModificationDateTimeField(
auto_now=True, verbose_name="Updated at"
),
),
(
"deleted_at",
outbo.apps.core.fields.RemovalDateTimeField(
blank=True, editable=False, null=True, verbose_name="Deleted at"
),
),
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username",
),
),
(
"email",
models.EmailField(
blank=True,
max_length=254,
unique=True,
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",
),
),
(
"first_name",
models.CharField(
blank=True, max_length=30, verbose_name="first name"
),
),
(
"last_name",
models.CharField(
blank=True, max_length=150, verbose_name="last name"
),
),
(
"photo",
models.ImageField(
blank=True,
null=True,
upload_to="users/",
verbose_name="profile photo",
),
),
(
"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",
),
),
],
options={"verbose_name": "user", "verbose_name_plural": "users",},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
]
@@ -0,0 +1,41 @@
# Generated by Django 2.2.7 on 2019-11-29 08:57
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentication", "0001_initial"),
("organizations", "0001_initial"),
("auth", "0011_update_proxy_permissions"),
]
operations = [
migrations.AddField(
model_name="user",
name="organization",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="organizations.Organization",
verbose_name="organization",
),
),
migrations.AddField(
model_name="user",
name="user_permissions",
field=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",
),
),
]
@@ -0,0 +1,25 @@
# Generated by Django 2.2.8 on 2020-01-16 12:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentication", "0002_auto_20191129_0957"),
]
operations = [
migrations.AddField(
model_name="user",
name="job_title",
field=models.CharField(blank=True, max_length=30, verbose_name="job title"),
),
migrations.AddField(
model_name="user",
name="phone_number",
field=models.CharField(
blank=True, max_length=30, verbose_name="phone_number"
),
),
]
@@ -0,0 +1,24 @@
# Generated by Django 2.2.8 on 2020-02-11 13:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentication", "0003_auto_20200116_1318"),
]
operations = [
migrations.AlterField(
model_name="user",
name="email",
field=models.EmailField(
blank=True,
max_length=254,
null=True,
unique=True,
verbose_name="email address",
),
),
]
@@ -0,0 +1,25 @@
# Generated by Django 2.2.10 on 2020-05-25 08:25
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("authentication", "0004_auto_20200211_1405"),
]
operations = [
migrations.AlterField(
model_name="user",
name="organization",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="organizations.Organization",
verbose_name="organization",
),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 2.2.16 on 2020-11-03 13:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0005_auto_20200525_1025'),
]
operations = [
migrations.AddField(
model_name='user',
name='is_observer',
field=models.BooleanField(default=False, help_text='Designates whether the user is an observer.', verbose_name='observer status'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 2.2.22 on 2021-05-11 07:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0006_user_is_observer'),
]
operations = [
migrations.AddField(
model_name='user',
name='is_trader',
field=models.BooleanField(default=False, help_text='Designates whether the user is an trader.', verbose_name='trader status'),
),
]
@@ -0,0 +1,20 @@
# Generated by Django 2.2.22 on 2021-05-11 09:28
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('organizations', '0002_auto_20200525_1025'),
('authentication', '0007_user_is_trader'),
]
operations = [
migrations.AddField(
model_name='user',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='organizations.Category', verbose_name='category'),
),
]
@@ -0,0 +1,19 @@
# Generated by Django 2.2.22 on 2021-05-17 10:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('authentication', '0008_user_category'),
]
operations = [
migrations.AlterField(
model_name='user',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='organizations.Category', verbose_name='category for trader'),
),
]
Whitespace-only changes.
+160
View File
@@ -0,0 +1,160 @@
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.contrib.contenttypes.models import ContentType
from django.core.mail import send_mail
from django.db import models
from django.utils.translation import gettext_lazy as _
from rules.contrib.models import RulesModelMixin, RulesModelBase
from outbo.apps.authentication import rules
from outbo.apps.authentication.managers import UserManager
from outbo.apps.core.models import TimestampableDeletableMixin
class User(
RulesModelMixin,
TimestampableDeletableMixin,
AbstractBaseUser,
PermissionsMixin,
metaclass=RulesModelBase,
):
objects = UserManager()
all_objects = UserManager(alive_only=False)
class Meta:
verbose_name = _("user")
verbose_name_plural = _("users")
rules_permissions = {
"read": rules.can_read_user,
"read_current": rules.can_read_current_user,
}
username_validator = UnicodeUsernameValidator()
username = models.CharField(
verbose_name=_("username"),
max_length=150,
unique=True,
help_text=_(
"Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
),
validators=[username_validator],
error_messages={"unique": _("A user with that username already exists.")},
)
email = models.EmailField(
verbose_name=_("email address"), blank=True, null=True, unique=True
)
is_staff = models.BooleanField(
verbose_name=_("staff status"),
default=False,
help_text=_("Designates whether the user can log into this admin site."),
)
is_observer = models.BooleanField(
verbose_name=_("observer status"),
default=False,
help_text=_("Designates whether the user is an observer."),
)
is_trader = models.BooleanField(
verbose_name=_("trader status"),
default=False,
help_text=_("Designates whether the user is an trader."),
)
first_name = models.CharField(
verbose_name=_("first name"), max_length=30, blank=True
)
last_name = models.CharField(
verbose_name=_("last name"), max_length=150, blank=True
)
organization = models.ForeignKey(
verbose_name=_("organization"),
to="organizations.Organization",
on_delete=models.CASCADE,
null=True,
default=None,
)
category = models.ForeignKey(
verbose_name=_("category for trader"),
to="organizations.Category",
on_delete=models.CASCADE,
blank=True,
null=True,
)
photo = models.ImageField(
verbose_name=_("profile photo"), upload_to="users/", blank=True, null=True
)
job_title = models.CharField(verbose_name=_("job title"), max_length=30, blank=True)
phone_number = models.CharField(
verbose_name=_("phone_number"), max_length=30, blank=True
)
USERNAME_FIELD = "username"
EMAIL_FIELD = "email"
REQUIRED_FIELDS = []
@property
def photo_url(self):
return None if not self.photo else self.photo.url
@property
def is_active(self):
return self.deleted_at is None
@property
def full_name(self):
return f"{self.first_name} {self.last_name}".strip()
@property
def is_agency(self):
return (
False
if not self.organization
else self.organization.type
== ContentType.objects.get(
app_label="organizations", model="agencyorganization"
)
)
@property
def is_client(self):
return (
False
if not self.organization
else self.organization.type
== ContentType.objects.get(
app_label="organizations", model="clientorganization"
)
)
@property
def is_supplier(self):
return (
False
if not self.organization
else self.organization.type
== ContentType.objects.get(
app_label="organizations", model="supplierorganization"
)
)
@property
def user_role(self):
from outbo.apps.projects.models.member import MembershipType
if self.is_client:
return MembershipType.CLIENT
elif self.is_agency:
return MembershipType.AGENCY
elif self.is_supplier:
return MembershipType.SUPPLIER
def email_user(self, subject, message, from_email=None, **kwargs):
"""Send an email to this user."""
send_mail(subject, message, from_email, [self.email], **kwargs)
+10
View File
@@ -0,0 +1,10 @@
from rules import predicate
@predicate
def _is_active(user):
return user.is_active
can_read_user = _is_active
can_read_current_user = _is_active
+74
View File
@@ -0,0 +1,74 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.authtoken.serializers import (
AuthTokenSerializer as BaseAuthTokenSerializer,
)
from outbo.apps.authentication.models import User
from outbo.apps.organizations.models import Organization
from outbo.apps.organizations.serializers import OrganizationSerializer
class AuthTokenSerializer(BaseAuthTokenSerializer):
def validate(self, attrs):
validated_attrs = super().validate(attrs)
user = validated_attrs["user"]
if user.is_staff:
msg = _("Administrators cannot login via API.")
raise serializers.ValidationError(msg, code="authorization")
return validated_attrs
class _BaseUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
class UserSerializer:
class List(_BaseUserSerializer):
organization_name = serializers.SlugRelatedField(
source="organization", slug_field="name", read_only=True
)
photo = serializers.SerializerMethodField()
class Meta(_BaseUserSerializer.Meta):
fields = [
"id",
"full_name",
"organization_name",
"photo",
"email",
"phone_number",
"job_title",
]
def get_photo(self, instance: User):
if instance.photo_url:
return self.context["request"].build_absolute_uri(instance.photo_url)
class Retrieve(_BaseUserSerializer):
organization = serializers.SerializerMethodField()
photo = serializers.SerializerMethodField()
class Meta(_BaseUserSerializer.Meta):
fields = [
"id",
"username",
"full_name",
"organization",
"photo",
"email",
"phone_number",
"job_title",
"is_observer",
"is_trader",
]
def get_photo(self, instance: User):
if instance.photo_url:
return self.context["request"].build_absolute_uri(instance.photo_url)
# NOTE: Hacky workaround
def get_organization(self, instance):
serializer = OrganizationSerializer.List(read_only=True)
serializer.instance = Organization.objects.get(id=instance.organization_id)
return serializer.data
+13
View File
@@ -0,0 +1,13 @@
from django.urls import path
from rest_framework import routers
from outbo.apps.authentication.views import ObtainAuthToken
from outbo.apps.authentication.viewsets import UserViewSet
app_name = "authentication"
router = routers.SimpleRouter()
router.register(prefix="users", viewset=UserViewSet)
view_urls = [path("token/", ObtainAuthToken.as_view(), name="token")]
urlpatterns = view_urls + router.urls
+19
View File
@@ -0,0 +1,19 @@
from djangorestframework_camel_case.parser import (
CamelCaseFormParser,
CamelCaseMultiPartParser,
CamelCaseJSONParser,
)
from djangorestframework_camel_case.render import CamelCaseJSONRenderer
from rest_framework.authtoken.views import ObtainAuthToken as BaseObtainAuthToken
from outbo.apps.authentication.serializers import AuthTokenSerializer
class ObtainAuthToken(BaseObtainAuthToken):
serializer_class = AuthTokenSerializer
renderer_classes = [CamelCaseJSONRenderer]
parser_classes = [
CamelCaseFormParser,
CamelCaseMultiPartParser,
CamelCaseJSONParser,
]
+32
View File
@@ -0,0 +1,32 @@
from rest_framework.decorators import action
from rest_framework.response import Response
from outbo.apps.authentication.models import User
from outbo.apps.authentication.serializers import UserSerializer
from outbo.apps.core.viewsets import (
MultipleSerializerViewSet,
AutoPermissionViewSetMixin,
ListModelMixin,
RetrieveModelMixin,
)
class UserViewSet(
RetrieveModelMixin,
ListModelMixin,
AutoPermissionViewSetMixin,
MultipleSerializerViewSet,
):
queryset = User.objects.regular_users()
serializer_class_set = UserSerializer
serializer_map = {**MultipleSerializerViewSet.serializer_map, "me": "Retrieve"}
permission_type_map = {
**AutoPermissionViewSetMixin.permission_type_map,
"me": "read_current",
}
@action(detail=False, methods=["get"])
def me(self, request):
current_user = request.user
serializer = self.get_serializer(current_user)
return Response(serializer.data)