Initial commit from ZIP

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

No files matched your search

View File
Whitespace-only changes.
+18
View File
@@ -0,0 +1,18 @@
from nested_admin.nested import NestedTabularInline
from polymorphic.admin import (
PolymorphicParentModelAdmin as BasePolymorphicParentModelAdmin,
)
class PolymorphicParentModelAdmin(BasePolymorphicParentModelAdmin):
def get_child_type_choices(self, request, action):
choices = super().get_child_type_choices(request=request, action=action)
return tuple((ct_id, label.title()) for (ct_id, label) in choices)
class NestedTabularSingleItemInline(NestedTabularInline):
template = "nesting/admin/inlines/tabular_single.html"
class NestedStackedSingleItemInline(NestedTabularInline):
template = "nesting/admin/inlines/stacked_single.html"
+122
View File
@@ -0,0 +1,122 @@
from functools import partial
from django import forms
from django.db import models
from django_extensions.db.fields import (
CreationDateTimeField as BaseCreationDateTimeField,
ModificationDateTimeField as BaseModificationDateTimeField,
)
from django_fsm import FSMFieldMixin
from outbo.apps.core.forms import EnumChoiceField
class CreationDateTimeField(BaseCreationDateTimeField):
"""
CreationDateTimeField
By default, sets editable=False, blank=True, auto_now_add=True
"""
class ModificationDateTimeField(BaseModificationDateTimeField):
"""
ModificationDateTimeField
By default, sets editable=False, blank=True, auto_now=True
Sets value to now every time the object is saved.
"""
class RemovalDateTimeField(models.DateTimeField):
"""
RemovalDateTimeField
By default, sets editable=False, blank=True, null=True
"""
def __init__(self, *args, **kwargs):
kwargs.setdefault("editable", False)
kwargs.setdefault("blank", True)
kwargs.setdefault("null", True)
models.DateTimeField.__init__(self, *args, **kwargs)
def get_internal_type(self):
return "DateTimeField"
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
if self.editable is not False:
kwargs["editable"] = True
if self.blank is not True:
kwargs["blank"] = False
if self.null is not True:
kwargs["null"] = False
return name, path, args, kwargs
class FSMKeyField(FSMFieldMixin, models.ForeignKey):
def get_state(self, instance):
return instance.__dict__[self.attname]
def set_state(self, instance, state):
instance.__dict__[self.attname] = self.to_python(state)
class EnumCharField(models.CharField):
def __init__(self, *args, **kwargs):
enum = kwargs.pop("enum", None)
if enum is not None:
if kwargs.get("choices", None) is not None:
raise ValueError("Cannot provide choices for EnumCharField")
kwargs["choices"] = [(item, item.value) for item in enum]
super().__init__(*args, **kwargs)
self.validators.pop() # lengthvalidator not necessary on this one
self.enum = enum
def get_internal_type(self):
return "CharField"
def formfield(self, **kwargs):
enum_form_class = partial(EnumChoiceField, enum=self.enum)
defaults = {
"widget": forms.Select,
"form_class": enum_form_class,
"choices": [(str(item), item.value) for item in self.enum],
}
defaults.update(kwargs)
return super().formfield(**defaults)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
kwargs["enum"] = self.enum
if "choices" in kwargs:
del kwargs["choices"]
return name, path, args, kwargs
def to_python(self, value):
value = super().to_python(value)
if value is not None:
if not isinstance(value, self.enum):
enum_name, enum_value = value.split(".", 1)
if enum_name == self.enum.__name__:
return self.enum[enum_value]
return value
def get_prep_value(self, value):
value = super().get_prep_value(value)
if value is None:
return value
if isinstance(value, self.enum):
return str(value)
return value
def from_db_value(self, value, *args):
if value is not None:
enum_name, enum_value = value.split(".", 1)
if enum_name == self.enum.__name__:
return self.enum[enum_value]
return value
+29
View File
@@ -0,0 +1,29 @@
from django import forms
from django.core.exceptions import ValidationError
class EnumChoiceField(forms.ChoiceField):
def __init__(self, enum, **kwargs):
kwargs.setdefault("choices", [(item, item.value) for item in enum])
super().__init__(**kwargs)
self.enum = enum
def prepare_value(self, value):
if isinstance(value, self.enum):
return str(value)
return value
def to_python(self, value):
value = super().to_python(value)
if not isinstance(value, self.enum):
try:
enum_name, enum_value = value.split(".", 1)
if enum_name == self.enum.__name__:
value = self.enum[enum_value]
except ValueError:
raise ValidationError(self.error_messages["invalid"], code="invalid")
return value
def clean(self, value):
value = super().clean(value)
return self.enum(value)
View File
Whitespace-only changes.
Whitespace-only changes.
+236
View File
@@ -0,0 +1,236 @@
from typing import TYPE_CHECKING, Tuple, Iterable
from django.core.management.base import BaseCommand
from faker import Factory
from tests.factories.authentication import (
UserFactory,
AgencyUserFactory,
ClientUserFactory,
SupplierUserFactory,
)
from tests.factories.organizations import (
CategoryFactory,
BrandFactory,
)
from tests.factories.products import ProductStageFactory, PackagingTypeFactory
from tests.factories.token import TokenFactory
faker = Factory()
if TYPE_CHECKING:
from outbo.apps.organizations.models import ClientOrganization
class Command(BaseCommand):
help = "Seed with some existing data"
def add_arguments(self, parser):
parser.add_argument("--dev", action="store_true", help="Seed with dev data")
def __make_users_and_organizations(self, *, password=None):
self.stdout.write("Seeding organizations and users...")
if password is None:
password = "password"
UserFactory(
username="admin",
password=password,
email="admin@example.com",
is_superuser=True,
is_staff=True,
)
agency = AgencyUserFactory(
username="agency",
password=password,
email="agency@example.com",
organization__name="Sample agency",
)
client = ClientUserFactory(
username="client",
password=password,
email="client@example.com",
organization__name="Sample client",
)
supplier = SupplierUserFactory(
username="supplier",
password=password,
email="supplier@example.com",
organization__name="Sample supplier",
)
TokenFactory(user=agency, key="6f9afaa36ee8b224181dc14e2ca9af33e97c0f71")
TokenFactory(user=client, key="481372f60325289029679df25c72e6ccd05892ed")
TokenFactory(user=supplier, key="d23d8f187614c8a76fe7316470e6f50ff5bc4188")
def __make_product_stages(self):
self.stdout.write("Seeding product stages...")
ProductStageFactory(
id="waiting_for_brief",
label="1",
group=1,
description="Briefing",
next_action="Oczekuje na uzupełnienie briefu",
order=1,
)
ProductStageFactory(
id="waiting_for_brief_accept_by_agency",
label="1A",
group=1,
description="Akceptacja agencji",
next_action="Oczekuje na akceptację agencji",
order=2,
)
ProductStageFactory(
id="waiting_for_brief_accept_by_client",
label="1B",
group=1,
description="Akceptacja klienta",
next_action="Oczekuje na akceptację klienta",
order=3,
)
ProductStageFactory(
id="waiting_for_draft",
label="2A",
group=2,
description="Publikacja draftu",
next_action="Oczekuje na publikację draftu",
order=4,
)
ProductStageFactory(
id="waiting_for_draft_accept_by_supplier",
label="2B",
group=2,
description="Akceptacja dostawcy",
next_action="Oczekuje na akceptację dostawcy",
order=5,
)
ProductStageFactory(
id="waiting_for_draft_accept_by_client",
label="2C",
group=2,
description="Akceptacja klienta",
next_action="Oczekuje na akceptację klientget_random_stringa",
order=6,
)
ProductStageFactory(
id="waiting_for_ff",
label="3A",
group=3,
description="Publikacja FF",
next_action="Oczekuje na publikację draftu",
order=7,
)
ProductStageFactory(
id="waiting_for_ff_accept_by_supplier",
label="3B",
group=3,
description="Akceptacja dostawcy",
next_action="Oczekuje na akceptację dostawcy",
order=8,
)
ProductStageFactory(
id="waiting_for_ff_accept_by_client",
label="3C",
group=3,
description="Akceptacja klienta",
next_action="Oczekuje na akceptację klienta",
order=9,
)
ProductStageFactory(
id="waiting_for_pf",
label="4A",
group=4,
description="Publikacja printing files",
next_action="Oczekuje na publikację printing files",
order=10,
)
ProductStageFactory(
id="waiting_for_pf_accept_by_supplier",
label="4B",
group=4,
description="Akceptacja dostawcy",
next_action="Oczekuje na akceptację dostawcy",
order=11,
)
ProductStageFactory(
id="waiting_for_pf_accept_by_client",
label="4C",
group=4,
description="Akceptacja klienta",
next_action="Oczekuje na akceptację klienta",
order=12,
)
ProductStageFactory(
id="completed",
label="5",
group=5,
description="Zako\u0144czono",
next_action="Oczekuje na archiwizację",
order=13,
)
def __make_packaging_types(self, *, items: Iterable[str] = None):
self.stdout.write("Seeding packaging types...")
if items:
for name in items:
PackagingTypeFactory(name=name)
else:
PackagingTypeFactory.create_batch(size=20)
def __make_brands(
self, *, items: Iterable[Tuple[str, "ClientOrganization"]] = None
):
self.stdout.write("Seeding brands...")
if items:
for name, client in items:
BrandFactory(name=name, client=client)
else:
BrandFactory.create_batch(size=20)
def __make_categories(self, *, items: Iterable[str] = None):
self.stdout.write("Seeding categories...")
if items:
for item in items:
CategoryFactory(name=item)
else:
CategoryFactory.create_batch(size=20)
def handle(self, *args, **options):
self.stdout.write("Seeding the database...")
if not options["dev"]:
packaging_types = (
"Carton/gift box",
"Header",
"Hangtag",
"Paper band",
"Paper board",
"Blister",
"Image insert",
"Simple band",
"Label/sticker",
"Simple CDU/Displaybox",
)
raise NotImplementedError()
password = "!secret123"
self.__make_users_and_organizations(password=password)
self.__make_product_stages()
self.__make_packaging_types()
self.__make_brands()
self.__make_categories()
self.stdout.write("Seeding completed.")
self.stdout.write("Credentials:")
self.stdout.write(f" admin: {password}")
self.stdout.write(f" agency: {password}")
self.stdout.write(f" client: {password}")
self.stdout.write(f" supplier: {password}")
+42
View File
@@ -0,0 +1,42 @@
# Generated by Django 2.2.16 on 2021-01-21 14:43
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="EmailLog",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("sender", models.CharField(max_length=254)),
("receiver", models.CharField(max_length=254)),
("time", models.DateTimeField()),
(
"status",
models.CharField(
choices=[
("sending", "sending"),
("sent", "sent"),
("queued", "queued"),
("invalid", "invalid"),
("rejected", "rejected"),
("failed", "failed"),
("unknown", "unknown"),
],
max_length=20,
),
),
],
),
]
View File
Whitespace-only changes.
+117
View File
@@ -0,0 +1,117 @@
from datetime import datetime
from django.conf import settings
from django.db import models
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from model_utils import Choices
from polymorphic.base import PolymorphicModelBase
from polymorphic.managers import PolymorphicManager
from polymorphic.models import PolymorphicModel
from polymorphic.query import PolymorphicQuerySet
from rules.contrib.models import RulesModelBaseMixin, RulesModelMixin
from outbo.apps.core.fields import (
RemovalDateTimeField,
CreationDateTimeField,
ModificationDateTimeField,
)
from outbo.apps.core.utils import get_now
EmailLogStatus = Choices("sending", "sent", "queued", "invalid", "rejected", "failed", "unknown")
class EmailLog(models.Model):
sender = models.CharField(max_length=254, blank=False, null=False)
receiver = models.CharField(max_length=254, blank=False, null=False)
time = models.DateTimeField(blank=False, null=False)
status = models.CharField(max_length=20, blank=False, null=False, choices=EmailLogStatus)
if "anymail" in settings.INSTALLED_APPS:
from anymail.signals import pre_send, post_send
from anymail.message import AnymailStatus
from django.core.mail import EmailMessage
def log_email_message(sender: str, receiver: str, time: datetime, status: str):
try:
EmailLog.objects.create(sender=sender, receiver=receiver, time=time, status=status)
except:
pass
@receiver(pre_send)
def handle_pre_send(sender, message: EmailMessage, **kwargs):
log_email_message(
sender=str(message.from_email),
receiver=", ".join(message.to),
time=get_now(),
status=EmailLogStatus.sending,
)
@receiver(post_send)
def handle_post_send(sender, message: EmailMessage, status: AnymailStatus, esp_name, **kwargs):
for email, recipient_status in status.recipients.items():
log_email_message(
sender=str(message.from_email),
receiver=email,
time=get_now(),
status=recipient_status.status or "failed",
)
class SoftDeletableQuerySet(models.QuerySet):
def delete(self):
return super().update(deleted_at=get_now())
def hard_delete(self):
return super().delete()
class SoftDeletableManager(models.Manager):
queryset_class = SoftDeletableQuerySet
def __init__(self, *args, **kwargs):
self.alive_only = kwargs.pop("alive_only", True)
super().__init__(*args, **kwargs)
def get_queryset(self):
if self.alive_only:
return self.queryset_class(self.model).filter(deleted_at=None)
return self.queryset_class(self.model)
class TimestampableDeletableMixin(models.Model):
objects = SoftDeletableManager()
all_objects = SoftDeletableManager(alive_only=False)
created_at = CreationDateTimeField(_("Created at"))
updated_at = ModificationDateTimeField(_("Updated at"))
deleted_at = RemovalDateTimeField(_("Deleted at"))
class Meta:
abstract = True
def delete(self, using=None, **kwargs):
self.deleted_at = get_now()
# TODO: handle autodeletion of nested stuff
self.save(using=using)
def __str__(self):
return super().__str__() if self.deleted_at is None else f"{super().__str__()} (deleted)"
class RulesPolymorphicModelBase(RulesModelBaseMixin, PolymorphicModelBase):
pass
class RulesPolymorphicModel(RulesModelMixin, PolymorphicModel, metaclass=RulesPolymorphicModelBase):
class Meta:
abstract = True
class SoftDeletablePolymorphicQuerySet(SoftDeletableQuerySet, PolymorphicQuerySet):
pass
class SoftDeletablePolymorphicManager(SoftDeletableManager, PolymorphicManager):
queryset_class = SoftDeletablePolymorphicQuerySet
+26
View File
@@ -0,0 +1,26 @@
from djangorestframework_camel_case.parser import CamelCaseMultiPartParser
from formencode.variabledecode import variable_decode
from rest_framework import parsers
from rest_framework.parsers import MultiPartParser
class NestedMultiPartParser(MultiPartParser):
def parse(self, stream, media_type=None, parser_context=None):
result = super().parse(
stream=stream, media_type=media_type, parser_context=parser_context
)
data = variable_decode(result.data)
files = variable_decode(result.files)
return parsers.DataAndFiles(data, files)
class CamelCaseNestedMultiPartParser(CamelCaseMultiPartParser):
def parse(self, stream, media_type=None, parser_context=None):
result = super().parse(
stream=stream, media_type=media_type, parser_context=parser_context
)
data = variable_decode(result.data)
files = variable_decode(result.files)
return parsers.DataAndFiles(data, files)
+16
View File
@@ -0,0 +1,16 @@
from rules import predicate
@predicate
def is_agency(user):
return user.is_authenticated and user.is_agency
@predicate
def is_client(user):
return user.is_authenticated and user.is_client
@predicate
def is_supplier(user):
return user.is_authenticated and user.is_supplier
+39
View File
@@ -0,0 +1,39 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_polymorphic.serializers import (
PolymorphicSerializer as BasePolymorphicSerializer,
)
class PolymorphicSerializer(BasePolymorphicSerializer):
resource_type_field_name = "type"
def to_resource_type(self, model_or_instance):
return model_or_instance._meta.verbose_name
class EnumCharField(serializers.ChoiceField):
default_error_messages = {"invalid_choice": _('"{input}" is not a valid choice.')}
def __init__(self, enum, **kwargs):
self.enum = enum
kwargs.setdefault("choices", [(item, item.value) for item in enum])
super().__init__(**kwargs)
self.enum = enum
def to_internal_value(self, value):
value = super().to_representation(value)
if not isinstance(value, self.enum):
try:
enum_name, enum_value = value.split(".", 1)
if enum_name == self.enum.__name__:
value = self.enum[enum_value]
except ValueError:
raise ValidationError(self.error_messages["invalid"], code="invalid")
return value
def to_representation(self, value):
if isinstance(value, self.enum):
return str(value)
return value
+13
View File
@@ -0,0 +1,13 @@
from storages.backends.s3boto3 import S3Boto3Storage
from storages.utils import setting
class S3StaticStorage(S3Boto3Storage):
bucket_name = setting("AWS_STORAGE_BUCKET_STATIC_NAME")
default_acl = "public-read"
querystring_auth = False
class S3MediaStorage(S3Boto3Storage):
bucket_name = setting("AWS_STORAGE_BUCKET_MEDIA_NAME")
default_acl = None
+9
View File
@@ -0,0 +1,9 @@
from django.urls import path
from outbo.apps.core.views import RootApiView
from outbo.apps.products.views import ExportBriefToPDFApiView
urlpatterns = [
path("", RootApiView.as_view(), name="root"),
path("briefs/<int:pk>/export-to-pdf/", ExportBriefToPDFApiView.as_view(), name="export_brief_to_pdf"),
]
+13
View File
@@ -0,0 +1,13 @@
from django.utils import timezone
def get_now():
return timezone.now()
def get_current_date():
return timezone.localtime(get_now()).date()
def is_empty_set(tested_set):
return tested_set == set()
+10
View File
@@ -0,0 +1,10 @@
from django.conf import settings
from rest_framework import permissions, views, response
class RootApiView(views.APIView):
permission_classes = [permissions.AllowAny]
def get(self, request):
print(request)
return response.Response({"version": settings.APP_VERSION})
+160
View File
@@ -0,0 +1,160 @@
import six
from django.db import transaction
from django.db.models import Model
from rest_framework import status, mixins
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.viewsets import GenericViewSet
from rest_framework_extensions.mixins import (
NestedViewSetMixin as BaseNestedViewSetMixin,
)
from rest_framework_extensions.settings import extensions_api_settings
from rules.contrib.rest_framework import (
AutoPermissionViewSetMixin as BaseAutoPermissionViewSetMixin,
)
class AutoPermissionViewSetMixin(BaseAutoPermissionViewSetMixin):
permission_type_map = {
**BaseAutoPermissionViewSetMixin.permission_type_map,
"metadata": None,
}
class CreateModelMixin:
def create(self, request, *args, **kwargs):
if isinstance(request.data, list):
with transaction.atomic():
response = []
for item in request.data:
write_serializer = self.get_serializer(data=item)
write_serializer.is_valid(raise_exception=True)
instance = self.perform_create(serializer=write_serializer)
read_serializer = self.get_read_serializer(instance=instance)
response.append(read_serializer.data)
return Response(response, status=status.HTTP_201_CREATED)
else:
write_serializer = self.get_serializer(data=request.data)
write_serializer.is_valid(raise_exception=True)
instance = self.perform_create(serializer=write_serializer)
read_serializer = self.get_read_serializer(instance=instance)
headers = self.get_success_headers(read_serializer.data)
return Response(
read_serializer.data, status=status.HTTP_201_CREATED, headers=headers
)
def perform_create(self, *, serializer):
return serializer.save()
def get_success_headers(self, data):
try:
return {"Location": str(data[api_settings.URL_FIELD_NAME])}
except (TypeError, KeyError):
return {}
class UpdateModelMixin:
def update(self, request, *args, **kwargs):
instance = self.get_object()
write_serializer = self.get_serializer(
instance, data=request.data, partial=False
)
write_serializer.is_valid(raise_exception=True)
instance = write_serializer.save()
if getattr(instance, "_prefetched_objects_cache", None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
read_serializer = self.get_read_serializer(instance=instance)
return Response(read_serializer.data)
class PartialUpdateModelMixin:
def partial_update(self, request, *args, **kwargs):
instance = self.get_object()
write_serializer = self.get_serializer(
instance, data=request.data, partial=True
)
write_serializer.is_valid(raise_exception=True)
instance = write_serializer.save()
if getattr(instance, "_prefetched_objects_cache", None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
read_serializer = self.get_read_serializer(instance=instance)
return Response(read_serializer.data)
class RetrieveModelMixin(mixins.RetrieveModelMixin):
pass
class ListModelMixin(mixins.ListModelMixin):
pass
class DestroyModelMixin(mixins.DestroyModelMixin):
pass
class MultipleSerializerViewSet(GenericViewSet):
serializer_class_set = None
serializer_map = {
"list": "List",
"retrieve": "Retrieve",
"create": "Create",
"partial_update": "Update",
}
def get_serializer_class(self):
assert (
self.serializer_class_set is not None
), "serializer_class_set should be defined"
if self.action not in self.serializer_map:
raise KeyError(
f"""serializer_map does not contain serializer for "{self.action}" method"""
)
return getattr(self.serializer_class_set, self.serializer_map[self.action])
def get_read_serializer(self, *args, **kwargs):
assert (
self.serializer_class_set is not None
), "serializer_class_set should be defined"
if self.action != "create":
if getattr(self.serializer_class_set, "Retrieve", None) is None:
raise KeyError(
f"""serializer_class_set does not contain "Retrieve" serializer"""
)
serializer_class = self.serializer_class_set.Retrieve
else:
if (
getattr(self.serializer_class_set, "Retrieve", None) is None
and getattr(self.serializer_class_set, "AfterCreate", None) is None
):
raise KeyError(
f"""serializer_class_set does not contain "Retrieve" nor "AfterCreate" serializer and is used in "create" action."""
)
if getattr(self.serializer_class_set, "AfterCreate", None) is not None:
serializer_class = self.serializer_class_set.AfterCreate
else:
serializer_class = self.serializer_class_set.Retrieve
kwargs["context"] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
class NestedViewSetMixin(BaseNestedViewSetMixin):
pass