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.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ProjectsAppConfig(AppConfig):
|
||||
name = "outbo.apps.projects"
|
||||
label = "projects"
|
||||
verbose_name = _("Projects")
|
||||
|
||||
|
||||
default_app_config = "outbo.apps.projects.ProjectsAppConfig"
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from outbo.apps.projects.models import Project, Member
|
||||
|
||||
|
||||
class ProjectCreationForm(forms.ModelForm):
|
||||
products_amount = forms.IntegerField(
|
||||
label=_("Products amount"), widget=forms.NumberInput, min_value=1, max_value=20
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = [
|
||||
"name",
|
||||
"description",
|
||||
"products_amount",
|
||||
"category",
|
||||
"brand",
|
||||
"printing_files_date",
|
||||
"promo_date",
|
||||
]
|
||||
|
||||
|
||||
class MemberInline(admin.TabularInline):
|
||||
model = Member
|
||||
readonly_fields = ["project"]
|
||||
extra = 1
|
||||
|
||||
|
||||
@admin.register(Project)
|
||||
class ProjectAdmin(admin.ModelAdmin):
|
||||
add_form = ProjectCreationForm
|
||||
inlines = [MemberInline]
|
||||
list_display = ('__str__', 'created_at', 'max_product_stage_description')
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
defaults = {}
|
||||
if obj is None:
|
||||
defaults["form"] = self.add_form
|
||||
defaults.update(kwargs)
|
||||
return super().get_form(request, obj, **defaults)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not change:
|
||||
obj.save(products_amount=form.cleaned_data["products_amount"])
|
||||
obj.save()
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
if obj:
|
||||
return super().get_readonly_fields(request, obj=obj) + ("slug",)
|
||||
return super().get_readonly_fields(request, obj=obj)
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
from django.db.models.fields import CharField
|
||||
|
||||
|
||||
class SlugField(CharField):
|
||||
def __init__(self, *args, slugify=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.slugify = slugify
|
||||
|
||||
def pre_save(self, model_instance, add):
|
||||
if add:
|
||||
value = self.slugify(model_instance)
|
||||
setattr(model_instance, self.attname, value)
|
||||
return value
|
||||
return super().pre_save(model_instance=model_instance, add=add)
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.db.models import QuerySet, Q
|
||||
from django_filters import rest_framework as filters, CharFilter
|
||||
|
||||
from outbo.apps.projects.models import Project
|
||||
|
||||
|
||||
class ProjectFilterSet(filters.FilterSet):
|
||||
query = CharFilter(method='query_projects')
|
||||
order_by = CharFilter(method='order_projects')
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = ('query',)
|
||||
|
||||
def query_projects(self, queryset: QuerySet, _name: str, value: str):
|
||||
return queryset.filter(
|
||||
Q(slug__icontains=value)
|
||||
| Q(name__icontains=value)
|
||||
| Q(description__icontains=value)
|
||||
| Q(product__brief__name_pol__icontains=value)
|
||||
| Q(product__brief__name_eng__icontains=value)
|
||||
).distinct()
|
||||
|
||||
def order_projects(self, queryset: QuerySet, name: str, value: str):
|
||||
if value.lower() == "mod":
|
||||
return queryset.order_by("-updated_at__date", "slug")
|
||||
if value.lower() == "pf":
|
||||
return queryset.order_by("printing_files_date", "slug")
|
||||
return queryset
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# 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 outbo.apps.projects.fields
|
||||
import rules.contrib.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("organizations", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ProjectNumber",
|
||||
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"
|
||||
),
|
||||
),
|
||||
("prefix", models.CharField(max_length=4)),
|
||||
("value", models.PositiveIntegerField(default=1)),
|
||||
],
|
||||
options={"abstract": False,},
|
||||
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Project",
|
||||
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"
|
||||
),
|
||||
),
|
||||
(
|
||||
"slug",
|
||||
outbo.apps.projects.fields.SlugField(
|
||||
max_length=15, verbose_name="project identifier"
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=254, verbose_name="name")),
|
||||
("description", models.TextField(verbose_name="description")),
|
||||
(
|
||||
"printing_files_date",
|
||||
models.DateField(verbose_name="printing files delivery date"),
|
||||
),
|
||||
("promo_date", models.DateField(verbose_name="promo date")),
|
||||
(
|
||||
"published",
|
||||
models.BooleanField(default=False, verbose_name="published"),
|
||||
),
|
||||
(
|
||||
"brand",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="organizations.Brand",
|
||||
verbose_name="brand",
|
||||
),
|
||||
),
|
||||
(
|
||||
"category",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="organizations.Category",
|
||||
verbose_name="category",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={"ordering": ["promo_date"],},
|
||||
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||
),
|
||||
]
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
# Generated by Django 2.2.8 on 2020-01-14 10:02
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import outbo.apps.core.fields
|
||||
import outbo.apps.projects.models.member
|
||||
import rules.contrib.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("projects", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Member",
|
||||
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.projects.models.member.MembershipType(
|
||||
"agency"
|
||||
),
|
||||
"agency",
|
||||
),
|
||||
(
|
||||
outbo.apps.projects.models.member.MembershipType(
|
||||
"client"
|
||||
),
|
||||
"client",
|
||||
),
|
||||
(
|
||||
outbo.apps.projects.models.member.MembershipType(
|
||||
"supplier"
|
||||
),
|
||||
"supplier",
|
||||
),
|
||||
],
|
||||
default=outbo.apps.projects.models.member.MembershipType(
|
||||
"supplier"
|
||||
),
|
||||
max_length=25,
|
||||
verbose_name="membership type",
|
||||
),
|
||||
),
|
||||
(
|
||||
"project",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="memberships",
|
||||
related_query_name="membership",
|
||||
to="projects.Project",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="memberships",
|
||||
related_query_name="membership",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={"abstract": False,},
|
||||
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 2.2.8 on 2020-01-14 13:00
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("projects", "0002_member"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="member",
|
||||
name="project",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="members",
|
||||
related_query_name="member",
|
||||
to="projects.Project",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="member",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="member",
|
||||
related_query_name="member",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 2.2.8 on 2020-02-11 14:09
|
||||
|
||||
from django.db import migrations
|
||||
import outbo.apps.core.fields
|
||||
import outbo.apps.projects.models.member
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("projects", "0003_auto_20200114_1400"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="member",
|
||||
name="role",
|
||||
field=outbo.apps.core.fields.EnumCharField(
|
||||
default=outbo.apps.projects.models.member.MembershipType("supplier"),
|
||||
enum=outbo.apps.projects.models.member.MembershipType,
|
||||
max_length=25,
|
||||
verbose_name="membership type",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 2.2.10 on 2020-02-11 16:05
|
||||
|
||||
from django.db import migrations
|
||||
import outbo.apps.core.fields
|
||||
import outbo.apps.projects.models.member
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("projects", "0004_auto_20200211_1509"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="member",
|
||||
name="role",
|
||||
field=outbo.apps.core.fields.EnumCharField(
|
||||
default=outbo.apps.projects.models.member.MembershipType("supplier"),
|
||||
enum=outbo.apps.projects.models.member.MembershipType,
|
||||
max_length=25,
|
||||
verbose_name="membership type",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,53 @@
|
||||
# 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 = [
|
||||
("projects", "0005_auto_20200211_1705"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="member",
|
||||
name="project",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="members",
|
||||
related_query_name="member",
|
||||
to="projects.Project",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="member",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="member",
|
||||
related_query_name="member",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="project",
|
||||
name="brand",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="organizations.Brand",
|
||||
verbose_name="brand",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="project",
|
||||
name="category",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="organizations.Category",
|
||||
verbose_name="category",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 2.2.16 on 2020-10-07 07:48
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('projects', '0006_auto_20200525_1025'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='project',
|
||||
options={'ordering': ['updated_at__date', 'slug']},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 2.2.16 on 2020-10-07 10:03
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('projects', '0007_auto_20201007_0948'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='project',
|
||||
options={'ordering': ['-updated_at__date', 'slug']},
|
||||
),
|
||||
]
|
||||
Whitespace-only changes.
@@ -0,0 +1,3 @@
|
||||
from .project_number import ProjectNumber
|
||||
from .project import Project
|
||||
from .member import Member
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from enum import Enum
|
||||
|
||||
from django.db import models
|
||||
from rules.contrib.models import RulesModel
|
||||
|
||||
from outbo.apps.core.fields import EnumCharField
|
||||
from outbo.apps.core.models import TimestampableDeletableMixin
|
||||
from outbo.apps.projects import rules
|
||||
from outbo.apps.projects.models import Project
|
||||
|
||||
|
||||
class MembershipType(Enum):
|
||||
AGENCY = "agency"
|
||||
CLIENT = "client"
|
||||
SUPPLIER = "supplier"
|
||||
|
||||
|
||||
class Member(TimestampableDeletableMixin, RulesModel):
|
||||
class Meta:
|
||||
rules_permissions = {
|
||||
"view": rules.can_read_project_member,
|
||||
"add": rules.can_create_project_member,
|
||||
"change": rules.can_update_project_member,
|
||||
"delete": rules.can_delete_project_member,
|
||||
}
|
||||
|
||||
user = models.ForeignKey(
|
||||
to="authentication.User",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="member",
|
||||
related_query_name="member",
|
||||
)
|
||||
project = models.ForeignKey(
|
||||
to=Project,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="members",
|
||||
related_query_name="member",
|
||||
)
|
||||
role = EnumCharField(
|
||||
verbose_name=_("membership type"),
|
||||
max_length=25,
|
||||
blank=False,
|
||||
null=False,
|
||||
default=MembershipType.SUPPLIER,
|
||||
enum=MembershipType,
|
||||
)
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rules.contrib.models import RulesModel
|
||||
|
||||
from outbo.apps.activity_log.enums import MessageType
|
||||
from outbo.apps.activity_log.models import ProjectLogItem
|
||||
from outbo.apps.core.models import TimestampableDeletableMixin, SoftDeletableManager
|
||||
from outbo.apps.products.models import Product
|
||||
from outbo.apps.projects import rules
|
||||
from outbo.apps.projects.fields import SlugField
|
||||
from outbo.apps.projects.models import ProjectNumber
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from outbo.apps.authentication.models import User
|
||||
|
||||
|
||||
class ProjectManager(SoftDeletableManager):
|
||||
def create(self, **kwargs):
|
||||
products_amount = kwargs.pop("products_amount", None)
|
||||
|
||||
project: "Project" = super().create(**kwargs)
|
||||
if products_amount is not None:
|
||||
project.add_products(products_amount=products_amount)
|
||||
project.save()
|
||||
return project
|
||||
|
||||
|
||||
class Project(TimestampableDeletableMixin, RulesModel):
|
||||
objects = ProjectManager()
|
||||
all_objects = ProjectManager(alive_only=False)
|
||||
|
||||
class Meta:
|
||||
rules_permissions = {
|
||||
"view": rules.can_read_project,
|
||||
"add": rules.can_create_project,
|
||||
"change": rules.can_update_project,
|
||||
"delete": rules.can_delete_project,
|
||||
}
|
||||
ordering = ["-updated_at__date", "slug"]
|
||||
|
||||
def slugify(self):
|
||||
brand_prefix = self.category.name[:2].upper()
|
||||
category_prefix = self.brand.name[:2].upper()
|
||||
|
||||
number = ProjectNumber.objects.get_next_number(
|
||||
prefix=f"{brand_prefix}{category_prefix}"
|
||||
)
|
||||
week_year = self.promo_date.strftime("%U%y")
|
||||
return f"#{brand_prefix}{category_prefix}{number}-{week_year}"
|
||||
|
||||
slug = SlugField(
|
||||
verbose_name=_("project identifier"), max_length=15, slugify=slugify
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
verbose_name=_("name"), max_length=254, blank=False, null=False
|
||||
)
|
||||
description = models.TextField(
|
||||
verbose_name=_("description"), blank=False, null=False
|
||||
)
|
||||
category = models.ForeignKey(
|
||||
verbose_name=_("category"),
|
||||
to="organizations.Category",
|
||||
on_delete=models.CASCADE,
|
||||
null=False,
|
||||
)
|
||||
brand = models.ForeignKey(
|
||||
verbose_name=_("brand"),
|
||||
to="organizations.Brand",
|
||||
on_delete=models.CASCADE,
|
||||
null=False,
|
||||
)
|
||||
printing_files_date = models.DateField(
|
||||
verbose_name=_("printing files delivery date"), null=False
|
||||
)
|
||||
promo_date = models.DateField(verbose_name=_("promo date"), null=False)
|
||||
|
||||
# internal fields
|
||||
|
||||
published = models.BooleanField(
|
||||
verbose_name=_("published"), blank=False, null=False, default=False
|
||||
)
|
||||
|
||||
@property
|
||||
def _max_product_stage_obj(self):
|
||||
max_product_stage_obj = (
|
||||
self.products.order_by("-stage__order").first()
|
||||
)
|
||||
if not max_product_stage_obj:
|
||||
return None
|
||||
return max_product_stage_obj
|
||||
|
||||
@property
|
||||
def max_product_stage(self):
|
||||
return self._max_product_stage_obj.stage.id
|
||||
|
||||
@property
|
||||
def max_product_stage_description(self):
|
||||
return self._max_product_stage_obj.stage.description
|
||||
|
||||
def __str__(self):
|
||||
if self.slug:
|
||||
return f"[{self.slug}] {self.name}"
|
||||
else:
|
||||
return self.name
|
||||
|
||||
def publishable_for_user(self, *, user: "User"):
|
||||
if user.is_observer or user.is_trader:
|
||||
return False
|
||||
return not self.published and all(
|
||||
[
|
||||
product.has_brief_publishable_for_user(user=user)
|
||||
for product in self.products.all()
|
||||
]
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
products_amount = kwargs.pop("products_amount", None)
|
||||
super().save(*args, **kwargs)
|
||||
if products_amount is not None:
|
||||
self.add_products(products_amount=products_amount)
|
||||
|
||||
def add_products(self, *, products_amount):
|
||||
[Product.objects.create(project=self) for _ in range(products_amount)]
|
||||
self.save()
|
||||
|
||||
def publish_as_user(self, *, user: "User"):
|
||||
self.published = True
|
||||
for product in self.products.all():
|
||||
product.send_action_emails_for_users_passing_test(
|
||||
test=lambda u: u in [m.user for m in self.members.all()]
|
||||
and u.is_agency,
|
||||
action_url="https://outbo.pl/projects/{id}/products/{pid}",
|
||||
)
|
||||
product.publish_brief_as_user(user=user)
|
||||
product.save()
|
||||
|
||||
ProjectLogItem.objects.create(
|
||||
project=self,
|
||||
created_by=user,
|
||||
message="Project published",
|
||||
message_type=MessageType.SUCCESS,
|
||||
)
|
||||
self.save()
|
||||
@@ -0,0 +1,26 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import F
|
||||
|
||||
from rules.contrib.models import RulesModel
|
||||
from outbo.apps.core.models import TimestampableDeletableMixin, SoftDeletableManager
|
||||
|
||||
|
||||
class ProjectNumberManager(SoftDeletableManager):
|
||||
def get_next_number(self, prefix):
|
||||
current = self.get_or_create(prefix=prefix)[0]
|
||||
value = current.value
|
||||
|
||||
current.value = F("value") + 1
|
||||
current.save()
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class ProjectNumber(TimestampableDeletableMixin, RulesModel):
|
||||
objects = ProjectNumberManager()
|
||||
all_objects = ProjectNumberManager(alive_only=False)
|
||||
|
||||
prefix = models.CharField(max_length=4)
|
||||
value = models.PositiveIntegerField(default=1)
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
from django.db.models import Q
|
||||
|
||||
from outbo.apps.core.rules import is_agency, is_supplier, is_client
|
||||
|
||||
|
||||
def read_project_query(user):
|
||||
assigned_to_user_query = Q(member__user=user, member__deleted_at__isnull=True)
|
||||
if is_client.test(user):
|
||||
return assigned_to_user_query & Q(published=True)
|
||||
elif (is_agency | is_supplier).test(user):
|
||||
return assigned_to_user_query & Q()
|
||||
return ~Q()
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from rules import predicate, always_deny
|
||||
|
||||
from outbo.apps.core.rules import is_supplier, is_agency, is_client
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from outbo.apps.projects.models import Project, Member
|
||||
|
||||
|
||||
@predicate
|
||||
def _project_published(_, project: "Project"):
|
||||
return project.published
|
||||
|
||||
|
||||
can_publish_project = is_supplier & ~_project_published
|
||||
|
||||
can_create_project = is_supplier
|
||||
can_read_project = is_supplier | is_agency | (is_client & _project_published)
|
||||
|
||||
can_update_project = can_publish_project
|
||||
can_delete_project = always_deny
|
||||
|
||||
|
||||
@predicate
|
||||
def _can_remove_this_member(user: "User", member: "Member"):
|
||||
project = member.project
|
||||
all_memberships = project.members
|
||||
current_user_membership = all_memberships.filter(user=user).first()
|
||||
if current_user_membership is None:
|
||||
return False
|
||||
current_role_memberships = all_memberships.filter(role=current_user_membership.role)
|
||||
return (
|
||||
member.role == current_user_membership.role
|
||||
and current_role_memberships.count() > 1
|
||||
)
|
||||
|
||||
|
||||
can_create_project_member = is_supplier
|
||||
can_read_project_member = always_deny
|
||||
can_update_project_member = always_deny
|
||||
can_delete_project_member = _can_remove_this_member
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
from rest_framework import serializers
|
||||
from rest_framework.reverse import reverse_lazy
|
||||
|
||||
from outbo.apps.projects.models import Project, Member
|
||||
from outbo.apps.projects.models.member import MembershipType
|
||||
|
||||
|
||||
class _BaseProjectSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = ["name"]
|
||||
|
||||
|
||||
class BaseMemberSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Member
|
||||
|
||||
|
||||
class MemberSerializer:
|
||||
class Create(BaseMemberSerializer):
|
||||
class Meta(BaseMemberSerializer.Meta):
|
||||
fields = ["user"]
|
||||
|
||||
class AfterCreate(BaseMemberSerializer):
|
||||
role = serializers.SerializerMethodField()
|
||||
|
||||
class Meta(BaseMemberSerializer.Meta):
|
||||
fields = ["id", "user", "role"]
|
||||
|
||||
def get_role(self, instance):
|
||||
t, val = str(instance.role).split(".", 1)
|
||||
return MembershipType[val].value
|
||||
|
||||
class List(BaseMemberSerializer):
|
||||
role = serializers.SerializerMethodField()
|
||||
|
||||
class Meta(BaseMemberSerializer.Meta):
|
||||
fields = ["id", "user", "role"]
|
||||
|
||||
def get_role(self, instance):
|
||||
t, val = str(instance.role).split(".", 1)
|
||||
return MembershipType[val].value
|
||||
|
||||
|
||||
class ProjectSerializer:
|
||||
class Retrieve(_BaseProjectSerializer):
|
||||
products = serializers.SerializerMethodField()
|
||||
publishable = serializers.SerializerMethodField()
|
||||
products_amount = serializers.SerializerMethodField()
|
||||
max_product_stage = serializers.ReadOnlyField()
|
||||
members = MemberSerializer.List(many=True)
|
||||
|
||||
class Meta(_BaseProjectSerializer.Meta):
|
||||
fields = [
|
||||
"slug",
|
||||
*_BaseProjectSerializer.Meta.fields,
|
||||
"description",
|
||||
"category",
|
||||
"members",
|
||||
"brand",
|
||||
"products_amount",
|
||||
"products",
|
||||
"printing_files_date",
|
||||
"promo_date",
|
||||
"max_product_stage",
|
||||
"published",
|
||||
"publishable",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
def get_publishable(self, project: "Project"):
|
||||
current_user = self.context["request"].user
|
||||
return project.publishable_for_user(user=current_user)
|
||||
|
||||
def get_products(self, project: "Project"):
|
||||
url = reverse_lazy("products:product-list")
|
||||
return f"{url}?project={project.pk}"
|
||||
|
||||
def get_products_amount(self, project: "Project"):
|
||||
return project.products.count()
|
||||
|
||||
class AfterCreate(_BaseProjectSerializer):
|
||||
products = serializers.SerializerMethodField()
|
||||
publishable = serializers.SerializerMethodField()
|
||||
products_amount = serializers.SerializerMethodField()
|
||||
max_product_stage = serializers.ReadOnlyField()
|
||||
members = MemberSerializer.List(many=True)
|
||||
|
||||
class Meta(_BaseProjectSerializer.Meta):
|
||||
fields = [
|
||||
"id",
|
||||
"slug",
|
||||
*_BaseProjectSerializer.Meta.fields,
|
||||
"description",
|
||||
"category",
|
||||
"brand",
|
||||
"products_amount",
|
||||
"products",
|
||||
"printing_files_date",
|
||||
"promo_date",
|
||||
"members",
|
||||
# Computed
|
||||
"max_product_stage",
|
||||
"published",
|
||||
"publishable",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
def get_publishable(self, project: "Project"):
|
||||
current_user = self.context["request"].user
|
||||
return project.publishable_for_user(user=current_user)
|
||||
|
||||
def get_products(self, project: "Project"):
|
||||
# TODO: Hyperlink?
|
||||
url = reverse_lazy("products:product-list")
|
||||
return f"{url}?project={project.pk}"
|
||||
|
||||
def get_products_amount(self, project: "Project"):
|
||||
return project.products.count()
|
||||
|
||||
class List(_BaseProjectSerializer):
|
||||
products = serializers.SerializerMethodField()
|
||||
max_product_stage = serializers.ReadOnlyField()
|
||||
members = MemberSerializer.List(many=True)
|
||||
|
||||
class Meta(_BaseProjectSerializer.Meta):
|
||||
fields = [
|
||||
"id",
|
||||
"slug",
|
||||
*_BaseProjectSerializer.Meta.fields,
|
||||
"members",
|
||||
"products",
|
||||
"printing_files_date",
|
||||
"category",
|
||||
"brand",
|
||||
# Computed
|
||||
"max_product_stage",
|
||||
"published",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
def get_products(self, instance):
|
||||
# TODO: refactor, maybe custom serializerfield
|
||||
url = reverse_lazy("products:product-list")
|
||||
return f"{url}?project={instance.pk}"
|
||||
|
||||
class Create(_BaseProjectSerializer):
|
||||
products_amount = serializers.IntegerField(min_value=1, write_only=True)
|
||||
|
||||
class Meta(_BaseProjectSerializer.Meta):
|
||||
fields = [
|
||||
*_BaseProjectSerializer.Meta.fields,
|
||||
"description",
|
||||
"category",
|
||||
"brand",
|
||||
"products_amount",
|
||||
"printing_files_date",
|
||||
"promo_date",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
# NOTE: business logic?
|
||||
def create(self, validated_data):
|
||||
products_amount = validated_data.pop("products_amount")
|
||||
project = super().create(validated_data)
|
||||
project.add_products(products_amount=products_amount)
|
||||
membership = Member.objects.create(
|
||||
user=self.context["request"].user,
|
||||
role=MembershipType.SUPPLIER,
|
||||
project=project,
|
||||
)
|
||||
project.members.add(membership)
|
||||
return project
|
||||
|
||||
class Update(serializers.Serializer):
|
||||
publish = serializers.BooleanField(default=False)
|
||||
|
||||
def update(self, instance: Project, validated_data):
|
||||
current_user = self.context["request"].user
|
||||
if validated_data["publish"] is True:
|
||||
instance.publish_as_user(user=current_user)
|
||||
return instance
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
from rest_framework_extensions.routers import ExtendedSimpleRouter
|
||||
|
||||
from outbo.apps.activity_log.viewsets import ProjectLogItemNestedViewSet
|
||||
from outbo.apps.projects.viewsets import ProjectViewSet, ProjectMemberNestedViewSet
|
||||
|
||||
app_name = "projects"
|
||||
router = ExtendedSimpleRouter()
|
||||
|
||||
_project_routes = router.register(prefix="projects", viewset=ProjectViewSet)
|
||||
_project_routes.register(
|
||||
prefix="messages",
|
||||
viewset=ProjectLogItemNestedViewSet,
|
||||
basename="project-message-item",
|
||||
parents_query_lookups=["project_id"],
|
||||
)
|
||||
_project_routes.register(
|
||||
prefix="members",
|
||||
viewset=ProjectMemberNestedViewSet,
|
||||
basename="project-member-item",
|
||||
parents_query_lookups=["project_id"],
|
||||
)
|
||||
|
||||
urlpatterns = router.urls
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
from django.db.models import Q
|
||||
from django_filters import rest_framework as filters
|
||||
|
||||
from outbo.apps.core.viewsets import (
|
||||
AutoPermissionViewSetMixin,
|
||||
MultipleSerializerViewSet,
|
||||
CreateModelMixin,
|
||||
PartialUpdateModelMixin,
|
||||
RetrieveModelMixin,
|
||||
ListModelMixin,
|
||||
DestroyModelMixin,
|
||||
NestedViewSetMixin,
|
||||
)
|
||||
from outbo.apps.projects.filters import ProjectFilterSet
|
||||
from outbo.apps.projects.models import Project, Member
|
||||
from outbo.apps.projects.models.member import MembershipType
|
||||
from outbo.apps.projects.queries import read_project_query
|
||||
from outbo.apps.projects.serializers import ProjectSerializer, MemberSerializer
|
||||
|
||||
|
||||
class ProjectViewSet(
|
||||
RetrieveModelMixin,
|
||||
ListModelMixin,
|
||||
CreateModelMixin,
|
||||
PartialUpdateModelMixin,
|
||||
AutoPermissionViewSetMixin,
|
||||
MultipleSerializerViewSet,
|
||||
):
|
||||
queryset = Project.objects.all()
|
||||
serializer_class_set = ProjectSerializer
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
filterset_class = ProjectFilterSet
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
if self.action == "list":
|
||||
qs_filter = read_project_query(self.request.user)
|
||||
qs = qs.filter(qs_filter)
|
||||
# TODO: try to refactor this poor loops
|
||||
if self.request.user.is_observer or self.request.user.is_trader:
|
||||
projects_id = []
|
||||
for project in Project.objects.filter(published=True):
|
||||
members = project.members.filter(role=self.request.user.user_role)
|
||||
for member in members:
|
||||
if member.user.organization == self.request.user.organization:
|
||||
projects_id.append(project.id)
|
||||
qs_id = qs.values_list("id", flat=True)
|
||||
qs = Project.objects.filter(Q(id__in=projects_id) | Q(id__in=qs_id))
|
||||
if self.request.user.is_trader and self.request.user.category is not None:
|
||||
qs = qs.filter(category=self.request.user.category)
|
||||
return qs
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
return super().partial_update(request=request, *args, **kwargs)
|
||||
|
||||
|
||||
class ProjectMemberNestedViewSet(
|
||||
ListModelMixin,
|
||||
CreateModelMixin,
|
||||
DestroyModelMixin,
|
||||
NestedViewSetMixin,
|
||||
AutoPermissionViewSetMixin,
|
||||
MultipleSerializerViewSet,
|
||||
):
|
||||
queryset = Member.objects.all()
|
||||
serializer_class_set = MemberSerializer
|
||||
|
||||
def perform_create(self, *, serializer: MemberSerializer.Create):
|
||||
user = serializer.validated_data["user"]
|
||||
role = None
|
||||
if user.is_agency:
|
||||
role = MembershipType.AGENCY
|
||||
elif user.is_client:
|
||||
role = MembershipType.CLIENT
|
||||
elif user.is_supplier:
|
||||
role = MembershipType.SUPPLIER
|
||||
parents = self.get_parents_query_dict()
|
||||
project = Project.objects.get(pk=parents["project_id"])
|
||||
membership = Member.objects.filter(project=project, user=user).first()
|
||||
if membership:
|
||||
serializer.instance = membership
|
||||
return serializer.save(project=project, role=role)
|
||||
Reference in new issue
Block a user