Initial commit from ZIP
This commit is contained in:
commit
378d4692a9
454 files changed
+40367
No files matched your search
+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"]
|
||||
@@ -0,0 +1,23 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from outbo.apps.photobriefing.models import BriefItemPhoto
|
||||
|
||||
|
||||
class BaseBriefItemPhotoSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BriefItemPhoto
|
||||
|
||||
|
||||
class BriefItemPhotoSerializer:
|
||||
class Create(BaseBriefItemPhotoSerializer):
|
||||
class Meta(BaseBriefItemPhotoSerializer.Meta):
|
||||
fields = ["file"]
|
||||
|
||||
class List(BaseBriefItemPhotoSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="photobriefing:briefitemphoto-detail", read_only=True
|
||||
)
|
||||
filename = serializers.ReadOnlyField()
|
||||
|
||||
class Meta(BaseBriefItemPhotoSerializer.Meta):
|
||||
fields = ["url", "filename"]
|
||||
@@ -0,0 +1,10 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from outbo.apps.photobriefing.models import Category
|
||||
|
||||
|
||||
class CategorySerializer:
|
||||
class List(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = ["label", "description"]
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
from rest_framework_extensions.routers import ExtendedSimpleRouter
|
||||
|
||||
from outbo.apps.photobriefing.viewsets import (
|
||||
BriefViewSet,
|
||||
BriefItemNestedViewSet,
|
||||
CategoryViewSet,
|
||||
BriefItemPhotoViewSet,
|
||||
BriefItemViewSet,
|
||||
)
|
||||
|
||||
app_name = "photobriefing"
|
||||
router = ExtendedSimpleRouter()
|
||||
router.register(prefix="photobriefing-categories", viewset=CategoryViewSet)
|
||||
router.register(prefix="photobriefing-items", viewset=BriefItemViewSet)
|
||||
router.register(prefix="photobriefing", viewset=BriefViewSet).register(
|
||||
prefix="items",
|
||||
viewset=BriefItemNestedViewSet,
|
||||
basename="brief-item",
|
||||
parents_query_lookups=["brief_id"],
|
||||
)
|
||||
|
||||
router.register(prefix="photobriefing-photos", viewset=BriefItemPhotoViewSet)
|
||||
|
||||
urlpatterns = router.urls
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
from django.http import HttpResponseRedirect
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
||||
from outbo.apps.core.viewsets import (
|
||||
AutoPermissionViewSetMixin,
|
||||
RetrieveModelMixin,
|
||||
MultipleSerializerViewSet,
|
||||
PartialUpdateModelMixin,
|
||||
NestedViewSetMixin,
|
||||
CreateModelMixin,
|
||||
ListModelMixin,
|
||||
DestroyModelMixin,
|
||||
)
|
||||
from outbo.apps.photobriefing import rules
|
||||
from outbo.apps.photobriefing.models import Brief, BriefItem, Category, BriefItemPhoto
|
||||
from outbo.apps.photobriefing.serializers import (
|
||||
BriefSerializer,
|
||||
BriefItemSerializer,
|
||||
CategorySerializer,
|
||||
BriefItemPhotoSerializer,
|
||||
)
|
||||
|
||||
|
||||
class BriefViewSet(
|
||||
AutoPermissionViewSetMixin,
|
||||
PartialUpdateModelMixin,
|
||||
RetrieveModelMixin,
|
||||
MultipleSerializerViewSet,
|
||||
):
|
||||
queryset = Brief.objects.all()
|
||||
serializer_class_set = BriefSerializer
|
||||
|
||||
|
||||
class BriefItemViewSet(
|
||||
AutoPermissionViewSetMixin, DestroyModelMixin, MultipleSerializerViewSet,
|
||||
):
|
||||
queryset = BriefItem.objects.all()
|
||||
serializer_class_set = BriefItemSerializer
|
||||
|
||||
|
||||
class BriefItemNestedViewSet(
|
||||
NestedViewSetMixin,
|
||||
AutoPermissionViewSetMixin,
|
||||
CreateModelMixin,
|
||||
MultipleSerializerViewSet,
|
||||
):
|
||||
queryset = BriefItem.objects.all()
|
||||
serializer_class_set = BriefItemSerializer
|
||||
|
||||
def perform_create(self, *, serializer):
|
||||
current_user = self.request.user
|
||||
parents = self.get_parents_query_dict()
|
||||
brief = Brief.objects.get(pk=parents["brief_id"])
|
||||
if not rules.can_edit_brief.test(current_user, brief):
|
||||
raise PermissionDenied() # NOTE: Should it be here?
|
||||
|
||||
return serializer.save(brief=brief)
|
||||
|
||||
|
||||
class BriefItemPhotoViewSet(
|
||||
AutoPermissionViewSetMixin, RetrieveModelMixin, MultipleSerializerViewSet,
|
||||
):
|
||||
queryset = BriefItemPhoto.objects.all()
|
||||
serializer_class_set = BriefItemPhotoSerializer
|
||||
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: BriefItemPhoto = self.get_object()
|
||||
return HttpResponseRedirect(redirect_to=photo.file.url)
|
||||
|
||||
|
||||
class CategoryViewSet(
|
||||
AutoPermissionViewSetMixin, ListModelMixin, MultipleSerializerViewSet,
|
||||
):
|
||||
queryset = Category.objects.all()
|
||||
serializer_class_set = CategorySerializer
|
||||
Reference in new issue
Block a user