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

View File
Whitespace-only changes.
View File
Whitespace-only changes.
View File
Whitespace-only changes.
+7
View File
@@ -0,0 +1,7 @@
from enum import Enum
class MessageType(Enum):
USER = "User"
SUCCESS = "Success"
FAILURE = "Failure"
@@ -0,0 +1,107 @@
# Generated by Django 2.2.7 on 2019-11-29 08:57
from django.db import migrations, models
import django.db.models.deletion
import outbo.apps.activity_log.enums
import outbo.apps.core.fields
import rules.contrib.models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="LogItem",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"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"
),
),
("message", models.TextField(verbose_name="message")),
(
"message_type",
models.CharField(
choices=[
(outbo.apps.activity_log.enums.MessageType("User"), "User"),
(
outbo.apps.activity_log.enums.MessageType("Success"),
"Success",
),
(
outbo.apps.activity_log.enums.MessageType("Failure"),
"Failure",
),
],
default=outbo.apps.activity_log.enums.MessageType("User"),
max_length=25,
verbose_name="message type",
),
),
],
options={"abstract": False,},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.CreateModel(
name="ProductLogItem",
fields=[
(
"logitem_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="activity_log.LogItem",
),
),
],
options={"ordering": ["-created_at"],},
bases=("activity_log.logitem",),
),
migrations.CreateModel(
name="ProjectLogItem",
fields=[
(
"logitem_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="activity_log.LogItem",
),
),
],
options={"ordering": ["-created_at"],},
bases=("activity_log.logitem",),
),
]
@@ -0,0 +1,39 @@
# Generated by Django 2.2.7 on 2019-11-29 08:57
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("activity_log", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="logitem",
name="created_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
verbose_name="created by",
),
),
migrations.AddField(
model_name="logitem",
name="polymorphic_ctype",
field=models.ForeignKey(
editable=False,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="polymorphic_activity_log.logitem_set+",
to="contenttypes.ContentType",
),
),
]
@@ -0,0 +1,28 @@
# 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 = [
("projects", "0001_initial"),
("activity_log", "0002_auto_20191129_0957"),
]
operations = [
migrations.AddField(
model_name="projectlogitem",
name="project",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="log_items",
related_query_name="log_item",
to="projects.Project",
verbose_name="project",
),
),
]
@@ -0,0 +1,38 @@
# 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 = [
("activity_log", "0003_projectlogitem_project"),
("products", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="productlogitem",
name="product",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="log_items",
related_query_name="log_item",
to="products.Product",
verbose_name="product",
),
),
migrations.AddField(
model_name="productlogitem",
name="stage",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="products.ProductStage",
),
),
]
@@ -0,0 +1,56 @@
# Generated by Django 2.2.10 on 2020-05-25 08:25
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("activity_log", "0004_auto_20191129_0957"),
]
operations = [
migrations.AlterField(
model_name="logitem",
name="created_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="created by",
),
),
migrations.AlterField(
model_name="productlogitem",
name="product",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="log_items",
related_query_name="log_item",
to="products.Product",
verbose_name="product",
),
),
migrations.AlterField(
model_name="productlogitem",
name="stage",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="products.ProductStage",
),
),
migrations.AlterField(
model_name="projectlogitem",
name="project",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="log_items",
related_query_name="log_item",
to="projects.Project",
verbose_name="project",
),
),
]
Whitespace-only changes.
+65
View File
@@ -0,0 +1,65 @@
from django.db import models
from django.utils.translation import pgettext, gettext_lazy as _
from rules import always_allow
from outbo.apps.activity_log.enums import MessageType
from outbo.apps.core.models import (
TimestampableDeletableMixin,
RulesPolymorphicModel,
SoftDeletablePolymorphicManager,
)
class LogItem(TimestampableDeletableMixin, RulesPolymorphicModel):
objects = SoftDeletablePolymorphicManager()
all_objects = SoftDeletablePolymorphicManager(alive_only=False)
message = models.TextField(verbose_name=_("message"))
message_type = models.CharField(
verbose_name=_("message type"),
max_length=25,
blank=False,
null=False,
default=MessageType.USER,
choices=((type, type.value) for type in MessageType),
)
created_by = models.ForeignKey(
verbose_name=_("created by"), to="authentication.User", on_delete=models.CASCADE
)
def __str__(self):
if self.message_type == MessageType.USER:
return self.message
return pgettext("log_item_messages", self.message)
class ProductLogItem(LogItem):
class Meta:
rules_permissions = {"view": always_allow, "add": always_allow}
ordering = ["-created_at"]
product = models.ForeignKey(
verbose_name=_("product"),
to="products.Product",
on_delete=models.CASCADE,
related_name="log_items",
related_query_name="log_item",
)
stage = models.ForeignKey(
to="products.ProductStage", on_delete=models.CASCADE, null=True, default=None
)
class ProjectLogItem(LogItem):
class Meta:
rules_permissions = {"view": always_allow, "add": always_allow}
ordering = ["-created_at"]
project = models.ForeignKey(
verbose_name=_("project"),
to="projects.Project",
on_delete=models.CASCADE,
related_name="log_items",
related_query_name="log_item",
)
+84
View File
@@ -0,0 +1,84 @@
from rest_framework import serializers
from outbo.apps.activity_log.enums import MessageType
from outbo.apps.activity_log.models import ProductLogItem, ProjectLogItem
class BaseProductLogItemSerializer(serializers.ModelSerializer):
class Meta:
model = ProductLogItem
def get_message_type(self, instance):
t, val = str(instance.message_type).split(".", 1)
return MessageType[val].value
class ProductLogItemSerializer:
class List(BaseProductLogItemSerializer):
message_type = serializers.SerializerMethodField()
class Meta(BaseProductLogItemSerializer.Meta):
fields = [
"message",
"message_type",
"stage",
"updated_at",
"created_at",
"created_by",
]
class Create(BaseProductLogItemSerializer):
class Meta(BaseProductLogItemSerializer.Meta):
fields = ["message"]
class AfterCreate(BaseProductLogItemSerializer):
message_type = serializers.SerializerMethodField()
class Meta(BaseProductLogItemSerializer.Meta):
fields = [
"message",
"message_type",
"stage",
"updated_at",
"created_at",
"created_by",
]
class BaseProjectLogItemSerializer(serializers.ModelSerializer):
class Meta:
model = ProjectLogItem
def get_message_type(self, instance):
t, val = str(instance.message_type).split(".", 1)
return MessageType[val].value
class ProjectLogItemSerializer:
class List(BaseProjectLogItemSerializer):
message_type = serializers.SerializerMethodField()
class Meta(BaseProjectLogItemSerializer.Meta):
fields = [
"message",
"message_type",
"updated_at",
"created_at",
"created_by",
]
class Create(BaseProjectLogItemSerializer):
class Meta(BaseProjectLogItemSerializer.Meta):
fields = ["message"]
class AfterCreate(BaseProjectLogItemSerializer):
message_type = serializers.SerializerMethodField()
class Meta(BaseProjectLogItemSerializer.Meta):
fields = [
"message",
"message_type",
"updated_at",
"created_at",
"created_by",
]
+2
View File
@@ -0,0 +1,2 @@
app_name = "activity_log"
urlpatterns = []
+48
View File
@@ -0,0 +1,48 @@
from outbo.apps.activity_log.models import ProductLogItem, ProjectLogItem
from outbo.apps.activity_log.serializers import (
ProductLogItemSerializer,
ProjectLogItemSerializer,
)
from outbo.apps.core.viewsets import (
AutoPermissionViewSetMixin,
ListModelMixin,
MultipleSerializerViewSet,
NestedViewSetMixin,
CreateModelMixin,
)
from outbo.apps.products.models import Product
from outbo.apps.projects.models import Project
class ProductLogItemNestedViewSet(
AutoPermissionViewSetMixin,
ListModelMixin,
CreateModelMixin,
NestedViewSetMixin,
MultipleSerializerViewSet,
):
queryset = ProductLogItem.objects.all()
serializer_class_set = ProductLogItemSerializer
def perform_create(self, *, serializer):
current_user = self.request.user
parents = self.get_parents_query_dict()
product = Product.objects.get(pk=parents["product_id"])
return serializer.save(product=product, created_by=current_user)
class ProjectLogItemNestedViewSet(
AutoPermissionViewSetMixin,
ListModelMixin,
CreateModelMixin,
NestedViewSetMixin,
MultipleSerializerViewSet,
):
queryset = ProjectLogItem.objects.all()
serializer_class_set = ProjectLogItemSerializer
def perform_create(self, *, serializer):
current_user = self.request.user
parents = self.get_parents_query_dict()
project = Project.objects.get(pk=parents["project_id"])
return serializer.save(project=project, created_by=current_user)
+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)
View File
Whitespace-only changes.
+18
View File
@@ -0,0 +1,18 @@
from nested_admin.nested import NestedTabularInline
from polymorphic.admin import (
PolymorphicParentModelAdmin as BasePolymorphicParentModelAdmin,
)
class PolymorphicParentModelAdmin(BasePolymorphicParentModelAdmin):
def get_child_type_choices(self, request, action):
choices = super().get_child_type_choices(request=request, action=action)
return tuple((ct_id, label.title()) for (ct_id, label) in choices)
class NestedTabularSingleItemInline(NestedTabularInline):
template = "nesting/admin/inlines/tabular_single.html"
class NestedStackedSingleItemInline(NestedTabularInline):
template = "nesting/admin/inlines/stacked_single.html"
+122
View File
@@ -0,0 +1,122 @@
from functools import partial
from django import forms
from django.db import models
from django_extensions.db.fields import (
CreationDateTimeField as BaseCreationDateTimeField,
ModificationDateTimeField as BaseModificationDateTimeField,
)
from django_fsm import FSMFieldMixin
from outbo.apps.core.forms import EnumChoiceField
class CreationDateTimeField(BaseCreationDateTimeField):
"""
CreationDateTimeField
By default, sets editable=False, blank=True, auto_now_add=True
"""
class ModificationDateTimeField(BaseModificationDateTimeField):
"""
ModificationDateTimeField
By default, sets editable=False, blank=True, auto_now=True
Sets value to now every time the object is saved.
"""
class RemovalDateTimeField(models.DateTimeField):
"""
RemovalDateTimeField
By default, sets editable=False, blank=True, null=True
"""
def __init__(self, *args, **kwargs):
kwargs.setdefault("editable", False)
kwargs.setdefault("blank", True)
kwargs.setdefault("null", True)
models.DateTimeField.__init__(self, *args, **kwargs)
def get_internal_type(self):
return "DateTimeField"
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
if self.editable is not False:
kwargs["editable"] = True
if self.blank is not True:
kwargs["blank"] = False
if self.null is not True:
kwargs["null"] = False
return name, path, args, kwargs
class FSMKeyField(FSMFieldMixin, models.ForeignKey):
def get_state(self, instance):
return instance.__dict__[self.attname]
def set_state(self, instance, state):
instance.__dict__[self.attname] = self.to_python(state)
class EnumCharField(models.CharField):
def __init__(self, *args, **kwargs):
enum = kwargs.pop("enum", None)
if enum is not None:
if kwargs.get("choices", None) is not None:
raise ValueError("Cannot provide choices for EnumCharField")
kwargs["choices"] = [(item, item.value) for item in enum]
super().__init__(*args, **kwargs)
self.validators.pop() # lengthvalidator not necessary on this one
self.enum = enum
def get_internal_type(self):
return "CharField"
def formfield(self, **kwargs):
enum_form_class = partial(EnumChoiceField, enum=self.enum)
defaults = {
"widget": forms.Select,
"form_class": enum_form_class,
"choices": [(str(item), item.value) for item in self.enum],
}
defaults.update(kwargs)
return super().formfield(**defaults)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
kwargs["enum"] = self.enum
if "choices" in kwargs:
del kwargs["choices"]
return name, path, args, kwargs
def to_python(self, value):
value = super().to_python(value)
if value is not None:
if not isinstance(value, self.enum):
enum_name, enum_value = value.split(".", 1)
if enum_name == self.enum.__name__:
return self.enum[enum_value]
return value
def get_prep_value(self, value):
value = super().get_prep_value(value)
if value is None:
return value
if isinstance(value, self.enum):
return str(value)
return value
def from_db_value(self, value, *args):
if value is not None:
enum_name, enum_value = value.split(".", 1)
if enum_name == self.enum.__name__:
return self.enum[enum_value]
return value
+29
View File
@@ -0,0 +1,29 @@
from django import forms
from django.core.exceptions import ValidationError
class EnumChoiceField(forms.ChoiceField):
def __init__(self, enum, **kwargs):
kwargs.setdefault("choices", [(item, item.value) for item in enum])
super().__init__(**kwargs)
self.enum = enum
def prepare_value(self, value):
if isinstance(value, self.enum):
return str(value)
return value
def to_python(self, value):
value = super().to_python(value)
if not isinstance(value, self.enum):
try:
enum_name, enum_value = value.split(".", 1)
if enum_name == self.enum.__name__:
value = self.enum[enum_value]
except ValueError:
raise ValidationError(self.error_messages["invalid"], code="invalid")
return value
def clean(self, value):
value = super().clean(value)
return self.enum(value)
View File
Whitespace-only changes.
Whitespace-only changes.
+236
View File
@@ -0,0 +1,236 @@
from typing import TYPE_CHECKING, Tuple, Iterable
from django.core.management.base import BaseCommand
from faker import Factory
from tests.factories.authentication import (
UserFactory,
AgencyUserFactory,
ClientUserFactory,
SupplierUserFactory,
)
from tests.factories.organizations import (
CategoryFactory,
BrandFactory,
)
from tests.factories.products import ProductStageFactory, PackagingTypeFactory
from tests.factories.token import TokenFactory
faker = Factory()
if TYPE_CHECKING:
from outbo.apps.organizations.models import ClientOrganization
class Command(BaseCommand):
help = "Seed with some existing data"
def add_arguments(self, parser):
parser.add_argument("--dev", action="store_true", help="Seed with dev data")
def __make_users_and_organizations(self, *, password=None):
self.stdout.write("Seeding organizations and users...")
if password is None:
password = "password"
UserFactory(
username="admin",
password=password,
email="admin@example.com",
is_superuser=True,
is_staff=True,
)
agency = AgencyUserFactory(
username="agency",
password=password,
email="agency@example.com",
organization__name="Sample agency",
)
client = ClientUserFactory(
username="client",
password=password,
email="client@example.com",
organization__name="Sample client",
)
supplier = SupplierUserFactory(
username="supplier",
password=password,
email="supplier@example.com",
organization__name="Sample supplier",
)
TokenFactory(user=agency, key="6f9afaa36ee8b224181dc14e2ca9af33e97c0f71")
TokenFactory(user=client, key="481372f60325289029679df25c72e6ccd05892ed")
TokenFactory(user=supplier, key="d23d8f187614c8a76fe7316470e6f50ff5bc4188")
def __make_product_stages(self):
self.stdout.write("Seeding product stages...")
ProductStageFactory(
id="waiting_for_brief",
label="1",
group=1,
description="Briefing",
next_action="Oczekuje na uzupełnienie briefu",
order=1,
)
ProductStageFactory(
id="waiting_for_brief_accept_by_agency",
label="1A",
group=1,
description="Akceptacja agencji",
next_action="Oczekuje na akceptację agencji",
order=2,
)
ProductStageFactory(
id="waiting_for_brief_accept_by_client",
label="1B",
group=1,
description="Akceptacja klienta",
next_action="Oczekuje na akceptację klienta",
order=3,
)
ProductStageFactory(
id="waiting_for_draft",
label="2A",
group=2,
description="Publikacja draftu",
next_action="Oczekuje na publikację draftu",
order=4,
)
ProductStageFactory(
id="waiting_for_draft_accept_by_supplier",
label="2B",
group=2,
description="Akceptacja dostawcy",
next_action="Oczekuje na akceptację dostawcy",
order=5,
)
ProductStageFactory(
id="waiting_for_draft_accept_by_client",
label="2C",
group=2,
description="Akceptacja klienta",
next_action="Oczekuje na akceptację klientget_random_stringa",
order=6,
)
ProductStageFactory(
id="waiting_for_ff",
label="3A",
group=3,
description="Publikacja FF",
next_action="Oczekuje na publikację draftu",
order=7,
)
ProductStageFactory(
id="waiting_for_ff_accept_by_supplier",
label="3B",
group=3,
description="Akceptacja dostawcy",
next_action="Oczekuje na akceptację dostawcy",
order=8,
)
ProductStageFactory(
id="waiting_for_ff_accept_by_client",
label="3C",
group=3,
description="Akceptacja klienta",
next_action="Oczekuje na akceptację klienta",
order=9,
)
ProductStageFactory(
id="waiting_for_pf",
label="4A",
group=4,
description="Publikacja printing files",
next_action="Oczekuje na publikację printing files",
order=10,
)
ProductStageFactory(
id="waiting_for_pf_accept_by_supplier",
label="4B",
group=4,
description="Akceptacja dostawcy",
next_action="Oczekuje na akceptację dostawcy",
order=11,
)
ProductStageFactory(
id="waiting_for_pf_accept_by_client",
label="4C",
group=4,
description="Akceptacja klienta",
next_action="Oczekuje na akceptację klienta",
order=12,
)
ProductStageFactory(
id="completed",
label="5",
group=5,
description="Zako\u0144czono",
next_action="Oczekuje na archiwizację",
order=13,
)
def __make_packaging_types(self, *, items: Iterable[str] = None):
self.stdout.write("Seeding packaging types...")
if items:
for name in items:
PackagingTypeFactory(name=name)
else:
PackagingTypeFactory.create_batch(size=20)
def __make_brands(
self, *, items: Iterable[Tuple[str, "ClientOrganization"]] = None
):
self.stdout.write("Seeding brands...")
if items:
for name, client in items:
BrandFactory(name=name, client=client)
else:
BrandFactory.create_batch(size=20)
def __make_categories(self, *, items: Iterable[str] = None):
self.stdout.write("Seeding categories...")
if items:
for item in items:
CategoryFactory(name=item)
else:
CategoryFactory.create_batch(size=20)
def handle(self, *args, **options):
self.stdout.write("Seeding the database...")
if not options["dev"]:
packaging_types = (
"Carton/gift box",
"Header",
"Hangtag",
"Paper band",
"Paper board",
"Blister",
"Image insert",
"Simple band",
"Label/sticker",
"Simple CDU/Displaybox",
)
raise NotImplementedError()
password = "!secret123"
self.__make_users_and_organizations(password=password)
self.__make_product_stages()
self.__make_packaging_types()
self.__make_brands()
self.__make_categories()
self.stdout.write("Seeding completed.")
self.stdout.write("Credentials:")
self.stdout.write(f" admin: {password}")
self.stdout.write(f" agency: {password}")
self.stdout.write(f" client: {password}")
self.stdout.write(f" supplier: {password}")
+42
View File
@@ -0,0 +1,42 @@
# Generated by Django 2.2.16 on 2021-01-21 14:43
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="EmailLog",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("sender", models.CharField(max_length=254)),
("receiver", models.CharField(max_length=254)),
("time", models.DateTimeField()),
(
"status",
models.CharField(
choices=[
("sending", "sending"),
("sent", "sent"),
("queued", "queued"),
("invalid", "invalid"),
("rejected", "rejected"),
("failed", "failed"),
("unknown", "unknown"),
],
max_length=20,
),
),
],
),
]
View File
Whitespace-only changes.
+117
View File
@@ -0,0 +1,117 @@
from datetime import datetime
from django.conf import settings
from django.db import models
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from model_utils import Choices
from polymorphic.base import PolymorphicModelBase
from polymorphic.managers import PolymorphicManager
from polymorphic.models import PolymorphicModel
from polymorphic.query import PolymorphicQuerySet
from rules.contrib.models import RulesModelBaseMixin, RulesModelMixin
from outbo.apps.core.fields import (
RemovalDateTimeField,
CreationDateTimeField,
ModificationDateTimeField,
)
from outbo.apps.core.utils import get_now
EmailLogStatus = Choices("sending", "sent", "queued", "invalid", "rejected", "failed", "unknown")
class EmailLog(models.Model):
sender = models.CharField(max_length=254, blank=False, null=False)
receiver = models.CharField(max_length=254, blank=False, null=False)
time = models.DateTimeField(blank=False, null=False)
status = models.CharField(max_length=20, blank=False, null=False, choices=EmailLogStatus)
if "anymail" in settings.INSTALLED_APPS:
from anymail.signals import pre_send, post_send
from anymail.message import AnymailStatus
from django.core.mail import EmailMessage
def log_email_message(sender: str, receiver: str, time: datetime, status: str):
try:
EmailLog.objects.create(sender=sender, receiver=receiver, time=time, status=status)
except:
pass
@receiver(pre_send)
def handle_pre_send(sender, message: EmailMessage, **kwargs):
log_email_message(
sender=str(message.from_email),
receiver=", ".join(message.to),
time=get_now(),
status=EmailLogStatus.sending,
)
@receiver(post_send)
def handle_post_send(sender, message: EmailMessage, status: AnymailStatus, esp_name, **kwargs):
for email, recipient_status in status.recipients.items():
log_email_message(
sender=str(message.from_email),
receiver=email,
time=get_now(),
status=recipient_status.status or "failed",
)
class SoftDeletableQuerySet(models.QuerySet):
def delete(self):
return super().update(deleted_at=get_now())
def hard_delete(self):
return super().delete()
class SoftDeletableManager(models.Manager):
queryset_class = SoftDeletableQuerySet
def __init__(self, *args, **kwargs):
self.alive_only = kwargs.pop("alive_only", True)
super().__init__(*args, **kwargs)
def get_queryset(self):
if self.alive_only:
return self.queryset_class(self.model).filter(deleted_at=None)
return self.queryset_class(self.model)
class TimestampableDeletableMixin(models.Model):
objects = SoftDeletableManager()
all_objects = SoftDeletableManager(alive_only=False)
created_at = CreationDateTimeField(_("Created at"))
updated_at = ModificationDateTimeField(_("Updated at"))
deleted_at = RemovalDateTimeField(_("Deleted at"))
class Meta:
abstract = True
def delete(self, using=None, **kwargs):
self.deleted_at = get_now()
# TODO: handle autodeletion of nested stuff
self.save(using=using)
def __str__(self):
return super().__str__() if self.deleted_at is None else f"{super().__str__()} (deleted)"
class RulesPolymorphicModelBase(RulesModelBaseMixin, PolymorphicModelBase):
pass
class RulesPolymorphicModel(RulesModelMixin, PolymorphicModel, metaclass=RulesPolymorphicModelBase):
class Meta:
abstract = True
class SoftDeletablePolymorphicQuerySet(SoftDeletableQuerySet, PolymorphicQuerySet):
pass
class SoftDeletablePolymorphicManager(SoftDeletableManager, PolymorphicManager):
queryset_class = SoftDeletablePolymorphicQuerySet
+26
View File
@@ -0,0 +1,26 @@
from djangorestframework_camel_case.parser import CamelCaseMultiPartParser
from formencode.variabledecode import variable_decode
from rest_framework import parsers
from rest_framework.parsers import MultiPartParser
class NestedMultiPartParser(MultiPartParser):
def parse(self, stream, media_type=None, parser_context=None):
result = super().parse(
stream=stream, media_type=media_type, parser_context=parser_context
)
data = variable_decode(result.data)
files = variable_decode(result.files)
return parsers.DataAndFiles(data, files)
class CamelCaseNestedMultiPartParser(CamelCaseMultiPartParser):
def parse(self, stream, media_type=None, parser_context=None):
result = super().parse(
stream=stream, media_type=media_type, parser_context=parser_context
)
data = variable_decode(result.data)
files = variable_decode(result.files)
return parsers.DataAndFiles(data, files)
+16
View File
@@ -0,0 +1,16 @@
from rules import predicate
@predicate
def is_agency(user):
return user.is_authenticated and user.is_agency
@predicate
def is_client(user):
return user.is_authenticated and user.is_client
@predicate
def is_supplier(user):
return user.is_authenticated and user.is_supplier
+39
View File
@@ -0,0 +1,39 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_polymorphic.serializers import (
PolymorphicSerializer as BasePolymorphicSerializer,
)
class PolymorphicSerializer(BasePolymorphicSerializer):
resource_type_field_name = "type"
def to_resource_type(self, model_or_instance):
return model_or_instance._meta.verbose_name
class EnumCharField(serializers.ChoiceField):
default_error_messages = {"invalid_choice": _('"{input}" is not a valid choice.')}
def __init__(self, enum, **kwargs):
self.enum = enum
kwargs.setdefault("choices", [(item, item.value) for item in enum])
super().__init__(**kwargs)
self.enum = enum
def to_internal_value(self, value):
value = super().to_representation(value)
if not isinstance(value, self.enum):
try:
enum_name, enum_value = value.split(".", 1)
if enum_name == self.enum.__name__:
value = self.enum[enum_value]
except ValueError:
raise ValidationError(self.error_messages["invalid"], code="invalid")
return value
def to_representation(self, value):
if isinstance(value, self.enum):
return str(value)
return value
+13
View File
@@ -0,0 +1,13 @@
from storages.backends.s3boto3 import S3Boto3Storage
from storages.utils import setting
class S3StaticStorage(S3Boto3Storage):
bucket_name = setting("AWS_STORAGE_BUCKET_STATIC_NAME")
default_acl = "public-read"
querystring_auth = False
class S3MediaStorage(S3Boto3Storage):
bucket_name = setting("AWS_STORAGE_BUCKET_MEDIA_NAME")
default_acl = None
+9
View File
@@ -0,0 +1,9 @@
from django.urls import path
from outbo.apps.core.views import RootApiView
from outbo.apps.products.views import ExportBriefToPDFApiView
urlpatterns = [
path("", RootApiView.as_view(), name="root"),
path("briefs/<int:pk>/export-to-pdf/", ExportBriefToPDFApiView.as_view(), name="export_brief_to_pdf"),
]
+13
View File
@@ -0,0 +1,13 @@
from django.utils import timezone
def get_now():
return timezone.now()
def get_current_date():
return timezone.localtime(get_now()).date()
def is_empty_set(tested_set):
return tested_set == set()
+10
View File
@@ -0,0 +1,10 @@
from django.conf import settings
from rest_framework import permissions, views, response
class RootApiView(views.APIView):
permission_classes = [permissions.AllowAny]
def get(self, request):
print(request)
return response.Response({"version": settings.APP_VERSION})
+160
View File
@@ -0,0 +1,160 @@
import six
from django.db import transaction
from django.db.models import Model
from rest_framework import status, mixins
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.viewsets import GenericViewSet
from rest_framework_extensions.mixins import (
NestedViewSetMixin as BaseNestedViewSetMixin,
)
from rest_framework_extensions.settings import extensions_api_settings
from rules.contrib.rest_framework import (
AutoPermissionViewSetMixin as BaseAutoPermissionViewSetMixin,
)
class AutoPermissionViewSetMixin(BaseAutoPermissionViewSetMixin):
permission_type_map = {
**BaseAutoPermissionViewSetMixin.permission_type_map,
"metadata": None,
}
class CreateModelMixin:
def create(self, request, *args, **kwargs):
if isinstance(request.data, list):
with transaction.atomic():
response = []
for item in request.data:
write_serializer = self.get_serializer(data=item)
write_serializer.is_valid(raise_exception=True)
instance = self.perform_create(serializer=write_serializer)
read_serializer = self.get_read_serializer(instance=instance)
response.append(read_serializer.data)
return Response(response, status=status.HTTP_201_CREATED)
else:
write_serializer = self.get_serializer(data=request.data)
write_serializer.is_valid(raise_exception=True)
instance = self.perform_create(serializer=write_serializer)
read_serializer = self.get_read_serializer(instance=instance)
headers = self.get_success_headers(read_serializer.data)
return Response(
read_serializer.data, status=status.HTTP_201_CREATED, headers=headers
)
def perform_create(self, *, serializer):
return serializer.save()
def get_success_headers(self, data):
try:
return {"Location": str(data[api_settings.URL_FIELD_NAME])}
except (TypeError, KeyError):
return {}
class UpdateModelMixin:
def update(self, request, *args, **kwargs):
instance = self.get_object()
write_serializer = self.get_serializer(
instance, data=request.data, partial=False
)
write_serializer.is_valid(raise_exception=True)
instance = write_serializer.save()
if getattr(instance, "_prefetched_objects_cache", None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
read_serializer = self.get_read_serializer(instance=instance)
return Response(read_serializer.data)
class PartialUpdateModelMixin:
def partial_update(self, request, *args, **kwargs):
instance = self.get_object()
write_serializer = self.get_serializer(
instance, data=request.data, partial=True
)
write_serializer.is_valid(raise_exception=True)
instance = write_serializer.save()
if getattr(instance, "_prefetched_objects_cache", None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
read_serializer = self.get_read_serializer(instance=instance)
return Response(read_serializer.data)
class RetrieveModelMixin(mixins.RetrieveModelMixin):
pass
class ListModelMixin(mixins.ListModelMixin):
pass
class DestroyModelMixin(mixins.DestroyModelMixin):
pass
class MultipleSerializerViewSet(GenericViewSet):
serializer_class_set = None
serializer_map = {
"list": "List",
"retrieve": "Retrieve",
"create": "Create",
"partial_update": "Update",
}
def get_serializer_class(self):
assert (
self.serializer_class_set is not None
), "serializer_class_set should be defined"
if self.action not in self.serializer_map:
raise KeyError(
f"""serializer_map does not contain serializer for "{self.action}" method"""
)
return getattr(self.serializer_class_set, self.serializer_map[self.action])
def get_read_serializer(self, *args, **kwargs):
assert (
self.serializer_class_set is not None
), "serializer_class_set should be defined"
if self.action != "create":
if getattr(self.serializer_class_set, "Retrieve", None) is None:
raise KeyError(
f"""serializer_class_set does not contain "Retrieve" serializer"""
)
serializer_class = self.serializer_class_set.Retrieve
else:
if (
getattr(self.serializer_class_set, "Retrieve", None) is None
and getattr(self.serializer_class_set, "AfterCreate", None) is None
):
raise KeyError(
f"""serializer_class_set does not contain "Retrieve" nor "AfterCreate" serializer and is used in "create" action."""
)
if getattr(self.serializer_class_set, "AfterCreate", None) is not None:
serializer_class = self.serializer_class_set.AfterCreate
else:
serializer_class = self.serializer_class_set.Retrieve
kwargs["context"] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
class NestedViewSetMixin(BaseNestedViewSetMixin):
pass
+11
View File
@@ -0,0 +1,11 @@
from django.utils.translation import gettext_lazy as _
from django.apps import AppConfig
class FinalPhotobriefingAppConfig(AppConfig):
name = "outbo.apps.final_photobriefing"
label = "final_photobriefing"
verbose_name = _("Final photo briefing")
default_app_config = "outbo.apps.final_photobriefing.FinalPhotobriefingAppConfig"
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+9
View File
@@ -0,0 +1,9 @@
from enum import Enum
class PhotoType(Enum):
INVALID = "<invalid>"
ARRANGEMENT_PHOTOS = "arrangement"
CLIPPING = "clipping"
PACKSHOT = "packshot"
EXTRA_PHOTO = "extraPhoto"
@@ -0,0 +1,49 @@
# Generated by Django 2.2.8 on 2019-12-18 13:54
import rules.contrib.models
from django.db import migrations, models
import outbo.apps.core.fields
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="FinalBrief",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"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"
),
),
],
options={"abstract": False,},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
]
@@ -0,0 +1,14 @@
# Generated by Django 2.2.8 on 2019-12-18 14:03
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("final_photobriefing", "0001_initial"),
]
operations = [
migrations.RemoveField(model_name="finalbrief", name="id",),
]
@@ -0,0 +1,27 @@
# Generated by Django 2.2.8 on 2019-12-18 14:03
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("photobriefing", "0001_initial"),
("final_photobriefing", "0002_remove_finalbrief_id"),
]
operations = [
migrations.AddField(
model_name="finalbrief",
name="brief",
field=models.ForeignKey(
default=0,
on_delete=django.db.models.deletion.PROTECT,
primary_key=True,
serialize=False,
to="photobriefing.Brief",
),
preserve_default=False,
),
]
@@ -0,0 +1,24 @@
# Generated by Django 2.2.8 on 2019-12-18 14:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("final_photobriefing", "0003_finalbrief_brief"),
]
operations = [
migrations.AlterField(
model_name="finalbrief",
name="brief",
field=models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
primary_key=True,
serialize=False,
to="photobriefing.Brief",
),
),
]
@@ -0,0 +1,26 @@
# Generated by Django 2.2.8 on 2019-12-18 14:06
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("final_photobriefing", "0004_auto_20191218_1505"),
]
operations = [
migrations.AlterField(
model_name="finalbrief",
name="brief",
field=models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
primary_key=True,
related_name="final",
related_query_name="final",
serialize=False,
to="photobriefing.Brief",
),
),
]
@@ -0,0 +1,102 @@
# Generated by Django 2.2.8 on 2019-12-19 08:48
from django.db import migrations, models
import django.db.models.deletion
import outbo.apps.core.fields
import outbo.apps.final_photobriefing.enums
import outbo.apps.final_photobriefing.models
import rules.contrib.models
class Migration(migrations.Migration):
dependencies = [
("final_photobriefing", "0005_auto_20191218_1506"),
]
operations = [
migrations.CreateModel(
name="FinalBriefPhoto",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"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"
),
),
(
"file",
models.FileField(
upload_to=outbo.apps.final_photobriefing.models.FormatFinalPhotoBriefPhotoFilename()
),
),
(
"category",
models.CharField(
choices=[
(
outbo.apps.final_photobriefing.enums.PhotoType(
"<invalid>"
),
"<invalid>",
),
(
outbo.apps.final_photobriefing.enums.PhotoType(
"arrangement"
),
"arrangement",
),
(
outbo.apps.final_photobriefing.enums.PhotoType(
"clipping"
),
"clipping",
),
(
outbo.apps.final_photobriefing.enums.PhotoType(
"packshot"
),
"packshot",
),
(
outbo.apps.final_photobriefing.enums.PhotoType(
"extraPhoto"
),
"extraPhoto",
),
],
max_length=30,
),
),
(
"brief",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="final_photobriefing.FinalBrief",
),
),
],
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
]
@@ -0,0 +1,56 @@
# Generated by Django 2.2.8 on 2019-12-20 10:14
from django.db import migrations, models
import django.db.models.deletion
import outbo.apps.final_photobriefing.enums
class Migration(migrations.Migration):
dependencies = [
("final_photobriefing", "0006_finalbriefphoto"),
]
operations = [
migrations.RemoveField(model_name="finalbriefphoto", name="category",),
migrations.AddField(
model_name="finalbriefphoto",
name="type",
field=models.CharField(
choices=[
(
outbo.apps.final_photobriefing.enums.PhotoType("<invalid>"),
"<invalid>",
),
(
outbo.apps.final_photobriefing.enums.PhotoType("arrangement"),
"arrangement",
),
(
outbo.apps.final_photobriefing.enums.PhotoType("clipping"),
"clipping",
),
(
outbo.apps.final_photobriefing.enums.PhotoType("packshot"),
"packshot",
),
(
outbo.apps.final_photobriefing.enums.PhotoType("extraPhoto"),
"extraPhoto",
),
],
default=outbo.apps.final_photobriefing.enums.PhotoType("<invalid>"),
max_length=30,
),
),
migrations.AlterField(
model_name="finalbriefphoto",
name="brief",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="photos",
related_query_name="photo",
to="final_photobriefing.FinalBrief",
),
),
]
@@ -0,0 +1,26 @@
# 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 = [
("final_photobriefing", "0007_auto_20191220_1114"),
]
operations = [
migrations.AlterField(
model_name="finalbrief",
name="brief",
field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name="final",
related_query_name="final",
serialize=False,
to="photobriefing.Brief",
),
),
]
Whitespace-only changes.
+63
View File
@@ -0,0 +1,63 @@
import os
from django.db import models
from django.utils.deconstruct import deconstructible
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
from outbo.apps.final_photobriefing import rules
from outbo.apps.final_photobriefing.enums import PhotoType
@deconstructible
class FormatFinalPhotoBriefPhotoFilename:
def __call__(self, instance, filename):
obj = instance.brief.brief
return "/".join(
("projects", obj.product.project.slug[1:], "final-photobriefing", filename)
)
class FinalBrief(TimestampableDeletableMixin, RulesModel):
class Meta:
rules_permissions = {
"add": rules.can_create_final_brief,
"view": rules.can_read_final_brief,
"change": rules.can_update_final_brief,
"delete": rules.can_delete_final_brief,
}
brief = models.OneToOneField(
to="photobriefing.Brief",
on_delete=models.CASCADE,
related_name="final",
related_query_name="final",
primary_key=True,
)
class FinalBriefPhoto(TimestampableDeletableMixin, RulesModel):
class Meta:
rules_permissions = {
"add": rules.can_create_final_brief_photo,
"view": rules.can_read_final_brief_photo,
"change": rules.can_update_final_brief_photo,
"delete": rules.can_delete_final_brief_photo,
}
brief = models.ForeignKey(
to=FinalBrief,
on_delete=models.CASCADE,
related_query_name="photo",
related_name="photos",
)
file = models.FileField(upload_to=FormatFinalPhotoBriefPhotoFilename())
type = models.CharField(
max_length=30,
choices=[(type, type.value) for type in PhotoType],
default=PhotoType.INVALID,
)
@property
def filename(self):
return os.path.basename(self.file.name)
+13
View File
@@ -0,0 +1,13 @@
from rules import always_deny, always_allow
from outbo.apps.core.rules import is_agency
can_create_final_brief = always_deny
can_read_final_brief = always_allow
can_update_final_brief = is_agency
can_delete_final_brief = always_deny
can_create_final_brief_photo = always_deny
can_read_final_brief_photo = always_allow
can_update_final_brief_photo = is_agency
can_delete_final_brief_photo = is_agency
+13
View File
@@ -0,0 +1,13 @@
from rest_framework_extensions.routers import ExtendedSimpleRouter
from outbo.apps.final_photobriefing.viewsets import (
FinalBriefViewSet,
FinalBriefPhotoViewSet,
)
app_name = "final_photobriefing"
router = ExtendedSimpleRouter()
router.register(prefix="final-photobriefing", viewset=FinalBriefViewSet)
router.register(prefix="final-photobriefing-photo", viewset=FinalBriefPhotoViewSet)
urlpatterns = router.urls
+99
View File
@@ -0,0 +1,99 @@
from django.http import HttpResponseRedirect
from rest_framework import serializers
from rest_framework.permissions import AllowAny
from outbo.apps.core.viewsets import (
AutoPermissionViewSetMixin,
RetrieveModelMixin,
MultipleSerializerViewSet,
PartialUpdateModelMixin,
DestroyModelMixin,
)
from outbo.apps.final_photobriefing.enums import PhotoType
from outbo.apps.final_photobriefing.models import FinalBrief, FinalBriefPhoto
class FinalBriefPhotoSerializer:
class List(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="final_photobriefing:finalbriefphoto-detail"
)
type = serializers.SerializerMethodField()
filename = serializers.ReadOnlyField()
class Meta:
model = FinalBriefPhoto
fields = ["url", "type", "filename"]
def get_type(self, instance):
t, val = instance.type.split(".", 1)
return PhotoType[val].value
class Create(serializers.ModelSerializer):
type = serializers.CharField()
class Meta:
model = FinalBriefPhoto
fields = ["type", "file"]
def validate_type(self, value):
try:
return PhotoType(value)
except ValueError:
raise serializers.ValidationError("Invalid photo type value")
class BaseFinalBriefSerializer(serializers.ModelSerializer):
class Meta:
model = FinalBrief
class FinalBriefSerializer:
class Retrieve(BaseFinalBriefSerializer):
photos = FinalBriefPhotoSerializer.List(many=True)
class Meta(BaseFinalBriefSerializer.Meta):
fields = ["photos", "created_at", "updated_at"]
class Update(BaseFinalBriefSerializer):
photos = FinalBriefPhotoSerializer.Create(many=True)
class Meta(BaseFinalBriefSerializer.Meta):
fields = ["photos"]
def update(self, instance, validated_data):
photos = validated_data.pop("photos", None)
if photos is not None:
for photo in photos:
FinalBriefPhoto.objects.create(brief=instance, **photo)
return instance
class FinalBriefViewSet(
AutoPermissionViewSetMixin,
RetrieveModelMixin,
PartialUpdateModelMixin,
MultipleSerializerViewSet,
):
queryset = FinalBrief.objects.all()
serializer_class_set = FinalBriefSerializer
class FinalBriefPhotoViewSet(
AutoPermissionViewSetMixin,
RetrieveModelMixin,
DestroyModelMixin,
MultipleSerializerViewSet,
):
queryset = FinalBriefPhoto.objects.all()
serializer_class_set = FinalBriefPhotoSerializer
retrieve_permission_classes = [AllowAny]
def get_permissions(self):
if self.action == "retrieve":
return [permission() for permission in self.retrieve_permission_classes]
return super().get_permissions()
def retrieve(self, request, *args, **kwargs):
photo = self.get_object()
return HttpResponseRedirect(redirect_to=photo.file.url)
+11
View File
@@ -0,0 +1,11 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class OrganizationsAppConfig(AppConfig):
name = "outbo.apps.organizations"
label = "organizations"
verbose_name = _("Organizations")
default_app_config = "outbo.apps.organizations.OrganizationsAppConfig"
+57
View File
@@ -0,0 +1,57 @@
from django.contrib import admin
from polymorphic.admin import (
PolymorphicChildModelAdmin,
PolymorphicChildModelFilter,
)
from outbo.apps.core.admin import PolymorphicParentModelAdmin
from outbo.apps.organizations.models import (
Organization,
AgencyOrganization,
ClientOrganization,
SupplierOrganization,
Brand,
Category,
)
class _OrganizationChildAdmin(PolymorphicChildModelAdmin):
base_model = Organization
@admin.register(AgencyOrganization)
class AgencyOrganizationAdmin(_OrganizationChildAdmin):
base_model = AgencyOrganization
@admin.register(ClientOrganization)
class ClientOrganizationAdmin(_OrganizationChildAdmin):
base_model = ClientOrganization
search_fields = ["name"]
@admin.register(SupplierOrganization)
class SupplierOrganizationAdmin(_OrganizationChildAdmin):
base_model = SupplierOrganization
search_fields = ["name"]
@admin.register(Organization)
class OrganizationAdmin(PolymorphicParentModelAdmin):
base_model = Organization
child_models = (AgencyOrganization, ClientOrganization, SupplierOrganization)
list_filter = (PolymorphicChildModelFilter,)
search_fields = ["name"]
@admin.register(Brand)
class BrandAdmin(admin.ModelAdmin):
autocomplete_fields = ["client", "suppliers", "categories"]
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
search_fields = ["name"]
# NOTE: https://stackoverflow.com/questions/9879687/adding-a-manytomanywidget-to-the-reverse-of-a-manytomanyfield-in-the-django-admi/13189954
@@ -0,0 +1,220 @@
# Generated by Django 2.2.7 on 2019-11-29 08:57
from django.db import migrations, models
import django.db.models.deletion
import outbo.apps.core.fields
import rules.contrib.models
class Migration(migrations.Migration):
initial = True
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
]
operations = [
migrations.CreateModel(
name="Category",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"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"
),
),
("name", models.CharField(max_length=254, verbose_name="name")),
],
options={
"verbose_name": "category",
"verbose_name_plural": "categories",
"ordering": ["name"],
},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.CreateModel(
name="Organization",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"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"
),
),
("name", models.CharField(max_length=100)),
(
"polymorphic_ctype",
models.ForeignKey(
editable=False,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="polymorphic_organizations.organization_set+",
to="contenttypes.ContentType",
),
),
],
options={"abstract": False,},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.CreateModel(
name="AgencyOrganization",
fields=[
(
"organization_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="organizations.Organization",
),
),
],
options={"verbose_name": "agency", "verbose_name_plural": "agencies",},
bases=("organizations.organization",),
),
migrations.CreateModel(
name="ClientOrganization",
fields=[
(
"organization_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="organizations.Organization",
),
),
("uses_simple_project_flow", models.BooleanField(default=False)),
],
options={"verbose_name": "client", "verbose_name_plural": "clients",},
bases=("organizations.organization",),
),
migrations.CreateModel(
name="SupplierOrganization",
fields=[
(
"organization_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="organizations.Organization",
),
),
],
options={"verbose_name": "supplier", "verbose_name_plural": "suppliers",},
bases=("organizations.organization",),
),
migrations.CreateModel(
name="Brand",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"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"
),
),
("name", models.CharField(max_length=50, verbose_name="name")),
(
"categories",
models.ManyToManyField(
related_name="brands",
related_query_name="brand",
to="organizations.Category",
verbose_name="categories",
),
),
(
"client",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="brands",
related_query_name="brand",
to="organizations.ClientOrganization",
verbose_name="client",
),
),
(
"suppliers",
models.ManyToManyField(
related_name="brands",
related_query_name="brand",
to="organizations.SupplierOrganization",
verbose_name="suppliers",
),
),
],
options={"verbose_name": "brand", "verbose_name_plural": "brands",},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
]
@@ -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 = [
("organizations", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="brand",
name="client",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="brands",
related_query_name="brand",
to="organizations.ClientOrganization",
verbose_name="client",
),
),
]
Whitespace-only changes.
@@ -0,0 +1,8 @@
from .organization import (
Organization,
AgencyOrganization,
ClientOrganization,
SupplierOrganization,
)
from .brand import Brand
from .category import Category
+38
View File
@@ -0,0 +1,38 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
from outbo.apps.organizations import rules
class Brand(TimestampableDeletableMixin, RulesModel):
class Meta:
verbose_name = _("brand")
verbose_name_plural = _("brands")
rules_permissions = {"read": rules.can_read_brand}
name = models.CharField(verbose_name=_("name"), max_length=50)
client = models.ForeignKey(
verbose_name=_("client"),
to="ClientOrganization",
on_delete=models.CASCADE,
related_name="brands",
related_query_name="brand",
null=False,
)
suppliers = models.ManyToManyField(
verbose_name=_("suppliers"),
to="SupplierOrganization",
related_name="brands",
related_query_name="brand",
)
categories = models.ManyToManyField(
verbose_name=_("categories"),
to="Category",
related_name="brands",
related_query_name="brand",
)
def __str__(self):
return self.name
+17
View File
@@ -0,0 +1,17 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
class Category(TimestampableDeletableMixin, RulesModel):
class Meta:
verbose_name = _("category")
verbose_name_plural = _("categories")
ordering = ["name"]
name = models.CharField(verbose_name=_("name"), max_length=254)
def __str__(self):
return self.name
@@ -0,0 +1,50 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
from outbo.apps.core.models import (
RulesPolymorphicModel,
TimestampableDeletableMixin,
SoftDeletablePolymorphicManager,
)
class Organization(TimestampableDeletableMixin, RulesPolymorphicModel):
objects = SoftDeletablePolymorphicManager()
all_objects = SoftDeletablePolymorphicManager(alive_only=False)
name = models.CharField(max_length=100)
def __str__(self):
return f"[{str(self.polymorphic_ctype).title()}] {self.name}"
@property
def type(self):
return self.polymorphic_ctype
@type.setter
def type(self, value):
self.polymorphic_ctype = value
@type.deleter
def type(self):
del self.polymorphic_ctype
class AgencyOrganization(Organization):
class Meta:
verbose_name = _("agency")
verbose_name_plural = _("agencies")
class ClientOrganization(Organization):
class Meta:
verbose_name = _("client")
verbose_name_plural = _("clients")
uses_simple_project_flow = models.BooleanField(null=False, default=False)
class SupplierOrganization(Organization):
class Meta:
verbose_name = _("supplier")
verbose_name_plural = _("suppliers")
+11
View File
@@ -0,0 +1,11 @@
from rules import always_deny, predicate
@predicate
def can_read_brand(user, brand):
return always_deny
@predicate
def can_read_category(user, category):
return always_deny
+94
View File
@@ -0,0 +1,94 @@
from rest_framework import serializers
from outbo.apps.core.serializers import PolymorphicSerializer
from outbo.apps.organizations.models import (
AgencyOrganization,
ClientOrganization,
SupplierOrganization,
Brand,
Category,
Organization,
)
# Organizations
class _BaseOrganizationSerializer(serializers.ModelSerializer):
class Meta:
fields = (
"id",
"name",
)
class _OrganizationSerializer:
class List(_BaseOrganizationSerializer):
class Meta(_BaseOrganizationSerializer.Meta):
model = Organization
class _AgencyOrganizationSerializer:
class List(_BaseOrganizationSerializer):
class Meta(_BaseOrganizationSerializer.Meta):
model = AgencyOrganization
class _ClientOrganizationSerializer:
class List(_BaseOrganizationSerializer):
class Meta(_BaseOrganizationSerializer.Meta):
model = ClientOrganization
class _SupplierOrganizationSerializer:
class List(_BaseOrganizationSerializer):
class Meta(_BaseOrganizationSerializer.Meta):
model = SupplierOrganization
class _BaseOrganizationPolymorphicSerializer(PolymorphicSerializer):
model_serializer_mapping = {
AgencyOrganization: _AgencyOrganizationSerializer.List,
ClientOrganization: _ClientOrganizationSerializer.List,
SupplierOrganization: _SupplierOrganizationSerializer.List,
Organization: _OrganizationSerializer.List,
}
class OrganizationSerializer:
class Retrieve(_BaseOrganizationPolymorphicSerializer):
pass
class List(_BaseOrganizationPolymorphicSerializer):
pass
# Brands
class _BaseBrandSerializer(serializers.ModelSerializer):
class Meta:
model = Brand
fields = ["name"]
class BrandSerializer(serializers.ModelSerializer):
# Create & Update not needed
class Retrieve(_BaseBrandSerializer):
pass
class List(_BaseBrandSerializer):
class Meta(_BaseBrandSerializer.Meta):
fields = ["id"] + _BaseBrandSerializer.Meta.fields
# Categories
class _BaseCategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ["name"]
class CategorySerializer:
class Retrieve(_BaseCategorySerializer):
pass
class List(_BaseCategorySerializer):
class Meta(_BaseCategorySerializer.Meta):
fields = ["id"] + _BaseCategorySerializer.Meta.fields
+10
View File
@@ -0,0 +1,10 @@
from rest_framework import routers
from outbo.apps.organizations.viewsets import BrandViewSet, CategoryViewSet
app_name = "organizations"
router = routers.SimpleRouter()
router.register(prefix="brands", viewset=BrandViewSet)
router.register(prefix="categories", viewset=CategoryViewSet)
urlpatterns = router.urls
+29
View File
@@ -0,0 +1,29 @@
from outbo.apps.core.viewsets import (
MultipleSerializerViewSet,
AutoPermissionViewSetMixin,
ListModelMixin,
RetrieveModelMixin,
)
from outbo.apps.organizations.models import Brand, Category
from outbo.apps.organizations.serializers import BrandSerializer, CategorySerializer
class BrandViewSet(
RetrieveModelMixin,
ListModelMixin,
AutoPermissionViewSetMixin,
MultipleSerializerViewSet,
):
queryset = Brand.objects.all()
serializer_class_set = BrandSerializer
class CategoryViewSet(
RetrieveModelMixin,
ListModelMixin,
AutoPermissionViewSetMixin,
MultipleSerializerViewSet,
):
queryset = Category.objects.all()
serializer_class_set = CategorySerializer
+11
View File
@@ -0,0 +1,11 @@
from django.utils.translation import gettext_lazy as _
from django.apps import AppConfig
class PhotobriefingAppConfig(AppConfig):
name = "outbo.apps.photobriefing"
label = "photobriefing"
verbose_name = _("Photo briefing")
default_app_config = "outbo.apps.photobriefing.PhotobriefingAppConfig"
+21
View File
@@ -0,0 +1,21 @@
from django.contrib import admin
from nested_admin import nested
from outbo.apps.photobriefing.models import Brief, BriefItem, BriefItemPhoto, Category
class BriefItemPhotoInline(nested.NestedStackedInline):
model = BriefItemPhoto
class BriefItemInline(nested.NestedStackedInline):
model = BriefItem
inlines = [BriefItemPhotoInline]
@admin.register(Brief)
class BriefAdmin(nested.NestedModelAdmin):
inlines = [BriefItemInline]
admin.site.register(Category)
+7
View File
@@ -0,0 +1,7 @@
from enum import Enum
class ChoiceRole(Enum):
INVALID = "<invalid>"
SUPPLIER = "supplier"
CLIENT = "client"
@@ -0,0 +1,304 @@
# Generated by Django 2.2.7 on 2019-11-29 08:57
from django.db import migrations, models
import django.db.models.deletion
import django_fsm
import outbo.apps.core.fields
import outbo.apps.core.utils
import outbo.apps.photobriefing.enums
import outbo.apps.photobriefing.models.brief_item_photo
import rules.contrib.models
class Migration(migrations.Migration):
initial = True
dependencies = [
("products", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="Brief",
fields=[
(
"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"
),
),
(
"product",
models.OneToOneField(
editable=False,
on_delete=django.db.models.deletion.PROTECT,
primary_key=True,
related_name="photo_brief",
related_query_name="photo_brief",
serialize=False,
to="products.Product",
verbose_name="product",
),
),
(
"session_date",
models.DateField(
default=outbo.apps.core.utils.get_current_date,
verbose_name="photo session date",
),
),
("concept_description", models.TextField(blank=True, default="")),
(
"stage",
django_fsm.FSMField(
default="created", max_length=50, protected=True
),
),
],
options={"abstract": False,},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.CreateModel(
name="BriefItem",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"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"
),
),
("title", models.CharField(max_length=100)),
("description", models.TextField()),
(
"brief",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="items",
related_query_name="item",
to="photobriefing.Brief",
verbose_name="brief",
),
),
],
options={"ordering": ["id"],},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.CreateModel(
name="Category",
fields=[
(
"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"
),
),
(
"label",
models.CharField(
max_length=30,
primary_key=True,
serialize=False,
verbose_name="label",
),
),
(
"description",
models.CharField(max_length=30, verbose_name="description"),
),
],
options={"verbose_name": "category", "verbose_name_plural": "categories",},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.CreateModel(
name="BriefItemPhoto",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"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"
),
),
(
"file",
models.ImageField(
upload_to=outbo.apps.photobriefing.models.brief_item_photo.filename,
verbose_name="file",
),
),
(
"item",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="photos",
related_query_name="photo",
to="photobriefing.BriefItem",
verbose_name="brief item",
),
),
],
options={"abstract": False,},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.AddField(
model_name="briefitem",
name="category",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="photobriefing.Category",
verbose_name="category",
),
),
migrations.CreateModel(
name="BriefItemChoice",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"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"
),
),
(
"role",
models.CharField(
choices=[
(
outbo.apps.photobriefing.enums.ChoiceRole("<invalid>"),
"<invalid>",
),
(
outbo.apps.photobriefing.enums.ChoiceRole("supplier"),
"supplier",
),
(
outbo.apps.photobriefing.enums.ChoiceRole("client"),
"client",
),
],
default=outbo.apps.photobriefing.enums.ChoiceRole("<invalid>"),
editable=False,
max_length=30,
),
),
(
"brief",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="choices",
related_query_name="choice",
to="photobriefing.Brief",
verbose_name="brief",
),
),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="choices+",
related_query_name="choice",
to="photobriefing.Category",
verbose_name="category",
),
),
(
"item",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="choices",
related_query_name="choice",
to="photobriefing.BriefItem",
verbose_name="item",
),
),
],
options={"unique_together": {("brief", "category", "role")},},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 2.2.8 on 2019-12-18 14:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("photobriefing", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="brief",
name="concept_description",
field=models.TextField(blank=True),
),
]
@@ -0,0 +1,92 @@
# 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 = [
("photobriefing", "0002_auto_20191218_1505"),
]
operations = [
migrations.AlterField(
model_name="brief",
name="product",
field=models.OneToOneField(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name="photo_brief",
related_query_name="photo_brief",
serialize=False,
to="products.Product",
verbose_name="product",
),
),
migrations.AlterField(
model_name="briefitem",
name="brief",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="items",
related_query_name="item",
to="photobriefing.Brief",
verbose_name="brief",
),
),
migrations.AlterField(
model_name="briefitem",
name="category",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="photobriefing.Category",
verbose_name="category",
),
),
migrations.AlterField(
model_name="briefitemchoice",
name="brief",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="choices",
related_query_name="choice",
to="photobriefing.Brief",
verbose_name="brief",
),
),
migrations.AlterField(
model_name="briefitemchoice",
name="category",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="choices+",
related_query_name="choice",
to="photobriefing.Category",
verbose_name="category",
),
),
migrations.AlterField(
model_name="briefitemchoice",
name="item",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="choices",
related_query_name="choice",
to="photobriefing.BriefItem",
verbose_name="item",
),
),
migrations.AlterField(
model_name="briefitemphoto",
name="item",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="photos",
related_query_name="photo",
to="photobriefing.BriefItem",
verbose_name="brief item",
),
),
]
Whitespace-only changes.
@@ -0,0 +1,5 @@
from .brief import Brief
from .brief_item import BriefItem
from .brief_item_photo import BriefItemPhoto
from .brief_item_choice import BriefItemChoice
from .category import Category
+107
View File
@@ -0,0 +1,107 @@
from itertools import product
from django.db import models
from django.utils.translation import gettext_lazy as _
from django_fsm import FSMField, transition
from rules.contrib.models import RulesModel
from outbo.apps.activity_log.enums import MessageType
from outbo.apps.activity_log.models import ProductLogItem
from outbo.apps.core.models import TimestampableDeletableMixin
from outbo.apps.core.utils import get_current_date, is_empty_set
from outbo.apps.final_photobriefing.models import FinalBrief
from outbo.apps.photobriefing import rules
from outbo.apps.photobriefing.enums import ChoiceRole
class Brief(TimestampableDeletableMixin, RulesModel):
class Meta:
rules_permissions = {
"add": rules.can_create_brief,
"view": rules.can_read_brief,
"change": rules.can_update_brief,
"delete": rules.can_delete_brief,
}
product = models.OneToOneField(
verbose_name=_("product"),
to="products.Product",
on_delete=models.CASCADE,
primary_key=True,
editable=False,
related_name="photo_brief",
related_query_name="photo_brief",
)
session_date = models.DateField(
verbose_name=_("photo session date"),
blank=False,
null=False,
default=get_current_date,
)
concept_description = models.TextField(blank=True, null=False)
stage = FSMField(default="created", protected=True)
def save(self, *args, **kwargs):
result = super().save(*args, **kwargs)
if self.product:
self.product.save()
return result
def acceptable_for_user(self, *, user):
if user.is_observer or user.is_trader:
return False
return not user.is_agency and self.stage == "published"
def publishable_for_user(self, *, user):
if user.is_observer or user.is_trader:
return False
all_categories = set(self.items.values_list("category", flat=True))
if self.editable_for_user(user=user) and not is_empty_set(all_categories):
return True
return False
def completable_for_user(self, *, user):
if user.is_observer or user.is_trader:
return False
if not user.is_agency or self.stage != "published":
return False
roles = [ChoiceRole.SUPPLIER, ChoiceRole.CLIENT]
all_categories = set(self.items.values_list("category", flat=True))
if is_empty_set(all_categories):
return False
for category, role in product(all_categories, roles):
if not self.choices.filter(category_id=category, role=role).exists():
return False
return True
def editable_for_user(self, *, user):
if user.is_observer or user.is_trader:
return False
if not user.is_agency or not self.stage == "created":
return False
return self.product.stage.group in [2, 3]
@transition(field="stage", source="created", target="published")
def publish_as_user(self, *, user):
ProductLogItem.objects.create(
product=self.product,
created_by=user,
message="Photobriefing published",
message_type=MessageType.SUCCESS,
)
@transition(field="stage", source="published", target="completed")
def complete_as_user(self, *, user):
FinalBrief.objects.create(brief=self)
ProductLogItem.objects.create(
product=self.product,
created_by=user,
message="Photobriefing completed",
message_type=MessageType.SUCCESS,
)
@@ -0,0 +1,32 @@
from django.db import models
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
from django.utils.translation import gettext_lazy as _
from outbo.apps.photobriefing import rules
class BriefItem(TimestampableDeletableMixin, RulesModel):
class Meta:
rules_permissions = {
"add": rules.can_create_brief_item,
"view": rules.can_read_brief_item,
"change": rules.can_update_brief_item,
"delete": rules.can_delete_brief_item,
}
ordering = ["id"]
brief = models.ForeignKey(
verbose_name=_("brief"),
to="Brief",
on_delete=models.CASCADE,
related_name="items",
related_query_name="item",
)
category = models.ForeignKey(
verbose_name=_("category"), to="Category", on_delete=models.CASCADE
)
title = models.CharField(max_length=100, blank=False, null=False)
description = models.TextField(blank=False, null=False)
@@ -0,0 +1,39 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
from outbo.apps.photobriefing.enums import ChoiceRole
class BriefItemChoice(TimestampableDeletableMixin, RulesModel):
class Meta:
unique_together = (("brief", "category", "role"),)
brief = models.ForeignKey(
verbose_name=_("brief"),
to="Brief",
on_delete=models.CASCADE,
related_name="choices",
related_query_name="choice",
)
category = models.ForeignKey(
verbose_name=_("category"),
to="Category",
on_delete=models.CASCADE,
related_name="choices+",
related_query_name="choice",
)
role = models.CharField(
max_length=30,
choices=[(role, role.value) for role in ChoiceRole],
default=ChoiceRole.INVALID,
editable=False,
)
item = models.ForeignKey(
verbose_name=_("item"),
to="BriefItem",
on_delete=models.CASCADE,
related_name="choices",
related_query_name="choice",
)
@@ -0,0 +1,40 @@
import os
from django.db import models
from django.utils.translation import gettext_lazy as _
from rules import always_allow
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
def filename(self, filename):
return "/".join(
(
"projects",
self.item.brief.product.project.slug[1:],
"photobriefing",
filename,
)
)
class BriefItemPhoto(TimestampableDeletableMixin, RulesModel):
class Meta:
rules_permissions = {
"view": always_allow,
}
item = models.ForeignKey(
verbose_name=_("brief item"),
to="BriefItem",
on_delete=models.CASCADE,
related_name="photos",
related_query_name="photo",
)
file = models.ImageField(verbose_name=_("file"), upload_to=filename)
@property
def filename(self):
return os.path.basename(self.file.name)
+17
View File
@@ -0,0 +1,17 @@
from django.db import models
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
from django.utils.translation import gettext_lazy as _, pgettext
class Category(TimestampableDeletableMixin, RulesModel):
class Meta:
verbose_name = _("category")
verbose_name_plural = _("categories")
label = models.CharField(verbose_name=_("label"), max_length=30, primary_key=True)
description = models.CharField(verbose_name=_("description"), max_length=30)
def __str__(self):
return pgettext("photo_brief_categories", self.description)
+27
View File
@@ -0,0 +1,27 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
from outbo.apps.final_photobriefing.enums import PhotoType
class Photo(TimestampableDeletableMixin, RulesModel):
brief = models.ForeignKey(
verbose_name=_("photo brief"),
to="Brief",
on_delete=models.CASCADE,
related_name="photos",
related_query_name="photo",
)
type = models.CharField(
max_length=50,
choices=((type, type.value) for type in PhotoType),
default=PhotoType.INVALID,
)
def filename(self, filename):
return f"projects/{self.brief.product.project.slug[1:]}/photos/{filename}"
file = models.ImageField(verbose_name=_("file"), upload_to=filename)
+63
View File
@@ -0,0 +1,63 @@
from typing import TYPE_CHECKING
from rules import always_deny, predicate
from outbo.apps.core.rules import is_agency
from outbo.apps.products.rules import can_read_product
if TYPE_CHECKING:
from outbo.apps.authentication.models import User
from outbo.apps.photobriefing.models import Brief, BriefItem
# Brief
@predicate
def _has_readable_product(user: "User", brief: "Brief"):
return can_read_product.test(user, brief.product)
@predicate
def can_edit_brief(user: "User", brief: "Brief"):
return brief.editable_for_user(user=user)
@predicate
def can_publish_brief(user: "User", brief: "Brief"):
return brief.publishable_for_user(user=user)
@predicate
def can_complete_brief(user: "User", brief: "Brief"):
return brief.completable_for_user(user=user)
@predicate
def can_accept_brief(user: "User", brief: "Brief"):
return brief.acceptable_for_user(user=user)
# CRUD
can_create_brief = always_deny
can_read_brief = _has_readable_product
can_update_brief = (
can_edit_brief | can_publish_brief | can_accept_brief | can_complete_brief
)
can_delete_brief = always_deny
# BriefItem
@predicate
def _has_readable_brief(user: "User", brief_item: "BriefItem"):
return can_read_brief.test(user, brief_item.brief)
@predicate
def _has_editable_brief(user: "User", brief_item: "BriefItem"):
return can_edit_brief.test(user, brief_item.brief)
# CRUD
can_create_brief_item = is_agency
can_read_brief_item = _has_readable_brief
can_update_brief_item = always_deny
can_delete_brief_item = _has_editable_brief
@@ -0,0 +1,5 @@
from .brief_item_photo import BriefItemPhotoSerializer
from .brief_item import BriefItemSerializer
from .brief_item_choice import BriefItemChoiceSerializer
from .brief import BriefSerializer
from .category import CategorySerializer
+159
View File
@@ -0,0 +1,159 @@
from typing import TYPE_CHECKING
from rest_framework import serializers
from rest_framework.exceptions import PermissionDenied
from outbo.apps.photobriefing.enums import ChoiceRole
from outbo.apps.photobriefing.models import Brief, BriefItemChoice
from outbo.apps.photobriefing.rules import can_complete_brief, can_publish_brief
from outbo.apps.photobriefing.serializers import (
BriefItemSerializer,
BriefItemChoiceSerializer,
)
if TYPE_CHECKING:
from outbo.apps.authentication.models import User
class BaseBriefSerializer(serializers.ModelSerializer):
class Meta:
model = Brief
def get_editable(self, instance: Brief):
current_user = self.context["request"].user
return instance.editable_for_user(user=current_user)
def get_acceptable(self, instance: Brief):
current_user = self.context["request"].user
return instance.acceptable_for_user(user=current_user)
def get_publishable(self, instance: Brief):
current_user = self.context["request"].user
return instance.publishable_for_user(user=current_user)
def get_completable(self, instance: Brief):
current_user = self.context["request"].user
return instance.completable_for_user(user=current_user)
class BriefSerializer:
class Retrieve(BaseBriefSerializer):
items = BriefItemSerializer.List(many=True)
choices = BriefItemChoiceSerializer.List(many=True)
editable = serializers.SerializerMethodField()
acceptable = serializers.SerializerMethodField()
publishable = serializers.SerializerMethodField()
completable = serializers.SerializerMethodField()
class Meta(BaseBriefSerializer.Meta):
fields = [
"stage",
"session_date",
"concept_description",
"items",
"choices",
"created_at",
"updated_at",
"editable",
"acceptable",
"publishable",
"completable",
]
class Update(BaseBriefSerializer):
publish = serializers.BooleanField()
complete = serializers.BooleanField()
choice = BriefItemChoiceSerializer.Update(partial=False)
items = BriefItemSerializer.Create(many=True)
class Meta(BaseBriefSerializer.Meta):
fields = [
"session_date",
"concept_description",
"items",
"choice",
"publish",
"complete",
]
def update(self, instance, validated_data):
if validated_data.get("choice", None) is not None:
serializer = BriefSerializer.ChoiceUpdate(context=self.context)
return serializer.update(instance, {"choice": validated_data["choice"]})
if validated_data.get("publish", None) is not None:
serializer = BriefSerializer.Publish(context=self.context)
return serializer.update(
instance, {"publish": validated_data["publish"]}
)
if validated_data.get("complete", None) is not None:
serializer = BriefSerializer.Complete(context=self.context)
return serializer.update(
instance, {"complete": validated_data["complete"]}
)
items = validated_data.pop("items", None)
if items:
for item in items:
item["brief"] = instance
BriefItemSerializer.Create(context=self.context).create(item)
return super().update(instance=instance, validated_data=validated_data)
class Publish(BaseBriefSerializer):
publish = serializers.BooleanField()
class Meta(BaseBriefSerializer.Meta):
fields = ["publish"]
def update(self, instance, validated_data):
publish = validated_data["publish"]
current_user = self.context["request"].user
if not can_publish_brief.test(current_user, instance):
raise PermissionDenied()
if publish:
instance.publish_as_user(user=current_user)
instance.save()
return instance
class Complete(BaseBriefSerializer):
complete = serializers.BooleanField()
class Meta(BaseBriefSerializer.Meta):
fields = ["complete"]
def update(self, instance: Brief, validated_data):
complete = validated_data["complete"]
current_user = self.context["request"].user
if not can_complete_brief.test(current_user, instance):
raise PermissionDenied()
if complete:
instance.complete_as_user(user=current_user)
instance.save()
return instance
class ChoiceUpdate(BaseBriefSerializer):
choice = BriefItemChoiceSerializer.Update()
class Meta(BaseBriefSerializer.Meta):
fields = ["choice"]
def update(self, instance, validated_data):
choice = validated_data["choice"]
current_user: "User" = self.context["request"].user
if current_user.is_supplier:
role = ChoiceRole.SUPPLIER
elif current_user.is_client:
role = ChoiceRole.CLIENT
else:
raise NotImplemented
current_choice, _ = BriefItemChoice.objects.get_or_create(
defaults={"item": choice["item"]},
brief=instance,
category=choice["category"],
role=role,
)
if current_choice.item != choice["item"]:
current_choice.item = choice["item"]
current_choice.save()
return instance
@@ -0,0 +1,38 @@
from rest_framework import serializers
from outbo.apps.photobriefing.models import BriefItem
from outbo.apps.photobriefing.serializers import BriefItemPhotoSerializer
class BaseBriefItemSerializer(serializers.ModelSerializer):
class Meta:
model = BriefItem
class BriefItemSerializer:
class List(BaseBriefItemSerializer):
photos = BriefItemPhotoSerializer.List(many=True)
class Meta(BaseBriefItemSerializer.Meta):
fields = ["id", "category", "title", "description", "photos"]
class Create(BaseBriefItemSerializer):
photos = BriefItemPhotoSerializer.Create(many=True)
class Meta(BaseBriefItemSerializer.Meta):
fields = ["category", "title", "description", "photos"]
def create(self, validated_data):
photos = validated_data.pop("photos", None)
instance = super().create(validated_data)
if photos is not None:
for photo in photos:
photo["item"] = instance
BriefItemPhotoSerializer.Create().create(photo)
return instance
class AfterCreate(BaseBriefItemSerializer):
photos = BriefItemPhotoSerializer.List(many=True)
class Meta(BaseBriefItemSerializer.Meta):
fields = ["id", "category", "title", "description", "photos"]
@@ -0,0 +1,30 @@
from rest_framework import serializers
from outbo.apps.photobriefing.enums import ChoiceRole
from outbo.apps.photobriefing.models import BriefItemChoice
class BaseBriefItemChoiceSerializer(serializers.ModelSerializer):
class Meta:
model = BriefItemChoice
class BriefItemChoiceSerializer:
class List(BaseBriefItemChoiceSerializer):
role = serializers.SerializerMethodField()
class Meta(BaseBriefItemChoiceSerializer.Meta):
fields = ["category", "item", "role"]
def get_role(self, instance):
role = eval(instance.role) # TODO: eval is evil
if role is ChoiceRole.SUPPLIER:
return "supplier"
if role is ChoiceRole.CLIENT:
return "client"
raise NotImplementedError
class Update(BaseBriefItemChoiceSerializer):
class Meta:
model = BriefItemChoice
fields = ["category", "item"]
@@ -0,0 +1,23 @@
from rest_framework import serializers
from outbo.apps.photobriefing.models import BriefItemPhoto
class BaseBriefItemPhotoSerializer(serializers.ModelSerializer):
class Meta:
model = BriefItemPhoto
class BriefItemPhotoSerializer:
class Create(BaseBriefItemPhotoSerializer):
class Meta(BaseBriefItemPhotoSerializer.Meta):
fields = ["file"]
class List(BaseBriefItemPhotoSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="photobriefing:briefitemphoto-detail", read_only=True
)
filename = serializers.ReadOnlyField()
class Meta(BaseBriefItemPhotoSerializer.Meta):
fields = ["url", "filename"]
Loaded 100 of 454 files, more files were not shown because too many files have changed in this diff. Show more