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

Vendored
BIN
View File
Binary file not shown.
Executable
+1
View File
@@ -0,0 +1 @@
.idea/
Executable
+151
View File
@@ -0,0 +1,151 @@
stages:
- docker-build
- deploy
build-web-release:
stage: docker-build
image: docker:latest
only:
- /^release/.*$/
variables:
DOCKER_DRIVER: "overlay2"
OUTBO_BACKEND_URL: "https://outbo-api.stage.ermlab.com"
services:
- docker:dind
before_script:
- docker version
- docker info
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
script:
- docker pull ${CI_REGISTRY_IMAGE}/web:${CI_COMMIT_BEFORE_SHA} || true
- docker pull ${CI_REGISTRY_IMAGE}/web:latest || true
- docker build -f web.Dockerfile --build-arg outbo_env=staging --build-arg backend_url=https://outbo-api.stage.ermlab.com/ --cache-from ${CI_REGISTRY_IMAGE}/web:${CI_COMMIT_BEFORE_SHA} --cache-from ${CI_REGISTRY_IMAGE}/web:latest -t ${CI_REGISTRY_IMAGE}/web:latest -t ${CI_REGISTRY_IMAGE}/web:${CI_COMMIT_SHA} ./evg-outbo-web
- docker push ${CI_REGISTRY_IMAGE}/web:latest
- docker push ${CI_REGISTRY_IMAGE}/web:${CI_COMMIT_SHA}
after_script:
- docker logout ${CI_REGISTRY}
build-web-master:
stage: docker-build
image: docker:latest
only:
- master
variables:
DOCKER_DRIVER: "overlay2"
services:
- docker:dind
before_script:
- docker version
- docker info
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
script:
- docker pull ${CI_REGISTRY_IMAGE}/web:${CI_COMMIT_BEFORE_SHA} || true
- docker pull ${CI_REGISTRY_IMAGE}/web:latest || true
- docker build --build-arg outbo_env=production --build-arg backend_url=https://api.outbo.pl/ -f web.Dockerfile --cache-from ${CI_REGISTRY_IMAGE}/web:${CI_COMMIT_BEFORE_SHA} --cache-from ${CI_REGISTRY_IMAGE}/web:latest -t ${CI_REGISTRY_IMAGE}/web:latest -t ${CI_REGISTRY_IMAGE}/web:${CI_COMMIT_SHA} ./evg-outbo-web
- docker push ${CI_REGISTRY_IMAGE}/web:latest
- docker push ${CI_REGISTRY_IMAGE}/web:${CI_COMMIT_SHA}
after_script:
- docker logout ${CI_REGISTRY}
build-api:
stage: docker-build
image: docker:latest
only:
- /^release/.*$/
- master
variables:
DOCKER_DRIVER: "overlay2"
services:
- docker:dind
before_script:
- docker version
- docker info
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
script:
- docker pull ${CI_REGISTRY_IMAGE}/api:${CI_COMMIT_BEFORE_SHA} || true
- docker pull ${CI_REGISTRY_IMAGE}/api:latest || true
- docker build -f api.Dockerfile --cache-from ${CI_REGISTRY_IMAGE}/api:${CI_COMMIT_BEFORE_SHA} --cache-from ${CI_REGISTRY_IMAGE}/api:latest -t ${CI_REGISTRY_IMAGE}/api:latest -t ${CI_REGISTRY_IMAGE}/api:${CI_COMMIT_SHA} ./evg-outbo-api
- docker push ${CI_REGISTRY_IMAGE}/api:latest
- docker push ${CI_REGISTRY_IMAGE}/api:${CI_COMMIT_SHA}
after_script:
- docker logout ${CI_REGISTRY}
deploy-staging:
stage: deploy
image: kroniak/ssh-client
only:
- /^release/.*$/
services:
- docker:dind
before_script:
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
- echo "${STAGE_KEY}" > ~/.ssh/id_ed25519
- chmod 600 ~/.ssh/id_ed25519
- eval "$(ssh-agent -s)"
- ssh-add ~/.ssh/id_ed25519
- ssh-keyscan -H ${STAGE_HOST} >> ~/.ssh/known_hosts
script:
- scp -r ./docker-compose.staging.yml ${STAGE_USER}@${STAGE_HOST}:~/docker-compose/evg-outbo/docker-compose.yml
- >
ssh ${STAGE_USER}@${STAGE_HOST} << EOF
docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
cd ~/docker-compose/evg-outbo/
sed -i -e 's#__FRONTEND_IMAGE__#${CI_REGISTRY_IMAGE}/web:${CI_COMMIT_SHA}#' docker-compose.yml
sed -i -e 's#__BACKEND_IMAGE__#${CI_REGISTRY_IMAGE}/api:${CI_COMMIT_SHA}#' docker-compose.yml
docker pull ${CI_REGISTRY_IMAGE}/api:${CI_COMMIT_SHA}
docker pull ${CI_REGISTRY_IMAGE}/web:${CI_COMMIT_SHA}
docker-compose rm --force backend
docker-compose rm --force frontend
docker-compose up -d backend
docker-compose up -d frontend
EOF
deploy-production:
stage: deploy
image: kroniak/ssh-client
only:
- master
services:
- docker:dind
before_script:
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
- echo "${PROD_KEY}" > ~/.ssh/id_ed25519
- chmod 600 ~/.ssh/id_ed25519
- eval "$(ssh-agent -s)"
- ssh-add ~/.ssh/id_ed25519
- ssh-keyscan -H ${PROD_HOST} >> ~/.ssh/known_hosts
script:
- scp -r ./docker-compose.prod.yml ${PROD_USER}@${PROD_HOST}:~/docker-compose.yml
- >
ssh ${PROD_USER}@${PROD_HOST} << EOF
docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
sed -i -e 's#COMPOSE_BACKEND_IMAGE=.*#COMPOSE_BACKEND_IMAGE=${CI_REGISTRY_IMAGE}/api:${CI_COMMIT_SHA}#' .env
sed -i -e 's#COMPOSE_FRONTEND_IMAGE=.*#COMPOSE_FRONTEND_IMAGE=${CI_REGISTRY_IMAGE}/web:${CI_COMMIT_SHA}#' .env
docker pull ${CI_REGISTRY_IMAGE}/web:${CI_COMMIT_SHA}
docker pull ${CI_REGISTRY_IMAGE}/api:${CI_COMMIT_SHA}
docker-compose rm --force backend
docker-compose rm --force frontend
docker-compose up -d backend
docker-compose up -d frontend
EOF
Executable
+5
View File
@@ -0,0 +1,5 @@
## EVG Outbo
### Technology stack
1. React 16.9 + Typescript 3.6
1. Django 2.2
Executable
+24
View File
@@ -0,0 +1,24 @@
FROM python:3.8
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN apt-get update && apt-get install -y nano && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /code && chown -R www-data:www-data /code
RUN mkdir -p /var/www && chown -R www-data:www-data /var/www
USER www-data
ENV PATH /var/www/.local/bin:${PATH}
WORKDIR /code
COPY pyproject.toml poetry.lock /code/
RUN pip install --upgrade -U pip
RUN pip install -U poetry
RUN poetry config virtualenvs.create false --local
RUN poetry install --no-dev --extras "production"
COPY . /code/
CMD exec gunicorn outbo.wsgi:application --access-logfile - --bind 0.0.0.0:8000 --workers 2
+21
View File
@@ -0,0 +1,21 @@
version: "3.8"
services:
db:
image: postgres:12.3
restart: always
environment:
- POSTGRES_USER=post
- POSTGRES_PASSWORD=post1
- POSTGRES_DB=post
volumes:
- ../.local/outbo/db:/var/lib/postgresql/data
ports:
- "127.0.0.1:5432:5432"
networks:
- internal-network
- external-network
networks:
internal-network:
internal: true
external-network:
+65
View File
@@ -0,0 +1,65 @@
version: "3.2"
services:
db:
env_file:
- .env
image: postgres:11
restart: unless-stopped
volumes:
- ./db:/var/lib/postgresql/data
networks:
- internal-network
pgadmin:
env_file:
- .env
image: dpage/pgadmin4
volumes:
- ./pgadmin:/var/lib/pgadmin
restart: unless-stopped
depends_on:
- db
networks:
- internal-network
backend:
env_file:
- .env
image: "${COMPOSE_BACKEND_IMAGE}"
restart: unless-stopped
depends_on:
- db
networks:
- internal-network
- external-network
frontend:
env_file:
- .env
image: "${COMPOSE_FRONTEND_IMAGE}"
restart: unless-stopped
depends_on:
- backend
networks:
- internal-network
server:
env_file:
- .env
image: "abiosoft/caddy"
restart: unless-stopped
volumes:
- ./Caddyfile:/etc/Caddyfile
- ./.caddy:/root/.caddy
ports:
- 80:80
- 443:443
networks:
- internal-network
- external-network
networks:
internal-network:
internal: true
external-network:
+60
View File
@@ -0,0 +1,60 @@
version: "3.2"
services:
db:
image: postgres:11
restart: "no"
env_file:
- staging.env
volumes:
- ./db:/var/lib/postgresql/data
networks:
- internal-network
pgadmin:
env_file:
- staging.env
image: dpage/pgadmin4
volumes:
- ./pgadmin:/var/lib/pgadmin
ports:
- "${COMPOSE_PGADMIN_PORT}:80"
restart: "no"
depends_on:
- db
networks:
- internal-network
- external-network
backend:
image: "__BACKEND_IMAGE__"
restart: "no"
command: bash -c "python manage.py collectstatic --no-input && python manage.py migrate --no-input && exec gunicorn outbo.wsgi:application --access-logfile - --bind 0.0.0.0:8000 --workers 2"
env_file:
- staging.env
volumes:
- '/var/www/html/evg-outbo:/host'
ports:
- "${COMPOSE_BACKEND_PORT}:8000"
depends_on:
- db
networks:
- internal-network
- external-network
frontend:
image: "__FRONTEND_IMAGE__"
restart: "no"
env_file:
- staging.env
ports:
- "${COMPOSE_FRONTEND_PORT}:80"
depends_on:
- backend
networks:
- internal-network
- external-network
networks:
internal-network:
internal: true
external-network:
+10
View File
@@ -0,0 +1,10 @@
version: "3.2"
services:
backend:
build:
context: ./evg-outbo-api
dockerfile: api.Dockerfile
frontend:
build:
context: ./evg-outbo-web
dockerfile: web.Dockerfile
BIN
View File
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
[run]
branch = True
source = outbo
omit =
*/migrations/*
[report]
exclude_lines =
pragma: no cover
if __name__ == .__main__.:
if TYPE_CHECKING:
[html]
directory = htmlcov/
+5
View File
@@ -0,0 +1,5 @@
[flake8]
max-line-length = 80
max-complexity = 18
select = C,E,F,W,B,B950
ignore = E203, E501, W503
+286
View File
@@ -0,0 +1,286 @@
# Created by https://www.gitignore.io/api/python,django,pycharm+all
# Edit at https://www.gitignore.io/?templates=python,django,pycharm+all
### Django ###
*.log
*.pot
*.pyc
__pycache__/
local_settings.py
db.sqlite3
media
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
# in your Git repository. Update and uncomment the following line accordingly.
# <django-project-name>/staticfiles/
### Django.Python Stack ###
# Byte-compiled / optimized / DLL files
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
# Django stuff:
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
dev.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
### PyCharm+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### PyCharm+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
# Sonarlint plugin
.idea/sonarlint
### Python ###
# Byte-compiled / optimized / DLL files
# C extensions
# Distribution / packaging
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
# Installer logs
# Unit test / coverage reports
# Translations
# Django stuff:
# Flask stuff:
# Scrapy stuff:
# Sphinx documentation
# PyBuilder
# Jupyter Notebook
# IPython
# pyenv
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# celery beat schedule file
# SageMath parsed files
# Environments
# Spyder project settings
# Rope project settings
# mkdocs documentation
# mypy
# Pyre type checker
# End of https://www.gitignore.io/api/python,django,pycharm+all
# static & upload files
/static/
/uploads/
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
from patches import patches
def main():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "outbo.settings")
os.environ.setdefault("DJANGO_CONFIGURATION", "Dev")
from configurations.management import execute_from_command_line
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()
View File
Whitespace-only changes.
View File
Whitespace-only changes.
View File
Whitespace-only changes.
View File
Whitespace-only changes.
+7
View File
@@ -0,0 +1,7 @@
from enum import Enum
class MessageType(Enum):
USER = "User"
SUCCESS = "Success"
FAILURE = "Failure"
@@ -0,0 +1,107 @@
# Generated by Django 2.2.7 on 2019-11-29 08:57
from django.db import migrations, models
import django.db.models.deletion
import outbo.apps.activity_log.enums
import outbo.apps.core.fields
import rules.contrib.models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="LogItem",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
outbo.apps.core.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="Created at"
),
),
(
"updated_at",
outbo.apps.core.fields.ModificationDateTimeField(
auto_now=True, verbose_name="Updated at"
),
),
(
"deleted_at",
outbo.apps.core.fields.RemovalDateTimeField(
blank=True, editable=False, null=True, verbose_name="Deleted at"
),
),
("message", models.TextField(verbose_name="message")),
(
"message_type",
models.CharField(
choices=[
(outbo.apps.activity_log.enums.MessageType("User"), "User"),
(
outbo.apps.activity_log.enums.MessageType("Success"),
"Success",
),
(
outbo.apps.activity_log.enums.MessageType("Failure"),
"Failure",
),
],
default=outbo.apps.activity_log.enums.MessageType("User"),
max_length=25,
verbose_name="message type",
),
),
],
options={"abstract": False,},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.CreateModel(
name="ProductLogItem",
fields=[
(
"logitem_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="activity_log.LogItem",
),
),
],
options={"ordering": ["-created_at"],},
bases=("activity_log.logitem",),
),
migrations.CreateModel(
name="ProjectLogItem",
fields=[
(
"logitem_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="activity_log.LogItem",
),
),
],
options={"ordering": ["-created_at"],},
bases=("activity_log.logitem",),
),
]
@@ -0,0 +1,39 @@
# Generated by Django 2.2.7 on 2019-11-29 08:57
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("activity_log", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="logitem",
name="created_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
verbose_name="created by",
),
),
migrations.AddField(
model_name="logitem",
name="polymorphic_ctype",
field=models.ForeignKey(
editable=False,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="polymorphic_activity_log.logitem_set+",
to="contenttypes.ContentType",
),
),
]
@@ -0,0 +1,28 @@
# Generated by Django 2.2.7 on 2019-11-29 08:57
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
("projects", "0001_initial"),
("activity_log", "0002_auto_20191129_0957"),
]
operations = [
migrations.AddField(
model_name="projectlogitem",
name="project",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="log_items",
related_query_name="log_item",
to="projects.Project",
verbose_name="project",
),
),
]
@@ -0,0 +1,38 @@
# Generated by Django 2.2.7 on 2019-11-29 08:57
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
("activity_log", "0003_projectlogitem_project"),
("products", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="productlogitem",
name="product",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="log_items",
related_query_name="log_item",
to="products.Product",
verbose_name="product",
),
),
migrations.AddField(
model_name="productlogitem",
name="stage",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="products.ProductStage",
),
),
]
@@ -0,0 +1,56 @@
# Generated by Django 2.2.10 on 2020-05-25 08:25
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("activity_log", "0004_auto_20191129_0957"),
]
operations = [
migrations.AlterField(
model_name="logitem",
name="created_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="created by",
),
),
migrations.AlterField(
model_name="productlogitem",
name="product",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="log_items",
related_query_name="log_item",
to="products.Product",
verbose_name="product",
),
),
migrations.AlterField(
model_name="productlogitem",
name="stage",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="products.ProductStage",
),
),
migrations.AlterField(
model_name="projectlogitem",
name="project",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="log_items",
related_query_name="log_item",
to="projects.Project",
verbose_name="project",
),
),
]
Whitespace-only changes.
+65
View File
@@ -0,0 +1,65 @@
from django.db import models
from django.utils.translation import pgettext, gettext_lazy as _
from rules import always_allow
from outbo.apps.activity_log.enums import MessageType
from outbo.apps.core.models import (
TimestampableDeletableMixin,
RulesPolymorphicModel,
SoftDeletablePolymorphicManager,
)
class LogItem(TimestampableDeletableMixin, RulesPolymorphicModel):
objects = SoftDeletablePolymorphicManager()
all_objects = SoftDeletablePolymorphicManager(alive_only=False)
message = models.TextField(verbose_name=_("message"))
message_type = models.CharField(
verbose_name=_("message type"),
max_length=25,
blank=False,
null=False,
default=MessageType.USER,
choices=((type, type.value) for type in MessageType),
)
created_by = models.ForeignKey(
verbose_name=_("created by"), to="authentication.User", on_delete=models.CASCADE
)
def __str__(self):
if self.message_type == MessageType.USER:
return self.message
return pgettext("log_item_messages", self.message)
class ProductLogItem(LogItem):
class Meta:
rules_permissions = {"view": always_allow, "add": always_allow}
ordering = ["-created_at"]
product = models.ForeignKey(
verbose_name=_("product"),
to="products.Product",
on_delete=models.CASCADE,
related_name="log_items",
related_query_name="log_item",
)
stage = models.ForeignKey(
to="products.ProductStage", on_delete=models.CASCADE, null=True, default=None
)
class ProjectLogItem(LogItem):
class Meta:
rules_permissions = {"view": always_allow, "add": always_allow}
ordering = ["-created_at"]
project = models.ForeignKey(
verbose_name=_("project"),
to="projects.Project",
on_delete=models.CASCADE,
related_name="log_items",
related_query_name="log_item",
)
+84
View File
@@ -0,0 +1,84 @@
from rest_framework import serializers
from outbo.apps.activity_log.enums import MessageType
from outbo.apps.activity_log.models import ProductLogItem, ProjectLogItem
class BaseProductLogItemSerializer(serializers.ModelSerializer):
class Meta:
model = ProductLogItem
def get_message_type(self, instance):
t, val = str(instance.message_type).split(".", 1)
return MessageType[val].value
class ProductLogItemSerializer:
class List(BaseProductLogItemSerializer):
message_type = serializers.SerializerMethodField()
class Meta(BaseProductLogItemSerializer.Meta):
fields = [
"message",
"message_type",
"stage",
"updated_at",
"created_at",
"created_by",
]
class Create(BaseProductLogItemSerializer):
class Meta(BaseProductLogItemSerializer.Meta):
fields = ["message"]
class AfterCreate(BaseProductLogItemSerializer):
message_type = serializers.SerializerMethodField()
class Meta(BaseProductLogItemSerializer.Meta):
fields = [
"message",
"message_type",
"stage",
"updated_at",
"created_at",
"created_by",
]
class BaseProjectLogItemSerializer(serializers.ModelSerializer):
class Meta:
model = ProjectLogItem
def get_message_type(self, instance):
t, val = str(instance.message_type).split(".", 1)
return MessageType[val].value
class ProjectLogItemSerializer:
class List(BaseProjectLogItemSerializer):
message_type = serializers.SerializerMethodField()
class Meta(BaseProjectLogItemSerializer.Meta):
fields = [
"message",
"message_type",
"updated_at",
"created_at",
"created_by",
]
class Create(BaseProjectLogItemSerializer):
class Meta(BaseProjectLogItemSerializer.Meta):
fields = ["message"]
class AfterCreate(BaseProjectLogItemSerializer):
message_type = serializers.SerializerMethodField()
class Meta(BaseProjectLogItemSerializer.Meta):
fields = [
"message",
"message_type",
"updated_at",
"created_at",
"created_by",
]
+2
View File
@@ -0,0 +1,2 @@
app_name = "activity_log"
urlpatterns = []
+48
View File
@@ -0,0 +1,48 @@
from outbo.apps.activity_log.models import ProductLogItem, ProjectLogItem
from outbo.apps.activity_log.serializers import (
ProductLogItemSerializer,
ProjectLogItemSerializer,
)
from outbo.apps.core.viewsets import (
AutoPermissionViewSetMixin,
ListModelMixin,
MultipleSerializerViewSet,
NestedViewSetMixin,
CreateModelMixin,
)
from outbo.apps.products.models import Product
from outbo.apps.projects.models import Project
class ProductLogItemNestedViewSet(
AutoPermissionViewSetMixin,
ListModelMixin,
CreateModelMixin,
NestedViewSetMixin,
MultipleSerializerViewSet,
):
queryset = ProductLogItem.objects.all()
serializer_class_set = ProductLogItemSerializer
def perform_create(self, *, serializer):
current_user = self.request.user
parents = self.get_parents_query_dict()
product = Product.objects.get(pk=parents["product_id"])
return serializer.save(product=product, created_by=current_user)
class ProjectLogItemNestedViewSet(
AutoPermissionViewSetMixin,
ListModelMixin,
CreateModelMixin,
NestedViewSetMixin,
MultipleSerializerViewSet,
):
queryset = ProjectLogItem.objects.all()
serializer_class_set = ProjectLogItemSerializer
def perform_create(self, *, serializer):
current_user = self.request.user
parents = self.get_parents_query_dict()
project = Project.objects.get(pk=parents["project_id"])
return serializer.save(project=project, created_by=current_user)
+16
View File
@@ -0,0 +1,16 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
from rest_framework.authentication import TokenAuthentication as BaseTokenAuthentication
class TokenAuthentication(BaseTokenAuthentication):
keyword = "Bearer"
class AuthenticationAppConfig(AppConfig):
name = "outbo.apps.authentication"
label = "authentication"
verbose_name = _("Authentication")
default_app_config = "outbo.apps.authentication.AuthenticationAppConfig"
+35
View File
@@ -0,0 +1,35 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import gettext_lazy as _
from outbo.apps.authentication.models import User
@admin.register(User)
class UserAdmin(BaseUserAdmin):
fieldsets = (
(None, {"fields": ("username", "password")}),
(
_("Personal info"),
{"fields": ("first_name", "last_name", "email", "photo", "phone_number")},
),
(_("Organization info"), {"fields": ("organization", "job_title", "is_observer", "is_trader", "category")}),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": ("username", "email", "password1", "password2", "is_observer", "is_trader", "category"),
},
),
)
list_display = ("username", "email", "first_name", "last_name")
list_filter = ()
search_fields = ("username", "first_name", "last_name", "email")
ordering = ("username",)
filter_horizontal = ()
autocomplete_fields = ["organization"]
def get_queryset(self, request):
return super().get_queryset(request).filter(is_superuser=False)
+41
View File
@@ -0,0 +1,41 @@
from django.contrib.auth.base_user import BaseUserManager
from outbo.apps.core.models import SoftDeletableManager
class UserManager(SoftDeletableManager, BaseUserManager):
def __init__(self, *, alive_only=False):
self.alive_only = alive_only
super().__init__()
def create_user(self, username, email, password=None, save=True, **kwargs):
if username is None:
raise TypeError("User must have a username.")
if email is None:
raise TypeError("User must have an email address.")
user = self.model(
username=username, email=self.normalize_email(email), **kwargs
)
user.set_password(password)
if save:
user.save()
return user
def create_superuser(self, username, email, password, save=True):
if password is None:
raise TypeError("Superuser must have a password.")
user = self.create_user(
username=username, email=email, password=password, save=False
)
user.is_superuser = True
user.is_staff = True
if save:
user.save()
return user
def regular_users(self):
return self.filter(is_staff=False)
@@ -0,0 +1,131 @@
# Generated by Django 2.2.7 on 2019-11-29 08:57
import django.contrib.auth.validators
from django.db import migrations, models
import outbo.apps.core.fields
import rules.contrib.models
class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0011_update_proxy_permissions"),
]
operations = [
migrations.CreateModel(
name="User",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"created_at",
outbo.apps.core.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="Created at"
),
),
(
"updated_at",
outbo.apps.core.fields.ModificationDateTimeField(
auto_now=True, verbose_name="Updated at"
),
),
(
"deleted_at",
outbo.apps.core.fields.RemovalDateTimeField(
blank=True, editable=False, null=True, verbose_name="Deleted at"
),
),
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username",
),
),
(
"email",
models.EmailField(
blank=True,
max_length=254,
unique=True,
verbose_name="email address",
),
),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Designates whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"first_name",
models.CharField(
blank=True, max_length=30, verbose_name="first name"
),
),
(
"last_name",
models.CharField(
blank=True, max_length=150, verbose_name="last name"
),
),
(
"photo",
models.ImageField(
blank=True,
null=True,
upload_to="users/",
verbose_name="profile photo",
),
),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.Group",
verbose_name="groups",
),
),
],
options={"verbose_name": "user", "verbose_name_plural": "users",},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
]
@@ -0,0 +1,41 @@
# Generated by Django 2.2.7 on 2019-11-29 08:57
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentication", "0001_initial"),
("organizations", "0001_initial"),
("auth", "0011_update_proxy_permissions"),
]
operations = [
migrations.AddField(
model_name="user",
name="organization",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="organizations.Organization",
verbose_name="organization",
),
),
migrations.AddField(
model_name="user",
name="user_permissions",
field=models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.Permission",
verbose_name="user permissions",
),
),
]
@@ -0,0 +1,25 @@
# Generated by Django 2.2.8 on 2020-01-16 12:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentication", "0002_auto_20191129_0957"),
]
operations = [
migrations.AddField(
model_name="user",
name="job_title",
field=models.CharField(blank=True, max_length=30, verbose_name="job title"),
),
migrations.AddField(
model_name="user",
name="phone_number",
field=models.CharField(
blank=True, max_length=30, verbose_name="phone_number"
),
),
]
@@ -0,0 +1,24 @@
# Generated by Django 2.2.8 on 2020-02-11 13:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentication", "0003_auto_20200116_1318"),
]
operations = [
migrations.AlterField(
model_name="user",
name="email",
field=models.EmailField(
blank=True,
max_length=254,
null=True,
unique=True,
verbose_name="email address",
),
),
]
@@ -0,0 +1,25 @@
# Generated by Django 2.2.10 on 2020-05-25 08:25
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("authentication", "0004_auto_20200211_1405"),
]
operations = [
migrations.AlterField(
model_name="user",
name="organization",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="organizations.Organization",
verbose_name="organization",
),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 2.2.16 on 2020-11-03 13:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0005_auto_20200525_1025'),
]
operations = [
migrations.AddField(
model_name='user',
name='is_observer',
field=models.BooleanField(default=False, help_text='Designates whether the user is an observer.', verbose_name='observer status'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 2.2.22 on 2021-05-11 07:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0006_user_is_observer'),
]
operations = [
migrations.AddField(
model_name='user',
name='is_trader',
field=models.BooleanField(default=False, help_text='Designates whether the user is an trader.', verbose_name='trader status'),
),
]
@@ -0,0 +1,20 @@
# Generated by Django 2.2.22 on 2021-05-11 09:28
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('organizations', '0002_auto_20200525_1025'),
('authentication', '0007_user_is_trader'),
]
operations = [
migrations.AddField(
model_name='user',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='organizations.Category', verbose_name='category'),
),
]
@@ -0,0 +1,19 @@
# Generated by Django 2.2.22 on 2021-05-17 10:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('authentication', '0008_user_category'),
]
operations = [
migrations.AlterField(
model_name='user',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='organizations.Category', verbose_name='category for trader'),
),
]
Whitespace-only changes.
+160
View File
@@ -0,0 +1,160 @@
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.contrib.contenttypes.models import ContentType
from django.core.mail import send_mail
from django.db import models
from django.utils.translation import gettext_lazy as _
from rules.contrib.models import RulesModelMixin, RulesModelBase
from outbo.apps.authentication import rules
from outbo.apps.authentication.managers import UserManager
from outbo.apps.core.models import TimestampableDeletableMixin
class User(
RulesModelMixin,
TimestampableDeletableMixin,
AbstractBaseUser,
PermissionsMixin,
metaclass=RulesModelBase,
):
objects = UserManager()
all_objects = UserManager(alive_only=False)
class Meta:
verbose_name = _("user")
verbose_name_plural = _("users")
rules_permissions = {
"read": rules.can_read_user,
"read_current": rules.can_read_current_user,
}
username_validator = UnicodeUsernameValidator()
username = models.CharField(
verbose_name=_("username"),
max_length=150,
unique=True,
help_text=_(
"Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
),
validators=[username_validator],
error_messages={"unique": _("A user with that username already exists.")},
)
email = models.EmailField(
verbose_name=_("email address"), blank=True, null=True, unique=True
)
is_staff = models.BooleanField(
verbose_name=_("staff status"),
default=False,
help_text=_("Designates whether the user can log into this admin site."),
)
is_observer = models.BooleanField(
verbose_name=_("observer status"),
default=False,
help_text=_("Designates whether the user is an observer."),
)
is_trader = models.BooleanField(
verbose_name=_("trader status"),
default=False,
help_text=_("Designates whether the user is an trader."),
)
first_name = models.CharField(
verbose_name=_("first name"), max_length=30, blank=True
)
last_name = models.CharField(
verbose_name=_("last name"), max_length=150, blank=True
)
organization = models.ForeignKey(
verbose_name=_("organization"),
to="organizations.Organization",
on_delete=models.CASCADE,
null=True,
default=None,
)
category = models.ForeignKey(
verbose_name=_("category for trader"),
to="organizations.Category",
on_delete=models.CASCADE,
blank=True,
null=True,
)
photo = models.ImageField(
verbose_name=_("profile photo"), upload_to="users/", blank=True, null=True
)
job_title = models.CharField(verbose_name=_("job title"), max_length=30, blank=True)
phone_number = models.CharField(
verbose_name=_("phone_number"), max_length=30, blank=True
)
USERNAME_FIELD = "username"
EMAIL_FIELD = "email"
REQUIRED_FIELDS = []
@property
def photo_url(self):
return None if not self.photo else self.photo.url
@property
def is_active(self):
return self.deleted_at is None
@property
def full_name(self):
return f"{self.first_name} {self.last_name}".strip()
@property
def is_agency(self):
return (
False
if not self.organization
else self.organization.type
== ContentType.objects.get(
app_label="organizations", model="agencyorganization"
)
)
@property
def is_client(self):
return (
False
if not self.organization
else self.organization.type
== ContentType.objects.get(
app_label="organizations", model="clientorganization"
)
)
@property
def is_supplier(self):
return (
False
if not self.organization
else self.organization.type
== ContentType.objects.get(
app_label="organizations", model="supplierorganization"
)
)
@property
def user_role(self):
from outbo.apps.projects.models.member import MembershipType
if self.is_client:
return MembershipType.CLIENT
elif self.is_agency:
return MembershipType.AGENCY
elif self.is_supplier:
return MembershipType.SUPPLIER
def email_user(self, subject, message, from_email=None, **kwargs):
"""Send an email to this user."""
send_mail(subject, message, from_email, [self.email], **kwargs)
+10
View File
@@ -0,0 +1,10 @@
from rules import predicate
@predicate
def _is_active(user):
return user.is_active
can_read_user = _is_active
can_read_current_user = _is_active
+74
View File
@@ -0,0 +1,74 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.authtoken.serializers import (
AuthTokenSerializer as BaseAuthTokenSerializer,
)
from outbo.apps.authentication.models import User
from outbo.apps.organizations.models import Organization
from outbo.apps.organizations.serializers import OrganizationSerializer
class AuthTokenSerializer(BaseAuthTokenSerializer):
def validate(self, attrs):
validated_attrs = super().validate(attrs)
user = validated_attrs["user"]
if user.is_staff:
msg = _("Administrators cannot login via API.")
raise serializers.ValidationError(msg, code="authorization")
return validated_attrs
class _BaseUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
class UserSerializer:
class List(_BaseUserSerializer):
organization_name = serializers.SlugRelatedField(
source="organization", slug_field="name", read_only=True
)
photo = serializers.SerializerMethodField()
class Meta(_BaseUserSerializer.Meta):
fields = [
"id",
"full_name",
"organization_name",
"photo",
"email",
"phone_number",
"job_title",
]
def get_photo(self, instance: User):
if instance.photo_url:
return self.context["request"].build_absolute_uri(instance.photo_url)
class Retrieve(_BaseUserSerializer):
organization = serializers.SerializerMethodField()
photo = serializers.SerializerMethodField()
class Meta(_BaseUserSerializer.Meta):
fields = [
"id",
"username",
"full_name",
"organization",
"photo",
"email",
"phone_number",
"job_title",
"is_observer",
"is_trader",
]
def get_photo(self, instance: User):
if instance.photo_url:
return self.context["request"].build_absolute_uri(instance.photo_url)
# NOTE: Hacky workaround
def get_organization(self, instance):
serializer = OrganizationSerializer.List(read_only=True)
serializer.instance = Organization.objects.get(id=instance.organization_id)
return serializer.data
+13
View File
@@ -0,0 +1,13 @@
from django.urls import path
from rest_framework import routers
from outbo.apps.authentication.views import ObtainAuthToken
from outbo.apps.authentication.viewsets import UserViewSet
app_name = "authentication"
router = routers.SimpleRouter()
router.register(prefix="users", viewset=UserViewSet)
view_urls = [path("token/", ObtainAuthToken.as_view(), name="token")]
urlpatterns = view_urls + router.urls
+19
View File
@@ -0,0 +1,19 @@
from djangorestframework_camel_case.parser import (
CamelCaseFormParser,
CamelCaseMultiPartParser,
CamelCaseJSONParser,
)
from djangorestframework_camel_case.render import CamelCaseJSONRenderer
from rest_framework.authtoken.views import ObtainAuthToken as BaseObtainAuthToken
from outbo.apps.authentication.serializers import AuthTokenSerializer
class ObtainAuthToken(BaseObtainAuthToken):
serializer_class = AuthTokenSerializer
renderer_classes = [CamelCaseJSONRenderer]
parser_classes = [
CamelCaseFormParser,
CamelCaseMultiPartParser,
CamelCaseJSONParser,
]
+32
View File
@@ -0,0 +1,32 @@
from rest_framework.decorators import action
from rest_framework.response import Response
from outbo.apps.authentication.models import User
from outbo.apps.authentication.serializers import UserSerializer
from outbo.apps.core.viewsets import (
MultipleSerializerViewSet,
AutoPermissionViewSetMixin,
ListModelMixin,
RetrieveModelMixin,
)
class UserViewSet(
RetrieveModelMixin,
ListModelMixin,
AutoPermissionViewSetMixin,
MultipleSerializerViewSet,
):
queryset = User.objects.regular_users()
serializer_class_set = UserSerializer
serializer_map = {**MultipleSerializerViewSet.serializer_map, "me": "Retrieve"}
permission_type_map = {
**AutoPermissionViewSetMixin.permission_type_map,
"me": "read_current",
}
@action(detail=False, methods=["get"])
def me(self, request):
current_user = request.user
serializer = self.get_serializer(current_user)
return Response(serializer.data)
View File
Whitespace-only changes.
+18
View File
@@ -0,0 +1,18 @@
from nested_admin.nested import NestedTabularInline
from polymorphic.admin import (
PolymorphicParentModelAdmin as BasePolymorphicParentModelAdmin,
)
class PolymorphicParentModelAdmin(BasePolymorphicParentModelAdmin):
def get_child_type_choices(self, request, action):
choices = super().get_child_type_choices(request=request, action=action)
return tuple((ct_id, label.title()) for (ct_id, label) in choices)
class NestedTabularSingleItemInline(NestedTabularInline):
template = "nesting/admin/inlines/tabular_single.html"
class NestedStackedSingleItemInline(NestedTabularInline):
template = "nesting/admin/inlines/stacked_single.html"
+122
View File
@@ -0,0 +1,122 @@
from functools import partial
from django import forms
from django.db import models
from django_extensions.db.fields import (
CreationDateTimeField as BaseCreationDateTimeField,
ModificationDateTimeField as BaseModificationDateTimeField,
)
from django_fsm import FSMFieldMixin
from outbo.apps.core.forms import EnumChoiceField
class CreationDateTimeField(BaseCreationDateTimeField):
"""
CreationDateTimeField
By default, sets editable=False, blank=True, auto_now_add=True
"""
class ModificationDateTimeField(BaseModificationDateTimeField):
"""
ModificationDateTimeField
By default, sets editable=False, blank=True, auto_now=True
Sets value to now every time the object is saved.
"""
class RemovalDateTimeField(models.DateTimeField):
"""
RemovalDateTimeField
By default, sets editable=False, blank=True, null=True
"""
def __init__(self, *args, **kwargs):
kwargs.setdefault("editable", False)
kwargs.setdefault("blank", True)
kwargs.setdefault("null", True)
models.DateTimeField.__init__(self, *args, **kwargs)
def get_internal_type(self):
return "DateTimeField"
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
if self.editable is not False:
kwargs["editable"] = True
if self.blank is not True:
kwargs["blank"] = False
if self.null is not True:
kwargs["null"] = False
return name, path, args, kwargs
class FSMKeyField(FSMFieldMixin, models.ForeignKey):
def get_state(self, instance):
return instance.__dict__[self.attname]
def set_state(self, instance, state):
instance.__dict__[self.attname] = self.to_python(state)
class EnumCharField(models.CharField):
def __init__(self, *args, **kwargs):
enum = kwargs.pop("enum", None)
if enum is not None:
if kwargs.get("choices", None) is not None:
raise ValueError("Cannot provide choices for EnumCharField")
kwargs["choices"] = [(item, item.value) for item in enum]
super().__init__(*args, **kwargs)
self.validators.pop() # lengthvalidator not necessary on this one
self.enum = enum
def get_internal_type(self):
return "CharField"
def formfield(self, **kwargs):
enum_form_class = partial(EnumChoiceField, enum=self.enum)
defaults = {
"widget": forms.Select,
"form_class": enum_form_class,
"choices": [(str(item), item.value) for item in self.enum],
}
defaults.update(kwargs)
return super().formfield(**defaults)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
kwargs["enum"] = self.enum
if "choices" in kwargs:
del kwargs["choices"]
return name, path, args, kwargs
def to_python(self, value):
value = super().to_python(value)
if value is not None:
if not isinstance(value, self.enum):
enum_name, enum_value = value.split(".", 1)
if enum_name == self.enum.__name__:
return self.enum[enum_value]
return value
def get_prep_value(self, value):
value = super().get_prep_value(value)
if value is None:
return value
if isinstance(value, self.enum):
return str(value)
return value
def from_db_value(self, value, *args):
if value is not None:
enum_name, enum_value = value.split(".", 1)
if enum_name == self.enum.__name__:
return self.enum[enum_value]
return value
+29
View File
@@ -0,0 +1,29 @@
from django import forms
from django.core.exceptions import ValidationError
class EnumChoiceField(forms.ChoiceField):
def __init__(self, enum, **kwargs):
kwargs.setdefault("choices", [(item, item.value) for item in enum])
super().__init__(**kwargs)
self.enum = enum
def prepare_value(self, value):
if isinstance(value, self.enum):
return str(value)
return value
def to_python(self, value):
value = super().to_python(value)
if not isinstance(value, self.enum):
try:
enum_name, enum_value = value.split(".", 1)
if enum_name == self.enum.__name__:
value = self.enum[enum_value]
except ValueError:
raise ValidationError(self.error_messages["invalid"], code="invalid")
return value
def clean(self, value):
value = super().clean(value)
return self.enum(value)
View File
Whitespace-only changes.
Whitespace-only changes.
+236
View File
@@ -0,0 +1,236 @@
from typing import TYPE_CHECKING, Tuple, Iterable
from django.core.management.base import BaseCommand
from faker import Factory
from tests.factories.authentication import (
UserFactory,
AgencyUserFactory,
ClientUserFactory,
SupplierUserFactory,
)
from tests.factories.organizations import (
CategoryFactory,
BrandFactory,
)
from tests.factories.products import ProductStageFactory, PackagingTypeFactory
from tests.factories.token import TokenFactory
faker = Factory()
if TYPE_CHECKING:
from outbo.apps.organizations.models import ClientOrganization
class Command(BaseCommand):
help = "Seed with some existing data"
def add_arguments(self, parser):
parser.add_argument("--dev", action="store_true", help="Seed with dev data")
def __make_users_and_organizations(self, *, password=None):
self.stdout.write("Seeding organizations and users...")
if password is None:
password = "password"
UserFactory(
username="admin",
password=password,
email="admin@example.com",
is_superuser=True,
is_staff=True,
)
agency = AgencyUserFactory(
username="agency",
password=password,
email="agency@example.com",
organization__name="Sample agency",
)
client = ClientUserFactory(
username="client",
password=password,
email="client@example.com",
organization__name="Sample client",
)
supplier = SupplierUserFactory(
username="supplier",
password=password,
email="supplier@example.com",
organization__name="Sample supplier",
)
TokenFactory(user=agency, key="6f9afaa36ee8b224181dc14e2ca9af33e97c0f71")
TokenFactory(user=client, key="481372f60325289029679df25c72e6ccd05892ed")
TokenFactory(user=supplier, key="d23d8f187614c8a76fe7316470e6f50ff5bc4188")
def __make_product_stages(self):
self.stdout.write("Seeding product stages...")
ProductStageFactory(
id="waiting_for_brief",
label="1",
group=1,
description="Briefing",
next_action="Oczekuje na uzupełnienie briefu",
order=1,
)
ProductStageFactory(
id="waiting_for_brief_accept_by_agency",
label="1A",
group=1,
description="Akceptacja agencji",
next_action="Oczekuje na akceptację agencji",
order=2,
)
ProductStageFactory(
id="waiting_for_brief_accept_by_client",
label="1B",
group=1,
description="Akceptacja klienta",
next_action="Oczekuje na akceptację klienta",
order=3,
)
ProductStageFactory(
id="waiting_for_draft",
label="2A",
group=2,
description="Publikacja draftu",
next_action="Oczekuje na publikację draftu",
order=4,
)
ProductStageFactory(
id="waiting_for_draft_accept_by_supplier",
label="2B",
group=2,
description="Akceptacja dostawcy",
next_action="Oczekuje na akceptację dostawcy",
order=5,
)
ProductStageFactory(
id="waiting_for_draft_accept_by_client",
label="2C",
group=2,
description="Akceptacja klienta",
next_action="Oczekuje na akceptację klientget_random_stringa",
order=6,
)
ProductStageFactory(
id="waiting_for_ff",
label="3A",
group=3,
description="Publikacja FF",
next_action="Oczekuje na publikację draftu",
order=7,
)
ProductStageFactory(
id="waiting_for_ff_accept_by_supplier",
label="3B",
group=3,
description="Akceptacja dostawcy",
next_action="Oczekuje na akceptację dostawcy",
order=8,
)
ProductStageFactory(
id="waiting_for_ff_accept_by_client",
label="3C",
group=3,
description="Akceptacja klienta",
next_action="Oczekuje na akceptację klienta",
order=9,
)
ProductStageFactory(
id="waiting_for_pf",
label="4A",
group=4,
description="Publikacja printing files",
next_action="Oczekuje na publikację printing files",
order=10,
)
ProductStageFactory(
id="waiting_for_pf_accept_by_supplier",
label="4B",
group=4,
description="Akceptacja dostawcy",
next_action="Oczekuje na akceptację dostawcy",
order=11,
)
ProductStageFactory(
id="waiting_for_pf_accept_by_client",
label="4C",
group=4,
description="Akceptacja klienta",
next_action="Oczekuje na akceptację klienta",
order=12,
)
ProductStageFactory(
id="completed",
label="5",
group=5,
description="Zako\u0144czono",
next_action="Oczekuje na archiwizację",
order=13,
)
def __make_packaging_types(self, *, items: Iterable[str] = None):
self.stdout.write("Seeding packaging types...")
if items:
for name in items:
PackagingTypeFactory(name=name)
else:
PackagingTypeFactory.create_batch(size=20)
def __make_brands(
self, *, items: Iterable[Tuple[str, "ClientOrganization"]] = None
):
self.stdout.write("Seeding brands...")
if items:
for name, client in items:
BrandFactory(name=name, client=client)
else:
BrandFactory.create_batch(size=20)
def __make_categories(self, *, items: Iterable[str] = None):
self.stdout.write("Seeding categories...")
if items:
for item in items:
CategoryFactory(name=item)
else:
CategoryFactory.create_batch(size=20)
def handle(self, *args, **options):
self.stdout.write("Seeding the database...")
if not options["dev"]:
packaging_types = (
"Carton/gift box",
"Header",
"Hangtag",
"Paper band",
"Paper board",
"Blister",
"Image insert",
"Simple band",
"Label/sticker",
"Simple CDU/Displaybox",
)
raise NotImplementedError()
password = "!secret123"
self.__make_users_and_organizations(password=password)
self.__make_product_stages()
self.__make_packaging_types()
self.__make_brands()
self.__make_categories()
self.stdout.write("Seeding completed.")
self.stdout.write("Credentials:")
self.stdout.write(f" admin: {password}")
self.stdout.write(f" agency: {password}")
self.stdout.write(f" client: {password}")
self.stdout.write(f" supplier: {password}")
+42
View File
@@ -0,0 +1,42 @@
# Generated by Django 2.2.16 on 2021-01-21 14:43
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="EmailLog",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("sender", models.CharField(max_length=254)),
("receiver", models.CharField(max_length=254)),
("time", models.DateTimeField()),
(
"status",
models.CharField(
choices=[
("sending", "sending"),
("sent", "sent"),
("queued", "queued"),
("invalid", "invalid"),
("rejected", "rejected"),
("failed", "failed"),
("unknown", "unknown"),
],
max_length=20,
),
),
],
),
]
View File
Whitespace-only changes.
+117
View File
@@ -0,0 +1,117 @@
from datetime import datetime
from django.conf import settings
from django.db import models
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from model_utils import Choices
from polymorphic.base import PolymorphicModelBase
from polymorphic.managers import PolymorphicManager
from polymorphic.models import PolymorphicModel
from polymorphic.query import PolymorphicQuerySet
from rules.contrib.models import RulesModelBaseMixin, RulesModelMixin
from outbo.apps.core.fields import (
RemovalDateTimeField,
CreationDateTimeField,
ModificationDateTimeField,
)
from outbo.apps.core.utils import get_now
EmailLogStatus = Choices("sending", "sent", "queued", "invalid", "rejected", "failed", "unknown")
class EmailLog(models.Model):
sender = models.CharField(max_length=254, blank=False, null=False)
receiver = models.CharField(max_length=254, blank=False, null=False)
time = models.DateTimeField(blank=False, null=False)
status = models.CharField(max_length=20, blank=False, null=False, choices=EmailLogStatus)
if "anymail" in settings.INSTALLED_APPS:
from anymail.signals import pre_send, post_send
from anymail.message import AnymailStatus
from django.core.mail import EmailMessage
def log_email_message(sender: str, receiver: str, time: datetime, status: str):
try:
EmailLog.objects.create(sender=sender, receiver=receiver, time=time, status=status)
except:
pass
@receiver(pre_send)
def handle_pre_send(sender, message: EmailMessage, **kwargs):
log_email_message(
sender=str(message.from_email),
receiver=", ".join(message.to),
time=get_now(),
status=EmailLogStatus.sending,
)
@receiver(post_send)
def handle_post_send(sender, message: EmailMessage, status: AnymailStatus, esp_name, **kwargs):
for email, recipient_status in status.recipients.items():
log_email_message(
sender=str(message.from_email),
receiver=email,
time=get_now(),
status=recipient_status.status or "failed",
)
class SoftDeletableQuerySet(models.QuerySet):
def delete(self):
return super().update(deleted_at=get_now())
def hard_delete(self):
return super().delete()
class SoftDeletableManager(models.Manager):
queryset_class = SoftDeletableQuerySet
def __init__(self, *args, **kwargs):
self.alive_only = kwargs.pop("alive_only", True)
super().__init__(*args, **kwargs)
def get_queryset(self):
if self.alive_only:
return self.queryset_class(self.model).filter(deleted_at=None)
return self.queryset_class(self.model)
class TimestampableDeletableMixin(models.Model):
objects = SoftDeletableManager()
all_objects = SoftDeletableManager(alive_only=False)
created_at = CreationDateTimeField(_("Created at"))
updated_at = ModificationDateTimeField(_("Updated at"))
deleted_at = RemovalDateTimeField(_("Deleted at"))
class Meta:
abstract = True
def delete(self, using=None, **kwargs):
self.deleted_at = get_now()
# TODO: handle autodeletion of nested stuff
self.save(using=using)
def __str__(self):
return super().__str__() if self.deleted_at is None else f"{super().__str__()} (deleted)"
class RulesPolymorphicModelBase(RulesModelBaseMixin, PolymorphicModelBase):
pass
class RulesPolymorphicModel(RulesModelMixin, PolymorphicModel, metaclass=RulesPolymorphicModelBase):
class Meta:
abstract = True
class SoftDeletablePolymorphicQuerySet(SoftDeletableQuerySet, PolymorphicQuerySet):
pass
class SoftDeletablePolymorphicManager(SoftDeletableManager, PolymorphicManager):
queryset_class = SoftDeletablePolymorphicQuerySet
+26
View File
@@ -0,0 +1,26 @@
from djangorestframework_camel_case.parser import CamelCaseMultiPartParser
from formencode.variabledecode import variable_decode
from rest_framework import parsers
from rest_framework.parsers import MultiPartParser
class NestedMultiPartParser(MultiPartParser):
def parse(self, stream, media_type=None, parser_context=None):
result = super().parse(
stream=stream, media_type=media_type, parser_context=parser_context
)
data = variable_decode(result.data)
files = variable_decode(result.files)
return parsers.DataAndFiles(data, files)
class CamelCaseNestedMultiPartParser(CamelCaseMultiPartParser):
def parse(self, stream, media_type=None, parser_context=None):
result = super().parse(
stream=stream, media_type=media_type, parser_context=parser_context
)
data = variable_decode(result.data)
files = variable_decode(result.files)
return parsers.DataAndFiles(data, files)
+16
View File
@@ -0,0 +1,16 @@
from rules import predicate
@predicate
def is_agency(user):
return user.is_authenticated and user.is_agency
@predicate
def is_client(user):
return user.is_authenticated and user.is_client
@predicate
def is_supplier(user):
return user.is_authenticated and user.is_supplier
+39
View File
@@ -0,0 +1,39 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_polymorphic.serializers import (
PolymorphicSerializer as BasePolymorphicSerializer,
)
class PolymorphicSerializer(BasePolymorphicSerializer):
resource_type_field_name = "type"
def to_resource_type(self, model_or_instance):
return model_or_instance._meta.verbose_name
class EnumCharField(serializers.ChoiceField):
default_error_messages = {"invalid_choice": _('"{input}" is not a valid choice.')}
def __init__(self, enum, **kwargs):
self.enum = enum
kwargs.setdefault("choices", [(item, item.value) for item in enum])
super().__init__(**kwargs)
self.enum = enum
def to_internal_value(self, value):
value = super().to_representation(value)
if not isinstance(value, self.enum):
try:
enum_name, enum_value = value.split(".", 1)
if enum_name == self.enum.__name__:
value = self.enum[enum_value]
except ValueError:
raise ValidationError(self.error_messages["invalid"], code="invalid")
return value
def to_representation(self, value):
if isinstance(value, self.enum):
return str(value)
return value
+13
View File
@@ -0,0 +1,13 @@
from storages.backends.s3boto3 import S3Boto3Storage
from storages.utils import setting
class S3StaticStorage(S3Boto3Storage):
bucket_name = setting("AWS_STORAGE_BUCKET_STATIC_NAME")
default_acl = "public-read"
querystring_auth = False
class S3MediaStorage(S3Boto3Storage):
bucket_name = setting("AWS_STORAGE_BUCKET_MEDIA_NAME")
default_acl = None
+9
View File
@@ -0,0 +1,9 @@
from django.urls import path
from outbo.apps.core.views import RootApiView
from outbo.apps.products.views import ExportBriefToPDFApiView
urlpatterns = [
path("", RootApiView.as_view(), name="root"),
path("briefs/<int:pk>/export-to-pdf/", ExportBriefToPDFApiView.as_view(), name="export_brief_to_pdf"),
]
+13
View File
@@ -0,0 +1,13 @@
from django.utils import timezone
def get_now():
return timezone.now()
def get_current_date():
return timezone.localtime(get_now()).date()
def is_empty_set(tested_set):
return tested_set == set()
+10
View File
@@ -0,0 +1,10 @@
from django.conf import settings
from rest_framework import permissions, views, response
class RootApiView(views.APIView):
permission_classes = [permissions.AllowAny]
def get(self, request):
print(request)
return response.Response({"version": settings.APP_VERSION})
+160
View File
@@ -0,0 +1,160 @@
import six
from django.db import transaction
from django.db.models import Model
from rest_framework import status, mixins
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.viewsets import GenericViewSet
from rest_framework_extensions.mixins import (
NestedViewSetMixin as BaseNestedViewSetMixin,
)
from rest_framework_extensions.settings import extensions_api_settings
from rules.contrib.rest_framework import (
AutoPermissionViewSetMixin as BaseAutoPermissionViewSetMixin,
)
class AutoPermissionViewSetMixin(BaseAutoPermissionViewSetMixin):
permission_type_map = {
**BaseAutoPermissionViewSetMixin.permission_type_map,
"metadata": None,
}
class CreateModelMixin:
def create(self, request, *args, **kwargs):
if isinstance(request.data, list):
with transaction.atomic():
response = []
for item in request.data:
write_serializer = self.get_serializer(data=item)
write_serializer.is_valid(raise_exception=True)
instance = self.perform_create(serializer=write_serializer)
read_serializer = self.get_read_serializer(instance=instance)
response.append(read_serializer.data)
return Response(response, status=status.HTTP_201_CREATED)
else:
write_serializer = self.get_serializer(data=request.data)
write_serializer.is_valid(raise_exception=True)
instance = self.perform_create(serializer=write_serializer)
read_serializer = self.get_read_serializer(instance=instance)
headers = self.get_success_headers(read_serializer.data)
return Response(
read_serializer.data, status=status.HTTP_201_CREATED, headers=headers
)
def perform_create(self, *, serializer):
return serializer.save()
def get_success_headers(self, data):
try:
return {"Location": str(data[api_settings.URL_FIELD_NAME])}
except (TypeError, KeyError):
return {}
class UpdateModelMixin:
def update(self, request, *args, **kwargs):
instance = self.get_object()
write_serializer = self.get_serializer(
instance, data=request.data, partial=False
)
write_serializer.is_valid(raise_exception=True)
instance = write_serializer.save()
if getattr(instance, "_prefetched_objects_cache", None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
read_serializer = self.get_read_serializer(instance=instance)
return Response(read_serializer.data)
class PartialUpdateModelMixin:
def partial_update(self, request, *args, **kwargs):
instance = self.get_object()
write_serializer = self.get_serializer(
instance, data=request.data, partial=True
)
write_serializer.is_valid(raise_exception=True)
instance = write_serializer.save()
if getattr(instance, "_prefetched_objects_cache", None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
read_serializer = self.get_read_serializer(instance=instance)
return Response(read_serializer.data)
class RetrieveModelMixin(mixins.RetrieveModelMixin):
pass
class ListModelMixin(mixins.ListModelMixin):
pass
class DestroyModelMixin(mixins.DestroyModelMixin):
pass
class MultipleSerializerViewSet(GenericViewSet):
serializer_class_set = None
serializer_map = {
"list": "List",
"retrieve": "Retrieve",
"create": "Create",
"partial_update": "Update",
}
def get_serializer_class(self):
assert (
self.serializer_class_set is not None
), "serializer_class_set should be defined"
if self.action not in self.serializer_map:
raise KeyError(
f"""serializer_map does not contain serializer for "{self.action}" method"""
)
return getattr(self.serializer_class_set, self.serializer_map[self.action])
def get_read_serializer(self, *args, **kwargs):
assert (
self.serializer_class_set is not None
), "serializer_class_set should be defined"
if self.action != "create":
if getattr(self.serializer_class_set, "Retrieve", None) is None:
raise KeyError(
f"""serializer_class_set does not contain "Retrieve" serializer"""
)
serializer_class = self.serializer_class_set.Retrieve
else:
if (
getattr(self.serializer_class_set, "Retrieve", None) is None
and getattr(self.serializer_class_set, "AfterCreate", None) is None
):
raise KeyError(
f"""serializer_class_set does not contain "Retrieve" nor "AfterCreate" serializer and is used in "create" action."""
)
if getattr(self.serializer_class_set, "AfterCreate", None) is not None:
serializer_class = self.serializer_class_set.AfterCreate
else:
serializer_class = self.serializer_class_set.Retrieve
kwargs["context"] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
class NestedViewSetMixin(BaseNestedViewSetMixin):
pass
+11
View File
@@ -0,0 +1,11 @@
from django.utils.translation import gettext_lazy as _
from django.apps import AppConfig
class FinalPhotobriefingAppConfig(AppConfig):
name = "outbo.apps.final_photobriefing"
label = "final_photobriefing"
verbose_name = _("Final photo briefing")
default_app_config = "outbo.apps.final_photobriefing.FinalPhotobriefingAppConfig"
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+9
View File
@@ -0,0 +1,9 @@
from enum import Enum
class PhotoType(Enum):
INVALID = "<invalid>"
ARRANGEMENT_PHOTOS = "arrangement"
CLIPPING = "clipping"
PACKSHOT = "packshot"
EXTRA_PHOTO = "extraPhoto"
@@ -0,0 +1,49 @@
# Generated by Django 2.2.8 on 2019-12-18 13:54
import rules.contrib.models
from django.db import migrations, models
import outbo.apps.core.fields
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="FinalBrief",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
outbo.apps.core.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="Created at"
),
),
(
"updated_at",
outbo.apps.core.fields.ModificationDateTimeField(
auto_now=True, verbose_name="Updated at"
),
),
(
"deleted_at",
outbo.apps.core.fields.RemovalDateTimeField(
blank=True, editable=False, null=True, verbose_name="Deleted at"
),
),
],
options={"abstract": False,},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
]
@@ -0,0 +1,14 @@
# Generated by Django 2.2.8 on 2019-12-18 14:03
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("final_photobriefing", "0001_initial"),
]
operations = [
migrations.RemoveField(model_name="finalbrief", name="id",),
]
@@ -0,0 +1,27 @@
# Generated by Django 2.2.8 on 2019-12-18 14:03
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("photobriefing", "0001_initial"),
("final_photobriefing", "0002_remove_finalbrief_id"),
]
operations = [
migrations.AddField(
model_name="finalbrief",
name="brief",
field=models.ForeignKey(
default=0,
on_delete=django.db.models.deletion.PROTECT,
primary_key=True,
serialize=False,
to="photobriefing.Brief",
),
preserve_default=False,
),
]
@@ -0,0 +1,24 @@
# Generated by Django 2.2.8 on 2019-12-18 14:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("final_photobriefing", "0003_finalbrief_brief"),
]
operations = [
migrations.AlterField(
model_name="finalbrief",
name="brief",
field=models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
primary_key=True,
serialize=False,
to="photobriefing.Brief",
),
),
]
@@ -0,0 +1,26 @@
# Generated by Django 2.2.8 on 2019-12-18 14:06
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("final_photobriefing", "0004_auto_20191218_1505"),
]
operations = [
migrations.AlterField(
model_name="finalbrief",
name="brief",
field=models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
primary_key=True,
related_name="final",
related_query_name="final",
serialize=False,
to="photobriefing.Brief",
),
),
]
@@ -0,0 +1,102 @@
# Generated by Django 2.2.8 on 2019-12-19 08:48
from django.db import migrations, models
import django.db.models.deletion
import outbo.apps.core.fields
import outbo.apps.final_photobriefing.enums
import outbo.apps.final_photobriefing.models
import rules.contrib.models
class Migration(migrations.Migration):
dependencies = [
("final_photobriefing", "0005_auto_20191218_1506"),
]
operations = [
migrations.CreateModel(
name="FinalBriefPhoto",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
outbo.apps.core.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="Created at"
),
),
(
"updated_at",
outbo.apps.core.fields.ModificationDateTimeField(
auto_now=True, verbose_name="Updated at"
),
),
(
"deleted_at",
outbo.apps.core.fields.RemovalDateTimeField(
blank=True, editable=False, null=True, verbose_name="Deleted at"
),
),
(
"file",
models.FileField(
upload_to=outbo.apps.final_photobriefing.models.FormatFinalPhotoBriefPhotoFilename()
),
),
(
"category",
models.CharField(
choices=[
(
outbo.apps.final_photobriefing.enums.PhotoType(
"<invalid>"
),
"<invalid>",
),
(
outbo.apps.final_photobriefing.enums.PhotoType(
"arrangement"
),
"arrangement",
),
(
outbo.apps.final_photobriefing.enums.PhotoType(
"clipping"
),
"clipping",
),
(
outbo.apps.final_photobriefing.enums.PhotoType(
"packshot"
),
"packshot",
),
(
outbo.apps.final_photobriefing.enums.PhotoType(
"extraPhoto"
),
"extraPhoto",
),
],
max_length=30,
),
),
(
"brief",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="final_photobriefing.FinalBrief",
),
),
],
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
]
@@ -0,0 +1,56 @@
# Generated by Django 2.2.8 on 2019-12-20 10:14
from django.db import migrations, models
import django.db.models.deletion
import outbo.apps.final_photobriefing.enums
class Migration(migrations.Migration):
dependencies = [
("final_photobriefing", "0006_finalbriefphoto"),
]
operations = [
migrations.RemoveField(model_name="finalbriefphoto", name="category",),
migrations.AddField(
model_name="finalbriefphoto",
name="type",
field=models.CharField(
choices=[
(
outbo.apps.final_photobriefing.enums.PhotoType("<invalid>"),
"<invalid>",
),
(
outbo.apps.final_photobriefing.enums.PhotoType("arrangement"),
"arrangement",
),
(
outbo.apps.final_photobriefing.enums.PhotoType("clipping"),
"clipping",
),
(
outbo.apps.final_photobriefing.enums.PhotoType("packshot"),
"packshot",
),
(
outbo.apps.final_photobriefing.enums.PhotoType("extraPhoto"),
"extraPhoto",
),
],
default=outbo.apps.final_photobriefing.enums.PhotoType("<invalid>"),
max_length=30,
),
),
migrations.AlterField(
model_name="finalbriefphoto",
name="brief",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="photos",
related_query_name="photo",
to="final_photobriefing.FinalBrief",
),
),
]
@@ -0,0 +1,26 @@
# Generated by Django 2.2.10 on 2020-05-25 08:25
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("final_photobriefing", "0007_auto_20191220_1114"),
]
operations = [
migrations.AlterField(
model_name="finalbrief",
name="brief",
field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name="final",
related_query_name="final",
serialize=False,
to="photobriefing.Brief",
),
),
]
Whitespace-only changes.
+63
View File
@@ -0,0 +1,63 @@
import os
from django.db import models
from django.utils.deconstruct import deconstructible
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
from outbo.apps.final_photobriefing import rules
from outbo.apps.final_photobriefing.enums import PhotoType
@deconstructible
class FormatFinalPhotoBriefPhotoFilename:
def __call__(self, instance, filename):
obj = instance.brief.brief
return "/".join(
("projects", obj.product.project.slug[1:], "final-photobriefing", filename)
)
class FinalBrief(TimestampableDeletableMixin, RulesModel):
class Meta:
rules_permissions = {
"add": rules.can_create_final_brief,
"view": rules.can_read_final_brief,
"change": rules.can_update_final_brief,
"delete": rules.can_delete_final_brief,
}
brief = models.OneToOneField(
to="photobriefing.Brief",
on_delete=models.CASCADE,
related_name="final",
related_query_name="final",
primary_key=True,
)
class FinalBriefPhoto(TimestampableDeletableMixin, RulesModel):
class Meta:
rules_permissions = {
"add": rules.can_create_final_brief_photo,
"view": rules.can_read_final_brief_photo,
"change": rules.can_update_final_brief_photo,
"delete": rules.can_delete_final_brief_photo,
}
brief = models.ForeignKey(
to=FinalBrief,
on_delete=models.CASCADE,
related_query_name="photo",
related_name="photos",
)
file = models.FileField(upload_to=FormatFinalPhotoBriefPhotoFilename())
type = models.CharField(
max_length=30,
choices=[(type, type.value) for type in PhotoType],
default=PhotoType.INVALID,
)
@property
def filename(self):
return os.path.basename(self.file.name)
+13
View File
@@ -0,0 +1,13 @@
from rules import always_deny, always_allow
from outbo.apps.core.rules import is_agency
can_create_final_brief = always_deny
can_read_final_brief = always_allow
can_update_final_brief = is_agency
can_delete_final_brief = always_deny
can_create_final_brief_photo = always_deny
can_read_final_brief_photo = always_allow
can_update_final_brief_photo = is_agency
can_delete_final_brief_photo = is_agency
+13
View File
@@ -0,0 +1,13 @@
from rest_framework_extensions.routers import ExtendedSimpleRouter
from outbo.apps.final_photobriefing.viewsets import (
FinalBriefViewSet,
FinalBriefPhotoViewSet,
)
app_name = "final_photobriefing"
router = ExtendedSimpleRouter()
router.register(prefix="final-photobriefing", viewset=FinalBriefViewSet)
router.register(prefix="final-photobriefing-photo", viewset=FinalBriefPhotoViewSet)
urlpatterns = router.urls
+99
View File
@@ -0,0 +1,99 @@
from django.http import HttpResponseRedirect
from rest_framework import serializers
from rest_framework.permissions import AllowAny
from outbo.apps.core.viewsets import (
AutoPermissionViewSetMixin,
RetrieveModelMixin,
MultipleSerializerViewSet,
PartialUpdateModelMixin,
DestroyModelMixin,
)
from outbo.apps.final_photobriefing.enums import PhotoType
from outbo.apps.final_photobriefing.models import FinalBrief, FinalBriefPhoto
class FinalBriefPhotoSerializer:
class List(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="final_photobriefing:finalbriefphoto-detail"
)
type = serializers.SerializerMethodField()
filename = serializers.ReadOnlyField()
class Meta:
model = FinalBriefPhoto
fields = ["url", "type", "filename"]
def get_type(self, instance):
t, val = instance.type.split(".", 1)
return PhotoType[val].value
class Create(serializers.ModelSerializer):
type = serializers.CharField()
class Meta:
model = FinalBriefPhoto
fields = ["type", "file"]
def validate_type(self, value):
try:
return PhotoType(value)
except ValueError:
raise serializers.ValidationError("Invalid photo type value")
class BaseFinalBriefSerializer(serializers.ModelSerializer):
class Meta:
model = FinalBrief
class FinalBriefSerializer:
class Retrieve(BaseFinalBriefSerializer):
photos = FinalBriefPhotoSerializer.List(many=True)
class Meta(BaseFinalBriefSerializer.Meta):
fields = ["photos", "created_at", "updated_at"]
class Update(BaseFinalBriefSerializer):
photos = FinalBriefPhotoSerializer.Create(many=True)
class Meta(BaseFinalBriefSerializer.Meta):
fields = ["photos"]
def update(self, instance, validated_data):
photos = validated_data.pop("photos", None)
if photos is not None:
for photo in photos:
FinalBriefPhoto.objects.create(brief=instance, **photo)
return instance
class FinalBriefViewSet(
AutoPermissionViewSetMixin,
RetrieveModelMixin,
PartialUpdateModelMixin,
MultipleSerializerViewSet,
):
queryset = FinalBrief.objects.all()
serializer_class_set = FinalBriefSerializer
class FinalBriefPhotoViewSet(
AutoPermissionViewSetMixin,
RetrieveModelMixin,
DestroyModelMixin,
MultipleSerializerViewSet,
):
queryset = FinalBriefPhoto.objects.all()
serializer_class_set = FinalBriefPhotoSerializer
retrieve_permission_classes = [AllowAny]
def get_permissions(self):
if self.action == "retrieve":
return [permission() for permission in self.retrieve_permission_classes]
return super().get_permissions()
def retrieve(self, request, *args, **kwargs):
photo = self.get_object()
return HttpResponseRedirect(redirect_to=photo.file.url)
+11
View File
@@ -0,0 +1,11 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class OrganizationsAppConfig(AppConfig):
name = "outbo.apps.organizations"
label = "organizations"
verbose_name = _("Organizations")
default_app_config = "outbo.apps.organizations.OrganizationsAppConfig"
+57
View File
@@ -0,0 +1,57 @@
from django.contrib import admin
from polymorphic.admin import (
PolymorphicChildModelAdmin,
PolymorphicChildModelFilter,
)
from outbo.apps.core.admin import PolymorphicParentModelAdmin
from outbo.apps.organizations.models import (
Organization,
AgencyOrganization,
ClientOrganization,
SupplierOrganization,
Brand,
Category,
)
class _OrganizationChildAdmin(PolymorphicChildModelAdmin):
base_model = Organization
@admin.register(AgencyOrganization)
class AgencyOrganizationAdmin(_OrganizationChildAdmin):
base_model = AgencyOrganization
@admin.register(ClientOrganization)
class ClientOrganizationAdmin(_OrganizationChildAdmin):
base_model = ClientOrganization
search_fields = ["name"]
@admin.register(SupplierOrganization)
class SupplierOrganizationAdmin(_OrganizationChildAdmin):
base_model = SupplierOrganization
search_fields = ["name"]
@admin.register(Organization)
class OrganizationAdmin(PolymorphicParentModelAdmin):
base_model = Organization
child_models = (AgencyOrganization, ClientOrganization, SupplierOrganization)
list_filter = (PolymorphicChildModelFilter,)
search_fields = ["name"]
@admin.register(Brand)
class BrandAdmin(admin.ModelAdmin):
autocomplete_fields = ["client", "suppliers", "categories"]
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
search_fields = ["name"]
# NOTE: https://stackoverflow.com/questions/9879687/adding-a-manytomanywidget-to-the-reverse-of-a-manytomanyfield-in-the-django-admi/13189954
@@ -0,0 +1,220 @@
# Generated by Django 2.2.7 on 2019-11-29 08:57
from django.db import migrations, models
import django.db.models.deletion
import outbo.apps.core.fields
import rules.contrib.models
class Migration(migrations.Migration):
initial = True
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
]
operations = [
migrations.CreateModel(
name="Category",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
outbo.apps.core.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="Created at"
),
),
(
"updated_at",
outbo.apps.core.fields.ModificationDateTimeField(
auto_now=True, verbose_name="Updated at"
),
),
(
"deleted_at",
outbo.apps.core.fields.RemovalDateTimeField(
blank=True, editable=False, null=True, verbose_name="Deleted at"
),
),
("name", models.CharField(max_length=254, verbose_name="name")),
],
options={
"verbose_name": "category",
"verbose_name_plural": "categories",
"ordering": ["name"],
},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.CreateModel(
name="Organization",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
outbo.apps.core.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="Created at"
),
),
(
"updated_at",
outbo.apps.core.fields.ModificationDateTimeField(
auto_now=True, verbose_name="Updated at"
),
),
(
"deleted_at",
outbo.apps.core.fields.RemovalDateTimeField(
blank=True, editable=False, null=True, verbose_name="Deleted at"
),
),
("name", models.CharField(max_length=100)),
(
"polymorphic_ctype",
models.ForeignKey(
editable=False,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="polymorphic_organizations.organization_set+",
to="contenttypes.ContentType",
),
),
],
options={"abstract": False,},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.CreateModel(
name="AgencyOrganization",
fields=[
(
"organization_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="organizations.Organization",
),
),
],
options={"verbose_name": "agency", "verbose_name_plural": "agencies",},
bases=("organizations.organization",),
),
migrations.CreateModel(
name="ClientOrganization",
fields=[
(
"organization_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="organizations.Organization",
),
),
("uses_simple_project_flow", models.BooleanField(default=False)),
],
options={"verbose_name": "client", "verbose_name_plural": "clients",},
bases=("organizations.organization",),
),
migrations.CreateModel(
name="SupplierOrganization",
fields=[
(
"organization_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="organizations.Organization",
),
),
],
options={"verbose_name": "supplier", "verbose_name_plural": "suppliers",},
bases=("organizations.organization",),
),
migrations.CreateModel(
name="Brand",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
outbo.apps.core.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="Created at"
),
),
(
"updated_at",
outbo.apps.core.fields.ModificationDateTimeField(
auto_now=True, verbose_name="Updated at"
),
),
(
"deleted_at",
outbo.apps.core.fields.RemovalDateTimeField(
blank=True, editable=False, null=True, verbose_name="Deleted at"
),
),
("name", models.CharField(max_length=50, verbose_name="name")),
(
"categories",
models.ManyToManyField(
related_name="brands",
related_query_name="brand",
to="organizations.Category",
verbose_name="categories",
),
),
(
"client",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="brands",
related_query_name="brand",
to="organizations.ClientOrganization",
verbose_name="client",
),
),
(
"suppliers",
models.ManyToManyField(
related_name="brands",
related_query_name="brand",
to="organizations.SupplierOrganization",
verbose_name="suppliers",
),
),
],
options={"verbose_name": "brand", "verbose_name_plural": "brands",},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
]
@@ -0,0 +1,25 @@
# Generated by Django 2.2.10 on 2020-05-25 08:25
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("organizations", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="brand",
name="client",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="brands",
related_query_name="brand",
to="organizations.ClientOrganization",
verbose_name="client",
),
),
]
Whitespace-only changes.
@@ -0,0 +1,8 @@
from .organization import (
Organization,
AgencyOrganization,
ClientOrganization,
SupplierOrganization,
)
from .brand import Brand
from .category import Category
+38
View File
@@ -0,0 +1,38 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
from outbo.apps.organizations import rules
class Brand(TimestampableDeletableMixin, RulesModel):
class Meta:
verbose_name = _("brand")
verbose_name_plural = _("brands")
rules_permissions = {"read": rules.can_read_brand}
name = models.CharField(verbose_name=_("name"), max_length=50)
client = models.ForeignKey(
verbose_name=_("client"),
to="ClientOrganization",
on_delete=models.CASCADE,
related_name="brands",
related_query_name="brand",
null=False,
)
suppliers = models.ManyToManyField(
verbose_name=_("suppliers"),
to="SupplierOrganization",
related_name="brands",
related_query_name="brand",
)
categories = models.ManyToManyField(
verbose_name=_("categories"),
to="Category",
related_name="brands",
related_query_name="brand",
)
def __str__(self):
return self.name
+17
View File
@@ -0,0 +1,17 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
class Category(TimestampableDeletableMixin, RulesModel):
class Meta:
verbose_name = _("category")
verbose_name_plural = _("categories")
ordering = ["name"]
name = models.CharField(verbose_name=_("name"), max_length=254)
def __str__(self):
return self.name
@@ -0,0 +1,50 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
from outbo.apps.core.models import (
RulesPolymorphicModel,
TimestampableDeletableMixin,
SoftDeletablePolymorphicManager,
)
class Organization(TimestampableDeletableMixin, RulesPolymorphicModel):
objects = SoftDeletablePolymorphicManager()
all_objects = SoftDeletablePolymorphicManager(alive_only=False)
name = models.CharField(max_length=100)
def __str__(self):
return f"[{str(self.polymorphic_ctype).title()}] {self.name}"
@property
def type(self):
return self.polymorphic_ctype
@type.setter
def type(self, value):
self.polymorphic_ctype = value
@type.deleter
def type(self):
del self.polymorphic_ctype
class AgencyOrganization(Organization):
class Meta:
verbose_name = _("agency")
verbose_name_plural = _("agencies")
class ClientOrganization(Organization):
class Meta:
verbose_name = _("client")
verbose_name_plural = _("clients")
uses_simple_project_flow = models.BooleanField(null=False, default=False)
class SupplierOrganization(Organization):
class Meta:
verbose_name = _("supplier")
verbose_name_plural = _("suppliers")
+11
View File
@@ -0,0 +1,11 @@
from rules import always_deny, predicate
@predicate
def can_read_brand(user, brand):
return always_deny
@predicate
def can_read_category(user, category):
return always_deny
+94
View File
@@ -0,0 +1,94 @@
from rest_framework import serializers
from outbo.apps.core.serializers import PolymorphicSerializer
from outbo.apps.organizations.models import (
AgencyOrganization,
ClientOrganization,
SupplierOrganization,
Brand,
Category,
Organization,
)
# Organizations
class _BaseOrganizationSerializer(serializers.ModelSerializer):
class Meta:
fields = (
"id",
"name",
)
class _OrganizationSerializer:
class List(_BaseOrganizationSerializer):
class Meta(_BaseOrganizationSerializer.Meta):
model = Organization
class _AgencyOrganizationSerializer:
class List(_BaseOrganizationSerializer):
class Meta(_BaseOrganizationSerializer.Meta):
model = AgencyOrganization
class _ClientOrganizationSerializer:
class List(_BaseOrganizationSerializer):
class Meta(_BaseOrganizationSerializer.Meta):
model = ClientOrganization
class _SupplierOrganizationSerializer:
class List(_BaseOrganizationSerializer):
class Meta(_BaseOrganizationSerializer.Meta):
model = SupplierOrganization
class _BaseOrganizationPolymorphicSerializer(PolymorphicSerializer):
model_serializer_mapping = {
AgencyOrganization: _AgencyOrganizationSerializer.List,
ClientOrganization: _ClientOrganizationSerializer.List,
SupplierOrganization: _SupplierOrganizationSerializer.List,
Organization: _OrganizationSerializer.List,
}
class OrganizationSerializer:
class Retrieve(_BaseOrganizationPolymorphicSerializer):
pass
class List(_BaseOrganizationPolymorphicSerializer):
pass
# Brands
class _BaseBrandSerializer(serializers.ModelSerializer):
class Meta:
model = Brand
fields = ["name"]
class BrandSerializer(serializers.ModelSerializer):
# Create & Update not needed
class Retrieve(_BaseBrandSerializer):
pass
class List(_BaseBrandSerializer):
class Meta(_BaseBrandSerializer.Meta):
fields = ["id"] + _BaseBrandSerializer.Meta.fields
# Categories
class _BaseCategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ["name"]
class CategorySerializer:
class Retrieve(_BaseCategorySerializer):
pass
class List(_BaseCategorySerializer):
class Meta(_BaseCategorySerializer.Meta):
fields = ["id"] + _BaseCategorySerializer.Meta.fields
+10
View File
@@ -0,0 +1,10 @@
from rest_framework import routers
from outbo.apps.organizations.viewsets import BrandViewSet, CategoryViewSet
app_name = "organizations"
router = routers.SimpleRouter()
router.register(prefix="brands", viewset=BrandViewSet)
router.register(prefix="categories", viewset=CategoryViewSet)
urlpatterns = router.urls
+29
View File
@@ -0,0 +1,29 @@
from outbo.apps.core.viewsets import (
MultipleSerializerViewSet,
AutoPermissionViewSetMixin,
ListModelMixin,
RetrieveModelMixin,
)
from outbo.apps.organizations.models import Brand, Category
from outbo.apps.organizations.serializers import BrandSerializer, CategorySerializer
class BrandViewSet(
RetrieveModelMixin,
ListModelMixin,
AutoPermissionViewSetMixin,
MultipleSerializerViewSet,
):
queryset = Brand.objects.all()
serializer_class_set = BrandSerializer
class CategoryViewSet(
RetrieveModelMixin,
ListModelMixin,
AutoPermissionViewSetMixin,
MultipleSerializerViewSet,
):
queryset = Category.objects.all()
serializer_class_set = CategorySerializer
+11
View File
@@ -0,0 +1,11 @@
from django.utils.translation import gettext_lazy as _
from django.apps import AppConfig
class PhotobriefingAppConfig(AppConfig):
name = "outbo.apps.photobriefing"
label = "photobriefing"
verbose_name = _("Photo briefing")
default_app_config = "outbo.apps.photobriefing.PhotobriefingAppConfig"
+21
View File
@@ -0,0 +1,21 @@
from django.contrib import admin
from nested_admin import nested
from outbo.apps.photobriefing.models import Brief, BriefItem, BriefItemPhoto, Category
class BriefItemPhotoInline(nested.NestedStackedInline):
model = BriefItemPhoto
class BriefItemInline(nested.NestedStackedInline):
model = BriefItem
inlines = [BriefItemPhotoInline]
@admin.register(Brief)
class BriefAdmin(nested.NestedModelAdmin):
inlines = [BriefItemInline]
admin.site.register(Category)
+7
View File
@@ -0,0 +1,7 @@
from enum import Enum
class ChoiceRole(Enum):
INVALID = "<invalid>"
SUPPLIER = "supplier"
CLIENT = "client"
@@ -0,0 +1,304 @@
# Generated by Django 2.2.7 on 2019-11-29 08:57
from django.db import migrations, models
import django.db.models.deletion
import django_fsm
import outbo.apps.core.fields
import outbo.apps.core.utils
import outbo.apps.photobriefing.enums
import outbo.apps.photobriefing.models.brief_item_photo
import rules.contrib.models
class Migration(migrations.Migration):
initial = True
dependencies = [
("products", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="Brief",
fields=[
(
"created_at",
outbo.apps.core.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="Created at"
),
),
(
"updated_at",
outbo.apps.core.fields.ModificationDateTimeField(
auto_now=True, verbose_name="Updated at"
),
),
(
"deleted_at",
outbo.apps.core.fields.RemovalDateTimeField(
blank=True, editable=False, null=True, verbose_name="Deleted at"
),
),
(
"product",
models.OneToOneField(
editable=False,
on_delete=django.db.models.deletion.PROTECT,
primary_key=True,
related_name="photo_brief",
related_query_name="photo_brief",
serialize=False,
to="products.Product",
verbose_name="product",
),
),
(
"session_date",
models.DateField(
default=outbo.apps.core.utils.get_current_date,
verbose_name="photo session date",
),
),
("concept_description", models.TextField(blank=True, default="")),
(
"stage",
django_fsm.FSMField(
default="created", max_length=50, protected=True
),
),
],
options={"abstract": False,},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.CreateModel(
name="BriefItem",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
outbo.apps.core.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="Created at"
),
),
(
"updated_at",
outbo.apps.core.fields.ModificationDateTimeField(
auto_now=True, verbose_name="Updated at"
),
),
(
"deleted_at",
outbo.apps.core.fields.RemovalDateTimeField(
blank=True, editable=False, null=True, verbose_name="Deleted at"
),
),
("title", models.CharField(max_length=100)),
("description", models.TextField()),
(
"brief",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="items",
related_query_name="item",
to="photobriefing.Brief",
verbose_name="brief",
),
),
],
options={"ordering": ["id"],},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.CreateModel(
name="Category",
fields=[
(
"created_at",
outbo.apps.core.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="Created at"
),
),
(
"updated_at",
outbo.apps.core.fields.ModificationDateTimeField(
auto_now=True, verbose_name="Updated at"
),
),
(
"deleted_at",
outbo.apps.core.fields.RemovalDateTimeField(
blank=True, editable=False, null=True, verbose_name="Deleted at"
),
),
(
"label",
models.CharField(
max_length=30,
primary_key=True,
serialize=False,
verbose_name="label",
),
),
(
"description",
models.CharField(max_length=30, verbose_name="description"),
),
],
options={"verbose_name": "category", "verbose_name_plural": "categories",},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.CreateModel(
name="BriefItemPhoto",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
outbo.apps.core.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="Created at"
),
),
(
"updated_at",
outbo.apps.core.fields.ModificationDateTimeField(
auto_now=True, verbose_name="Updated at"
),
),
(
"deleted_at",
outbo.apps.core.fields.RemovalDateTimeField(
blank=True, editable=False, null=True, verbose_name="Deleted at"
),
),
(
"file",
models.ImageField(
upload_to=outbo.apps.photobriefing.models.brief_item_photo.filename,
verbose_name="file",
),
),
(
"item",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="photos",
related_query_name="photo",
to="photobriefing.BriefItem",
verbose_name="brief item",
),
),
],
options={"abstract": False,},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.AddField(
model_name="briefitem",
name="category",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="photobriefing.Category",
verbose_name="category",
),
),
migrations.CreateModel(
name="BriefItemChoice",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
outbo.apps.core.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="Created at"
),
),
(
"updated_at",
outbo.apps.core.fields.ModificationDateTimeField(
auto_now=True, verbose_name="Updated at"
),
),
(
"deleted_at",
outbo.apps.core.fields.RemovalDateTimeField(
blank=True, editable=False, null=True, verbose_name="Deleted at"
),
),
(
"role",
models.CharField(
choices=[
(
outbo.apps.photobriefing.enums.ChoiceRole("<invalid>"),
"<invalid>",
),
(
outbo.apps.photobriefing.enums.ChoiceRole("supplier"),
"supplier",
),
(
outbo.apps.photobriefing.enums.ChoiceRole("client"),
"client",
),
],
default=outbo.apps.photobriefing.enums.ChoiceRole("<invalid>"),
editable=False,
max_length=30,
),
),
(
"brief",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="choices",
related_query_name="choice",
to="photobriefing.Brief",
verbose_name="brief",
),
),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="choices+",
related_query_name="choice",
to="photobriefing.Category",
verbose_name="category",
),
),
(
"item",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="choices",
related_query_name="choice",
to="photobriefing.BriefItem",
verbose_name="item",
),
),
],
options={"unique_together": {("brief", "category", "role")},},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 2.2.8 on 2019-12-18 14:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("photobriefing", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="brief",
name="concept_description",
field=models.TextField(blank=True),
),
]
Loaded 100 of 454 files, more files were not shown because too many files have changed in this diff. Show more