Initial commit from ZIP
This commit is contained in:
commit
378d4692a9
454 files changed
+40367
No files matched your search
Executable
Whitespace-only changes.
Executable
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
+7
@@ -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",),
|
||||
),
|
||||
]
|
||||
+39
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
+28
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
+38
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
+56
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
|
||||
app_name = "activity_log"
|
||||
urlpatterns = []
|
||||
+48
@@ -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
@@ -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
@@ -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
@@ -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),
|
||||
),
|
||||
]
|
||||
+41
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
+25
@@ -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"
|
||||
),
|
||||
),
|
||||
]
|
||||
+24
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
+25
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
+18
@@ -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'),
|
||||
),
|
||||
]
|
||||
+19
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
Whitespace-only changes.
Executable
+18
@@ -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"
|
||||
Executable
+122
@@ -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
|
||||
Executable
+29
@@ -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)
|
||||
Whitespace-only changes.
Whitespace-only changes.
+236
@@ -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}")
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
Whitespace-only changes.
Executable
+117
@@ -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
|
||||
Executable
+26
@@ -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)
|
||||
Executable
+16
@@ -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
|
||||
Executable
+39
@@ -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
|
||||
Executable
+13
@@ -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
|
||||
Executable
+9
@@ -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"),
|
||||
]
|
||||
Executable
+13
@@ -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()
|
||||
Executable
+10
@@ -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})
|
||||
Executable
+160
@@ -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
|
||||
@@ -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"
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
+14
@@ -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",),
|
||||
]
|
||||
+27
@@ -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,
|
||||
),
|
||||
]
|
||||
+24
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
+26
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
+102
@@ -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),
|
||||
),
|
||||
]
|
||||
+56
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
+26
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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),
|
||||
),
|
||||
]
|
||||
+25
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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),
|
||||
),
|
||||
]
|
||||
+18
@@ -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),
|
||||
),
|
||||
]
|
||||
+92
@@ -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
@@ -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)
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"]
|
||||
Loaded 100 of 454 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user