Initial commit from ZIP
This commit is contained in:
commit
378d4692a9
454 files changed
+40367
No files matched your search
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
.idea/
|
||||||
Executable
+151
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
## EVG Outbo
|
||||||
|
|
||||||
|
### Technology stack
|
||||||
|
1. React 16.9 + Typescript 3.6
|
||||||
|
1. Django 2.2
|
||||||
Executable
+24
@@ -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
|
||||||
Executable
+21
@@ -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:
|
||||||
Executable
+65
@@ -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:
|
||||||
Executable
+60
@@ -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:
|
||||||
Executable
+10
@@ -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
|
||||||
Vendored
BIN
Binary file not shown.
Executable
+14
@@ -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/
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
[flake8]
|
||||||
|
max-line-length = 80
|
||||||
|
max-complexity = 18
|
||||||
|
select = C,E,F,W,B,B950
|
||||||
|
ignore = E203, E501, W503
|
||||||
Executable
+286
@@ -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/
|
||||||
Executable
+19
@@ -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()
|
||||||
Executable
Whitespace-only changes.
Executable
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
+7
@@ -0,0 +1,7 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class MessageType(Enum):
|
||||||
|
USER = "User"
|
||||||
|
SUCCESS = "Success"
|
||||||
|
FAILURE = "Failure"
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Generated by Django 2.2.7 on 2019-11-29 08:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import outbo.apps.activity_log.enums
|
||||||
|
import outbo.apps.core.fields
|
||||||
|
import rules.contrib.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="LogItem",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
outbo.apps.core.fields.CreationDateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="Created at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
outbo.apps.core.fields.ModificationDateTimeField(
|
||||||
|
auto_now=True, verbose_name="Updated at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"deleted_at",
|
||||||
|
outbo.apps.core.fields.RemovalDateTimeField(
|
||||||
|
blank=True, editable=False, null=True, verbose_name="Deleted at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("message", models.TextField(verbose_name="message")),
|
||||||
|
(
|
||||||
|
"message_type",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
(outbo.apps.activity_log.enums.MessageType("User"), "User"),
|
||||||
|
(
|
||||||
|
outbo.apps.activity_log.enums.MessageType("Success"),
|
||||||
|
"Success",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
outbo.apps.activity_log.enums.MessageType("Failure"),
|
||||||
|
"Failure",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
default=outbo.apps.activity_log.enums.MessageType("User"),
|
||||||
|
max_length=25,
|
||||||
|
verbose_name="message type",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"abstract": False,},
|
||||||
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ProductLogItem",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"logitem_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="activity_log.LogItem",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"ordering": ["-created_at"],},
|
||||||
|
bases=("activity_log.logitem",),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ProjectLogItem",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"logitem_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="activity_log.LogItem",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"ordering": ["-created_at"],},
|
||||||
|
bases=("activity_log.logitem",),
|
||||||
|
),
|
||||||
|
]
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
# Generated by Django 2.2.7 on 2019-11-29 08:57
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("contenttypes", "0002_remove_content_type_name"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("activity_log", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="logitem",
|
||||||
|
name="created_by",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="created by",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="logitem",
|
||||||
|
name="polymorphic_ctype",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
editable=False,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="polymorphic_activity_log.logitem_set+",
|
||||||
|
to="contenttypes.ContentType",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 2.2.7 on 2019-11-29 08:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("projects", "0001_initial"),
|
||||||
|
("activity_log", "0002_auto_20191129_0957"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="projectlogitem",
|
||||||
|
name="project",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="log_items",
|
||||||
|
related_query_name="log_item",
|
||||||
|
to="projects.Project",
|
||||||
|
verbose_name="project",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 2.2.7 on 2019-11-29 08:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("activity_log", "0003_projectlogitem_project"),
|
||||||
|
("products", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="productlogitem",
|
||||||
|
name="product",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="log_items",
|
||||||
|
related_query_name="log_item",
|
||||||
|
to="products.Product",
|
||||||
|
verbose_name="product",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="productlogitem",
|
||||||
|
name="stage",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="products.ProductStage",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
# Generated by Django 2.2.10 on 2020-05-25 08:25
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("activity_log", "0004_auto_20191129_0957"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="logitem",
|
||||||
|
name="created_by",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="created by",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="productlogitem",
|
||||||
|
name="product",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="log_items",
|
||||||
|
related_query_name="log_item",
|
||||||
|
to="products.Product",
|
||||||
|
verbose_name="product",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="productlogitem",
|
||||||
|
name="stage",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="products.ProductStage",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="projectlogitem",
|
||||||
|
name="project",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="log_items",
|
||||||
|
related_query_name="log_item",
|
||||||
|
to="projects.Project",
|
||||||
|
verbose_name="project",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
Whitespace-only changes.
+65
@@ -0,0 +1,65 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import pgettext, gettext_lazy as _
|
||||||
|
from rules import always_allow
|
||||||
|
|
||||||
|
from outbo.apps.activity_log.enums import MessageType
|
||||||
|
from outbo.apps.core.models import (
|
||||||
|
TimestampableDeletableMixin,
|
||||||
|
RulesPolymorphicModel,
|
||||||
|
SoftDeletablePolymorphicManager,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LogItem(TimestampableDeletableMixin, RulesPolymorphicModel):
|
||||||
|
objects = SoftDeletablePolymorphicManager()
|
||||||
|
all_objects = SoftDeletablePolymorphicManager(alive_only=False)
|
||||||
|
|
||||||
|
message = models.TextField(verbose_name=_("message"))
|
||||||
|
message_type = models.CharField(
|
||||||
|
verbose_name=_("message type"),
|
||||||
|
max_length=25,
|
||||||
|
blank=False,
|
||||||
|
null=False,
|
||||||
|
default=MessageType.USER,
|
||||||
|
choices=((type, type.value) for type in MessageType),
|
||||||
|
)
|
||||||
|
created_by = models.ForeignKey(
|
||||||
|
verbose_name=_("created by"), to="authentication.User", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.message_type == MessageType.USER:
|
||||||
|
return self.message
|
||||||
|
return pgettext("log_item_messages", self.message)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductLogItem(LogItem):
|
||||||
|
class Meta:
|
||||||
|
rules_permissions = {"view": always_allow, "add": always_allow}
|
||||||
|
ordering = ["-created_at"]
|
||||||
|
|
||||||
|
product = models.ForeignKey(
|
||||||
|
verbose_name=_("product"),
|
||||||
|
to="products.Product",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="log_items",
|
||||||
|
related_query_name="log_item",
|
||||||
|
)
|
||||||
|
|
||||||
|
stage = models.ForeignKey(
|
||||||
|
to="products.ProductStage", on_delete=models.CASCADE, null=True, default=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectLogItem(LogItem):
|
||||||
|
class Meta:
|
||||||
|
rules_permissions = {"view": always_allow, "add": always_allow}
|
||||||
|
ordering = ["-created_at"]
|
||||||
|
|
||||||
|
project = models.ForeignKey(
|
||||||
|
verbose_name=_("project"),
|
||||||
|
to="projects.Project",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="log_items",
|
||||||
|
related_query_name="log_item",
|
||||||
|
)
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from outbo.apps.activity_log.enums import MessageType
|
||||||
|
from outbo.apps.activity_log.models import ProductLogItem, ProjectLogItem
|
||||||
|
|
||||||
|
|
||||||
|
class BaseProductLogItemSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ProductLogItem
|
||||||
|
|
||||||
|
def get_message_type(self, instance):
|
||||||
|
t, val = str(instance.message_type).split(".", 1)
|
||||||
|
return MessageType[val].value
|
||||||
|
|
||||||
|
|
||||||
|
class ProductLogItemSerializer:
|
||||||
|
class List(BaseProductLogItemSerializer):
|
||||||
|
message_type = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta(BaseProductLogItemSerializer.Meta):
|
||||||
|
fields = [
|
||||||
|
"message",
|
||||||
|
"message_type",
|
||||||
|
"stage",
|
||||||
|
"updated_at",
|
||||||
|
"created_at",
|
||||||
|
"created_by",
|
||||||
|
]
|
||||||
|
|
||||||
|
class Create(BaseProductLogItemSerializer):
|
||||||
|
class Meta(BaseProductLogItemSerializer.Meta):
|
||||||
|
fields = ["message"]
|
||||||
|
|
||||||
|
class AfterCreate(BaseProductLogItemSerializer):
|
||||||
|
message_type = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta(BaseProductLogItemSerializer.Meta):
|
||||||
|
fields = [
|
||||||
|
"message",
|
||||||
|
"message_type",
|
||||||
|
"stage",
|
||||||
|
"updated_at",
|
||||||
|
"created_at",
|
||||||
|
"created_by",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BaseProjectLogItemSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ProjectLogItem
|
||||||
|
|
||||||
|
def get_message_type(self, instance):
|
||||||
|
t, val = str(instance.message_type).split(".", 1)
|
||||||
|
return MessageType[val].value
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectLogItemSerializer:
|
||||||
|
class List(BaseProjectLogItemSerializer):
|
||||||
|
message_type = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta(BaseProjectLogItemSerializer.Meta):
|
||||||
|
fields = [
|
||||||
|
"message",
|
||||||
|
"message_type",
|
||||||
|
"updated_at",
|
||||||
|
"created_at",
|
||||||
|
"created_by",
|
||||||
|
]
|
||||||
|
|
||||||
|
class Create(BaseProjectLogItemSerializer):
|
||||||
|
class Meta(BaseProjectLogItemSerializer.Meta):
|
||||||
|
fields = ["message"]
|
||||||
|
|
||||||
|
class AfterCreate(BaseProjectLogItemSerializer):
|
||||||
|
message_type = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta(BaseProjectLogItemSerializer.Meta):
|
||||||
|
fields = [
|
||||||
|
"message",
|
||||||
|
"message_type",
|
||||||
|
"updated_at",
|
||||||
|
"created_at",
|
||||||
|
"created_by",
|
||||||
|
]
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
app_name = "activity_log"
|
||||||
|
urlpatterns = []
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
from outbo.apps.activity_log.models import ProductLogItem, ProjectLogItem
|
||||||
|
from outbo.apps.activity_log.serializers import (
|
||||||
|
ProductLogItemSerializer,
|
||||||
|
ProjectLogItemSerializer,
|
||||||
|
)
|
||||||
|
from outbo.apps.core.viewsets import (
|
||||||
|
AutoPermissionViewSetMixin,
|
||||||
|
ListModelMixin,
|
||||||
|
MultipleSerializerViewSet,
|
||||||
|
NestedViewSetMixin,
|
||||||
|
CreateModelMixin,
|
||||||
|
)
|
||||||
|
from outbo.apps.products.models import Product
|
||||||
|
from outbo.apps.projects.models import Project
|
||||||
|
|
||||||
|
|
||||||
|
class ProductLogItemNestedViewSet(
|
||||||
|
AutoPermissionViewSetMixin,
|
||||||
|
ListModelMixin,
|
||||||
|
CreateModelMixin,
|
||||||
|
NestedViewSetMixin,
|
||||||
|
MultipleSerializerViewSet,
|
||||||
|
):
|
||||||
|
queryset = ProductLogItem.objects.all()
|
||||||
|
serializer_class_set = ProductLogItemSerializer
|
||||||
|
|
||||||
|
def perform_create(self, *, serializer):
|
||||||
|
current_user = self.request.user
|
||||||
|
parents = self.get_parents_query_dict()
|
||||||
|
product = Product.objects.get(pk=parents["product_id"])
|
||||||
|
return serializer.save(product=product, created_by=current_user)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectLogItemNestedViewSet(
|
||||||
|
AutoPermissionViewSetMixin,
|
||||||
|
ListModelMixin,
|
||||||
|
CreateModelMixin,
|
||||||
|
NestedViewSetMixin,
|
||||||
|
MultipleSerializerViewSet,
|
||||||
|
):
|
||||||
|
queryset = ProjectLogItem.objects.all()
|
||||||
|
serializer_class_set = ProjectLogItemSerializer
|
||||||
|
|
||||||
|
def perform_create(self, *, serializer):
|
||||||
|
current_user = self.request.user
|
||||||
|
parents = self.get_parents_query_dict()
|
||||||
|
project = Project.objects.get(pk=parents["project_id"])
|
||||||
|
return serializer.save(project=project, created_by=current_user)
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework.authentication import TokenAuthentication as BaseTokenAuthentication
|
||||||
|
|
||||||
|
|
||||||
|
class TokenAuthentication(BaseTokenAuthentication):
|
||||||
|
keyword = "Bearer"
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationAppConfig(AppConfig):
|
||||||
|
name = "outbo.apps.authentication"
|
||||||
|
label = "authentication"
|
||||||
|
verbose_name = _("Authentication")
|
||||||
|
|
||||||
|
|
||||||
|
default_app_config = "outbo.apps.authentication.AuthenticationAppConfig"
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from outbo.apps.authentication.models import User
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(User)
|
||||||
|
class UserAdmin(BaseUserAdmin):
|
||||||
|
fieldsets = (
|
||||||
|
(None, {"fields": ("username", "password")}),
|
||||||
|
(
|
||||||
|
_("Personal info"),
|
||||||
|
{"fields": ("first_name", "last_name", "email", "photo", "phone_number")},
|
||||||
|
),
|
||||||
|
(_("Organization info"), {"fields": ("organization", "job_title", "is_observer", "is_trader", "category")}),
|
||||||
|
)
|
||||||
|
add_fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"classes": ("wide",),
|
||||||
|
"fields": ("username", "email", "password1", "password2", "is_observer", "is_trader", "category"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
list_display = ("username", "email", "first_name", "last_name")
|
||||||
|
list_filter = ()
|
||||||
|
search_fields = ("username", "first_name", "last_name", "email")
|
||||||
|
ordering = ("username",)
|
||||||
|
filter_horizontal = ()
|
||||||
|
autocomplete_fields = ["organization"]
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
return super().get_queryset(request).filter(is_superuser=False)
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
from django.contrib.auth.base_user import BaseUserManager
|
||||||
|
|
||||||
|
from outbo.apps.core.models import SoftDeletableManager
|
||||||
|
|
||||||
|
|
||||||
|
class UserManager(SoftDeletableManager, BaseUserManager):
|
||||||
|
def __init__(self, *, alive_only=False):
|
||||||
|
self.alive_only = alive_only
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def create_user(self, username, email, password=None, save=True, **kwargs):
|
||||||
|
if username is None:
|
||||||
|
raise TypeError("User must have a username.")
|
||||||
|
if email is None:
|
||||||
|
raise TypeError("User must have an email address.")
|
||||||
|
|
||||||
|
user = self.model(
|
||||||
|
username=username, email=self.normalize_email(email), **kwargs
|
||||||
|
)
|
||||||
|
user.set_password(password)
|
||||||
|
if save:
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
def create_superuser(self, username, email, password, save=True):
|
||||||
|
if password is None:
|
||||||
|
raise TypeError("Superuser must have a password.")
|
||||||
|
|
||||||
|
user = self.create_user(
|
||||||
|
username=username, email=email, password=password, save=False
|
||||||
|
)
|
||||||
|
user.is_superuser = True
|
||||||
|
user.is_staff = True
|
||||||
|
if save:
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
def regular_users(self):
|
||||||
|
return self.filter(is_staff=False)
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
# Generated by Django 2.2.7 on 2019-11-29 08:57
|
||||||
|
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import outbo.apps.core.fields
|
||||||
|
import rules.contrib.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("auth", "0011_update_proxy_permissions"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="User",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||||
|
(
|
||||||
|
"last_login",
|
||||||
|
models.DateTimeField(
|
||||||
|
blank=True, null=True, verbose_name="last login"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_superuser",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||||
|
verbose_name="superuser status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
outbo.apps.core.fields.CreationDateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="Created at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
outbo.apps.core.fields.ModificationDateTimeField(
|
||||||
|
auto_now=True, verbose_name="Updated at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"deleted_at",
|
||||||
|
outbo.apps.core.fields.RemovalDateTimeField(
|
||||||
|
blank=True, editable=False, null=True, verbose_name="Deleted at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"username",
|
||||||
|
models.CharField(
|
||||||
|
error_messages={
|
||||||
|
"unique": "A user with that username already exists."
|
||||||
|
},
|
||||||
|
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||||
|
max_length=150,
|
||||||
|
unique=True,
|
||||||
|
validators=[
|
||||||
|
django.contrib.auth.validators.UnicodeUsernameValidator()
|
||||||
|
],
|
||||||
|
verbose_name="username",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"email",
|
||||||
|
models.EmailField(
|
||||||
|
blank=True,
|
||||||
|
max_length=254,
|
||||||
|
unique=True,
|
||||||
|
verbose_name="email address",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_staff",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Designates whether the user can log into this admin site.",
|
||||||
|
verbose_name="staff status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"first_name",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=30, verbose_name="first name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"last_name",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=150, verbose_name="last name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"photo",
|
||||||
|
models.ImageField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
upload_to="users/",
|
||||||
|
verbose_name="profile photo",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"groups",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||||
|
related_name="user_set",
|
||||||
|
related_query_name="user",
|
||||||
|
to="auth.Group",
|
||||||
|
verbose_name="groups",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "user", "verbose_name_plural": "users",},
|
||||||
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
]
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
# Generated by Django 2.2.7 on 2019-11-29 08:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentication", "0001_initial"),
|
||||||
|
("organizations", "0001_initial"),
|
||||||
|
("auth", "0011_update_proxy_permissions"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="organization",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="organizations.Organization",
|
||||||
|
verbose_name="organization",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="user_permissions",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Specific permissions for this user.",
|
||||||
|
related_name="user_set",
|
||||||
|
related_query_name="user",
|
||||||
|
to="auth.Permission",
|
||||||
|
verbose_name="user permissions",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 2.2.8 on 2020-01-16 12:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentication", "0002_auto_20191129_0957"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="job_title",
|
||||||
|
field=models.CharField(blank=True, max_length=30, verbose_name="job title"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="phone_number",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, max_length=30, verbose_name="phone_number"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 2.2.8 on 2020-02-11 13:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentication", "0003_auto_20200116_1318"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="email",
|
||||||
|
field=models.EmailField(
|
||||||
|
blank=True,
|
||||||
|
max_length=254,
|
||||||
|
null=True,
|
||||||
|
unique=True,
|
||||||
|
verbose_name="email address",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 2.2.10 on 2020-05-25 08:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentication", "0004_auto_20200211_1405"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="organization",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="organizations.Organization",
|
||||||
|
verbose_name="organization",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.16 on 2020-11-03 13:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('authentication', '0005_auto_20200525_1025'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='is_observer',
|
||||||
|
field=models.BooleanField(default=False, help_text='Designates whether the user is an observer.', verbose_name='observer status'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.22 on 2021-05-11 07:29
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('authentication', '0006_user_is_observer'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='is_trader',
|
||||||
|
field=models.BooleanField(default=False, help_text='Designates whether the user is an trader.', verbose_name='trader status'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 2.2.22 on 2021-05-11 09:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('organizations', '0002_auto_20200525_1025'),
|
||||||
|
('authentication', '0007_user_is_trader'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='category',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='organizations.Category', verbose_name='category'),
|
||||||
|
),
|
||||||
|
]
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.2.22 on 2021-05-17 10:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('authentication', '0008_user_category'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='category',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='organizations.Category', verbose_name='category for trader'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Whitespace-only changes.
+160
@@ -0,0 +1,160 @@
|
|||||||
|
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
|
||||||
|
from django.contrib.auth.validators import UnicodeUsernameValidator
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.mail import send_mail
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rules.contrib.models import RulesModelMixin, RulesModelBase
|
||||||
|
|
||||||
|
from outbo.apps.authentication import rules
|
||||||
|
from outbo.apps.authentication.managers import UserManager
|
||||||
|
from outbo.apps.core.models import TimestampableDeletableMixin
|
||||||
|
|
||||||
|
|
||||||
|
class User(
|
||||||
|
RulesModelMixin,
|
||||||
|
TimestampableDeletableMixin,
|
||||||
|
AbstractBaseUser,
|
||||||
|
PermissionsMixin,
|
||||||
|
metaclass=RulesModelBase,
|
||||||
|
):
|
||||||
|
objects = UserManager()
|
||||||
|
all_objects = UserManager(alive_only=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("user")
|
||||||
|
verbose_name_plural = _("users")
|
||||||
|
rules_permissions = {
|
||||||
|
"read": rules.can_read_user,
|
||||||
|
"read_current": rules.can_read_current_user,
|
||||||
|
}
|
||||||
|
|
||||||
|
username_validator = UnicodeUsernameValidator()
|
||||||
|
|
||||||
|
username = models.CharField(
|
||||||
|
verbose_name=_("username"),
|
||||||
|
max_length=150,
|
||||||
|
unique=True,
|
||||||
|
help_text=_(
|
||||||
|
"Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
|
||||||
|
),
|
||||||
|
validators=[username_validator],
|
||||||
|
error_messages={"unique": _("A user with that username already exists.")},
|
||||||
|
)
|
||||||
|
|
||||||
|
email = models.EmailField(
|
||||||
|
verbose_name=_("email address"), blank=True, null=True, unique=True
|
||||||
|
)
|
||||||
|
|
||||||
|
is_staff = models.BooleanField(
|
||||||
|
verbose_name=_("staff status"),
|
||||||
|
default=False,
|
||||||
|
help_text=_("Designates whether the user can log into this admin site."),
|
||||||
|
)
|
||||||
|
|
||||||
|
is_observer = models.BooleanField(
|
||||||
|
verbose_name=_("observer status"),
|
||||||
|
default=False,
|
||||||
|
help_text=_("Designates whether the user is an observer."),
|
||||||
|
)
|
||||||
|
|
||||||
|
is_trader = models.BooleanField(
|
||||||
|
verbose_name=_("trader status"),
|
||||||
|
default=False,
|
||||||
|
help_text=_("Designates whether the user is an trader."),
|
||||||
|
)
|
||||||
|
|
||||||
|
first_name = models.CharField(
|
||||||
|
verbose_name=_("first name"), max_length=30, blank=True
|
||||||
|
)
|
||||||
|
last_name = models.CharField(
|
||||||
|
verbose_name=_("last name"), max_length=150, blank=True
|
||||||
|
)
|
||||||
|
organization = models.ForeignKey(
|
||||||
|
verbose_name=_("organization"),
|
||||||
|
to="organizations.Organization",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
null=True,
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
category = models.ForeignKey(
|
||||||
|
verbose_name=_("category for trader"),
|
||||||
|
to="organizations.Category",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
photo = models.ImageField(
|
||||||
|
verbose_name=_("profile photo"), upload_to="users/", blank=True, null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
job_title = models.CharField(verbose_name=_("job title"), max_length=30, blank=True)
|
||||||
|
phone_number = models.CharField(
|
||||||
|
verbose_name=_("phone_number"), max_length=30, blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
USERNAME_FIELD = "username"
|
||||||
|
EMAIL_FIELD = "email"
|
||||||
|
REQUIRED_FIELDS = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def photo_url(self):
|
||||||
|
return None if not self.photo else self.photo.url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_active(self):
|
||||||
|
return self.deleted_at is None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self):
|
||||||
|
return f"{self.first_name} {self.last_name}".strip()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_agency(self):
|
||||||
|
return (
|
||||||
|
False
|
||||||
|
if not self.organization
|
||||||
|
else self.organization.type
|
||||||
|
== ContentType.objects.get(
|
||||||
|
app_label="organizations", model="agencyorganization"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_client(self):
|
||||||
|
return (
|
||||||
|
False
|
||||||
|
if not self.organization
|
||||||
|
else self.organization.type
|
||||||
|
== ContentType.objects.get(
|
||||||
|
app_label="organizations", model="clientorganization"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_supplier(self):
|
||||||
|
return (
|
||||||
|
False
|
||||||
|
if not self.organization
|
||||||
|
else self.organization.type
|
||||||
|
== ContentType.objects.get(
|
||||||
|
app_label="organizations", model="supplierorganization"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_role(self):
|
||||||
|
from outbo.apps.projects.models.member import MembershipType
|
||||||
|
|
||||||
|
if self.is_client:
|
||||||
|
return MembershipType.CLIENT
|
||||||
|
elif self.is_agency:
|
||||||
|
return MembershipType.AGENCY
|
||||||
|
elif self.is_supplier:
|
||||||
|
return MembershipType.SUPPLIER
|
||||||
|
|
||||||
|
def email_user(self, subject, message, from_email=None, **kwargs):
|
||||||
|
"""Send an email to this user."""
|
||||||
|
send_mail(subject, message, from_email, [self.email], **kwargs)
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
from rules import predicate
|
||||||
|
|
||||||
|
|
||||||
|
@predicate
|
||||||
|
def _is_active(user):
|
||||||
|
return user.is_active
|
||||||
|
|
||||||
|
|
||||||
|
can_read_user = _is_active
|
||||||
|
can_read_current_user = _is_active
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework.authtoken.serializers import (
|
||||||
|
AuthTokenSerializer as BaseAuthTokenSerializer,
|
||||||
|
)
|
||||||
|
from outbo.apps.authentication.models import User
|
||||||
|
from outbo.apps.organizations.models import Organization
|
||||||
|
from outbo.apps.organizations.serializers import OrganizationSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class AuthTokenSerializer(BaseAuthTokenSerializer):
|
||||||
|
def validate(self, attrs):
|
||||||
|
validated_attrs = super().validate(attrs)
|
||||||
|
user = validated_attrs["user"]
|
||||||
|
if user.is_staff:
|
||||||
|
msg = _("Administrators cannot login via API.")
|
||||||
|
raise serializers.ValidationError(msg, code="authorization")
|
||||||
|
return validated_attrs
|
||||||
|
|
||||||
|
|
||||||
|
class _BaseUserSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
|
||||||
|
|
||||||
|
class UserSerializer:
|
||||||
|
class List(_BaseUserSerializer):
|
||||||
|
organization_name = serializers.SlugRelatedField(
|
||||||
|
source="organization", slug_field="name", read_only=True
|
||||||
|
)
|
||||||
|
photo = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta(_BaseUserSerializer.Meta):
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"full_name",
|
||||||
|
"organization_name",
|
||||||
|
"photo",
|
||||||
|
"email",
|
||||||
|
"phone_number",
|
||||||
|
"job_title",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_photo(self, instance: User):
|
||||||
|
if instance.photo_url:
|
||||||
|
return self.context["request"].build_absolute_uri(instance.photo_url)
|
||||||
|
|
||||||
|
class Retrieve(_BaseUserSerializer):
|
||||||
|
organization = serializers.SerializerMethodField()
|
||||||
|
photo = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta(_BaseUserSerializer.Meta):
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"username",
|
||||||
|
"full_name",
|
||||||
|
"organization",
|
||||||
|
"photo",
|
||||||
|
"email",
|
||||||
|
"phone_number",
|
||||||
|
"job_title",
|
||||||
|
"is_observer",
|
||||||
|
"is_trader",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_photo(self, instance: User):
|
||||||
|
if instance.photo_url:
|
||||||
|
return self.context["request"].build_absolute_uri(instance.photo_url)
|
||||||
|
|
||||||
|
# NOTE: Hacky workaround
|
||||||
|
def get_organization(self, instance):
|
||||||
|
serializer = OrganizationSerializer.List(read_only=True)
|
||||||
|
serializer.instance = Organization.objects.get(id=instance.organization_id)
|
||||||
|
return serializer.data
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from rest_framework import routers
|
||||||
|
|
||||||
|
from outbo.apps.authentication.views import ObtainAuthToken
|
||||||
|
from outbo.apps.authentication.viewsets import UserViewSet
|
||||||
|
|
||||||
|
app_name = "authentication"
|
||||||
|
|
||||||
|
router = routers.SimpleRouter()
|
||||||
|
router.register(prefix="users", viewset=UserViewSet)
|
||||||
|
|
||||||
|
view_urls = [path("token/", ObtainAuthToken.as_view(), name="token")]
|
||||||
|
urlpatterns = view_urls + router.urls
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
from djangorestframework_camel_case.parser import (
|
||||||
|
CamelCaseFormParser,
|
||||||
|
CamelCaseMultiPartParser,
|
||||||
|
CamelCaseJSONParser,
|
||||||
|
)
|
||||||
|
from djangorestframework_camel_case.render import CamelCaseJSONRenderer
|
||||||
|
from rest_framework.authtoken.views import ObtainAuthToken as BaseObtainAuthToken
|
||||||
|
|
||||||
|
from outbo.apps.authentication.serializers import AuthTokenSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class ObtainAuthToken(BaseObtainAuthToken):
|
||||||
|
serializer_class = AuthTokenSerializer
|
||||||
|
renderer_classes = [CamelCaseJSONRenderer]
|
||||||
|
parser_classes = [
|
||||||
|
CamelCaseFormParser,
|
||||||
|
CamelCaseMultiPartParser,
|
||||||
|
CamelCaseJSONParser,
|
||||||
|
]
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from outbo.apps.authentication.models import User
|
||||||
|
from outbo.apps.authentication.serializers import UserSerializer
|
||||||
|
from outbo.apps.core.viewsets import (
|
||||||
|
MultipleSerializerViewSet,
|
||||||
|
AutoPermissionViewSetMixin,
|
||||||
|
ListModelMixin,
|
||||||
|
RetrieveModelMixin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserViewSet(
|
||||||
|
RetrieveModelMixin,
|
||||||
|
ListModelMixin,
|
||||||
|
AutoPermissionViewSetMixin,
|
||||||
|
MultipleSerializerViewSet,
|
||||||
|
):
|
||||||
|
queryset = User.objects.regular_users()
|
||||||
|
serializer_class_set = UserSerializer
|
||||||
|
serializer_map = {**MultipleSerializerViewSet.serializer_map, "me": "Retrieve"}
|
||||||
|
permission_type_map = {
|
||||||
|
**AutoPermissionViewSetMixin.permission_type_map,
|
||||||
|
"me": "read_current",
|
||||||
|
}
|
||||||
|
|
||||||
|
@action(detail=False, methods=["get"])
|
||||||
|
def me(self, request):
|
||||||
|
current_user = request.user
|
||||||
|
serializer = self.get_serializer(current_user)
|
||||||
|
return Response(serializer.data)
|
||||||
Whitespace-only changes.
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
from nested_admin.nested import NestedTabularInline
|
||||||
|
from polymorphic.admin import (
|
||||||
|
PolymorphicParentModelAdmin as BasePolymorphicParentModelAdmin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PolymorphicParentModelAdmin(BasePolymorphicParentModelAdmin):
|
||||||
|
def get_child_type_choices(self, request, action):
|
||||||
|
choices = super().get_child_type_choices(request=request, action=action)
|
||||||
|
return tuple((ct_id, label.title()) for (ct_id, label) in choices)
|
||||||
|
|
||||||
|
|
||||||
|
class NestedTabularSingleItemInline(NestedTabularInline):
|
||||||
|
template = "nesting/admin/inlines/tabular_single.html"
|
||||||
|
|
||||||
|
|
||||||
|
class NestedStackedSingleItemInline(NestedTabularInline):
|
||||||
|
template = "nesting/admin/inlines/stacked_single.html"
|
||||||
Executable
+122
@@ -0,0 +1,122 @@
|
|||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.db import models
|
||||||
|
from django_extensions.db.fields import (
|
||||||
|
CreationDateTimeField as BaseCreationDateTimeField,
|
||||||
|
ModificationDateTimeField as BaseModificationDateTimeField,
|
||||||
|
)
|
||||||
|
from django_fsm import FSMFieldMixin
|
||||||
|
|
||||||
|
from outbo.apps.core.forms import EnumChoiceField
|
||||||
|
|
||||||
|
|
||||||
|
class CreationDateTimeField(BaseCreationDateTimeField):
|
||||||
|
"""
|
||||||
|
CreationDateTimeField
|
||||||
|
|
||||||
|
By default, sets editable=False, blank=True, auto_now_add=True
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ModificationDateTimeField(BaseModificationDateTimeField):
|
||||||
|
"""
|
||||||
|
ModificationDateTimeField
|
||||||
|
|
||||||
|
By default, sets editable=False, blank=True, auto_now=True
|
||||||
|
|
||||||
|
Sets value to now every time the object is saved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class RemovalDateTimeField(models.DateTimeField):
|
||||||
|
"""
|
||||||
|
RemovalDateTimeField
|
||||||
|
|
||||||
|
By default, sets editable=False, blank=True, null=True
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs.setdefault("editable", False)
|
||||||
|
kwargs.setdefault("blank", True)
|
||||||
|
kwargs.setdefault("null", True)
|
||||||
|
models.DateTimeField.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_internal_type(self):
|
||||||
|
return "DateTimeField"
|
||||||
|
|
||||||
|
def deconstruct(self):
|
||||||
|
name, path, args, kwargs = super().deconstruct()
|
||||||
|
if self.editable is not False:
|
||||||
|
kwargs["editable"] = True
|
||||||
|
if self.blank is not True:
|
||||||
|
kwargs["blank"] = False
|
||||||
|
if self.null is not True:
|
||||||
|
kwargs["null"] = False
|
||||||
|
return name, path, args, kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class FSMKeyField(FSMFieldMixin, models.ForeignKey):
|
||||||
|
def get_state(self, instance):
|
||||||
|
return instance.__dict__[self.attname]
|
||||||
|
|
||||||
|
def set_state(self, instance, state):
|
||||||
|
instance.__dict__[self.attname] = self.to_python(state)
|
||||||
|
|
||||||
|
|
||||||
|
class EnumCharField(models.CharField):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
enum = kwargs.pop("enum", None)
|
||||||
|
if enum is not None:
|
||||||
|
if kwargs.get("choices", None) is not None:
|
||||||
|
raise ValueError("Cannot provide choices for EnumCharField")
|
||||||
|
kwargs["choices"] = [(item, item.value) for item in enum]
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.validators.pop() # lengthvalidator not necessary on this one
|
||||||
|
self.enum = enum
|
||||||
|
|
||||||
|
def get_internal_type(self):
|
||||||
|
return "CharField"
|
||||||
|
|
||||||
|
def formfield(self, **kwargs):
|
||||||
|
enum_form_class = partial(EnumChoiceField, enum=self.enum)
|
||||||
|
defaults = {
|
||||||
|
"widget": forms.Select,
|
||||||
|
"form_class": enum_form_class,
|
||||||
|
"choices": [(str(item), item.value) for item in self.enum],
|
||||||
|
}
|
||||||
|
defaults.update(kwargs)
|
||||||
|
return super().formfield(**defaults)
|
||||||
|
|
||||||
|
def deconstruct(self):
|
||||||
|
name, path, args, kwargs = super().deconstruct()
|
||||||
|
kwargs["enum"] = self.enum
|
||||||
|
if "choices" in kwargs:
|
||||||
|
del kwargs["choices"]
|
||||||
|
|
||||||
|
return name, path, args, kwargs
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
value = super().to_python(value)
|
||||||
|
if value is not None:
|
||||||
|
if not isinstance(value, self.enum):
|
||||||
|
enum_name, enum_value = value.split(".", 1)
|
||||||
|
if enum_name == self.enum.__name__:
|
||||||
|
return self.enum[enum_value]
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get_prep_value(self, value):
|
||||||
|
value = super().get_prep_value(value)
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
if isinstance(value, self.enum):
|
||||||
|
return str(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def from_db_value(self, value, *args):
|
||||||
|
if value is not None:
|
||||||
|
enum_name, enum_value = value.split(".", 1)
|
||||||
|
if enum_name == self.enum.__name__:
|
||||||
|
return self.enum[enum_value]
|
||||||
|
return value
|
||||||
Executable
+29
@@ -0,0 +1,29 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class EnumChoiceField(forms.ChoiceField):
|
||||||
|
def __init__(self, enum, **kwargs):
|
||||||
|
kwargs.setdefault("choices", [(item, item.value) for item in enum])
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.enum = enum
|
||||||
|
|
||||||
|
def prepare_value(self, value):
|
||||||
|
if isinstance(value, self.enum):
|
||||||
|
return str(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
value = super().to_python(value)
|
||||||
|
if not isinstance(value, self.enum):
|
||||||
|
try:
|
||||||
|
enum_name, enum_value = value.split(".", 1)
|
||||||
|
if enum_name == self.enum.__name__:
|
||||||
|
value = self.enum[enum_value]
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError(self.error_messages["invalid"], code="invalid")
|
||||||
|
return value
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
value = super().clean(value)
|
||||||
|
return self.enum(value)
|
||||||
Whitespace-only changes.
Whitespace-only changes.
+236
@@ -0,0 +1,236 @@
|
|||||||
|
from typing import TYPE_CHECKING, Tuple, Iterable
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from faker import Factory
|
||||||
|
|
||||||
|
from tests.factories.authentication import (
|
||||||
|
UserFactory,
|
||||||
|
AgencyUserFactory,
|
||||||
|
ClientUserFactory,
|
||||||
|
SupplierUserFactory,
|
||||||
|
)
|
||||||
|
from tests.factories.organizations import (
|
||||||
|
CategoryFactory,
|
||||||
|
BrandFactory,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tests.factories.products import ProductStageFactory, PackagingTypeFactory
|
||||||
|
from tests.factories.token import TokenFactory
|
||||||
|
|
||||||
|
faker = Factory()
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from outbo.apps.organizations.models import ClientOrganization
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Seed with some existing data"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("--dev", action="store_true", help="Seed with dev data")
|
||||||
|
|
||||||
|
def __make_users_and_organizations(self, *, password=None):
|
||||||
|
self.stdout.write("Seeding organizations and users...")
|
||||||
|
|
||||||
|
if password is None:
|
||||||
|
password = "password"
|
||||||
|
|
||||||
|
UserFactory(
|
||||||
|
username="admin",
|
||||||
|
password=password,
|
||||||
|
email="admin@example.com",
|
||||||
|
is_superuser=True,
|
||||||
|
is_staff=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
agency = AgencyUserFactory(
|
||||||
|
username="agency",
|
||||||
|
password=password,
|
||||||
|
email="agency@example.com",
|
||||||
|
organization__name="Sample agency",
|
||||||
|
)
|
||||||
|
|
||||||
|
client = ClientUserFactory(
|
||||||
|
username="client",
|
||||||
|
password=password,
|
||||||
|
email="client@example.com",
|
||||||
|
organization__name="Sample client",
|
||||||
|
)
|
||||||
|
|
||||||
|
supplier = SupplierUserFactory(
|
||||||
|
username="supplier",
|
||||||
|
password=password,
|
||||||
|
email="supplier@example.com",
|
||||||
|
organization__name="Sample supplier",
|
||||||
|
)
|
||||||
|
|
||||||
|
TokenFactory(user=agency, key="6f9afaa36ee8b224181dc14e2ca9af33e97c0f71")
|
||||||
|
TokenFactory(user=client, key="481372f60325289029679df25c72e6ccd05892ed")
|
||||||
|
TokenFactory(user=supplier, key="d23d8f187614c8a76fe7316470e6f50ff5bc4188")
|
||||||
|
|
||||||
|
def __make_product_stages(self):
|
||||||
|
self.stdout.write("Seeding product stages...")
|
||||||
|
|
||||||
|
ProductStageFactory(
|
||||||
|
id="waiting_for_brief",
|
||||||
|
label="1",
|
||||||
|
group=1,
|
||||||
|
description="Briefing",
|
||||||
|
next_action="Oczekuje na uzupełnienie briefu",
|
||||||
|
order=1,
|
||||||
|
)
|
||||||
|
ProductStageFactory(
|
||||||
|
id="waiting_for_brief_accept_by_agency",
|
||||||
|
label="1A",
|
||||||
|
group=1,
|
||||||
|
description="Akceptacja agencji",
|
||||||
|
next_action="Oczekuje na akceptację agencji",
|
||||||
|
order=2,
|
||||||
|
)
|
||||||
|
ProductStageFactory(
|
||||||
|
id="waiting_for_brief_accept_by_client",
|
||||||
|
label="1B",
|
||||||
|
group=1,
|
||||||
|
description="Akceptacja klienta",
|
||||||
|
next_action="Oczekuje na akceptację klienta",
|
||||||
|
order=3,
|
||||||
|
)
|
||||||
|
ProductStageFactory(
|
||||||
|
id="waiting_for_draft",
|
||||||
|
label="2A",
|
||||||
|
group=2,
|
||||||
|
description="Publikacja draftu",
|
||||||
|
next_action="Oczekuje na publikację draftu",
|
||||||
|
order=4,
|
||||||
|
)
|
||||||
|
ProductStageFactory(
|
||||||
|
id="waiting_for_draft_accept_by_supplier",
|
||||||
|
label="2B",
|
||||||
|
group=2,
|
||||||
|
description="Akceptacja dostawcy",
|
||||||
|
next_action="Oczekuje na akceptację dostawcy",
|
||||||
|
order=5,
|
||||||
|
)
|
||||||
|
ProductStageFactory(
|
||||||
|
id="waiting_for_draft_accept_by_client",
|
||||||
|
label="2C",
|
||||||
|
group=2,
|
||||||
|
description="Akceptacja klienta",
|
||||||
|
next_action="Oczekuje na akceptację klientget_random_stringa",
|
||||||
|
order=6,
|
||||||
|
)
|
||||||
|
ProductStageFactory(
|
||||||
|
id="waiting_for_ff",
|
||||||
|
label="3A",
|
||||||
|
group=3,
|
||||||
|
description="Publikacja FF",
|
||||||
|
next_action="Oczekuje na publikację draftu",
|
||||||
|
order=7,
|
||||||
|
)
|
||||||
|
ProductStageFactory(
|
||||||
|
id="waiting_for_ff_accept_by_supplier",
|
||||||
|
label="3B",
|
||||||
|
group=3,
|
||||||
|
description="Akceptacja dostawcy",
|
||||||
|
next_action="Oczekuje na akceptację dostawcy",
|
||||||
|
order=8,
|
||||||
|
)
|
||||||
|
ProductStageFactory(
|
||||||
|
id="waiting_for_ff_accept_by_client",
|
||||||
|
label="3C",
|
||||||
|
group=3,
|
||||||
|
description="Akceptacja klienta",
|
||||||
|
next_action="Oczekuje na akceptację klienta",
|
||||||
|
order=9,
|
||||||
|
)
|
||||||
|
ProductStageFactory(
|
||||||
|
id="waiting_for_pf",
|
||||||
|
label="4A",
|
||||||
|
group=4,
|
||||||
|
description="Publikacja printing files",
|
||||||
|
next_action="Oczekuje na publikację printing files",
|
||||||
|
order=10,
|
||||||
|
)
|
||||||
|
ProductStageFactory(
|
||||||
|
id="waiting_for_pf_accept_by_supplier",
|
||||||
|
label="4B",
|
||||||
|
group=4,
|
||||||
|
description="Akceptacja dostawcy",
|
||||||
|
next_action="Oczekuje na akceptację dostawcy",
|
||||||
|
order=11,
|
||||||
|
)
|
||||||
|
ProductStageFactory(
|
||||||
|
id="waiting_for_pf_accept_by_client",
|
||||||
|
label="4C",
|
||||||
|
group=4,
|
||||||
|
description="Akceptacja klienta",
|
||||||
|
next_action="Oczekuje na akceptację klienta",
|
||||||
|
order=12,
|
||||||
|
)
|
||||||
|
ProductStageFactory(
|
||||||
|
id="completed",
|
||||||
|
label="5",
|
||||||
|
group=5,
|
||||||
|
description="Zako\u0144czono",
|
||||||
|
next_action="Oczekuje na archiwizację",
|
||||||
|
order=13,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __make_packaging_types(self, *, items: Iterable[str] = None):
|
||||||
|
self.stdout.write("Seeding packaging types...")
|
||||||
|
if items:
|
||||||
|
for name in items:
|
||||||
|
PackagingTypeFactory(name=name)
|
||||||
|
else:
|
||||||
|
PackagingTypeFactory.create_batch(size=20)
|
||||||
|
|
||||||
|
def __make_brands(
|
||||||
|
self, *, items: Iterable[Tuple[str, "ClientOrganization"]] = None
|
||||||
|
):
|
||||||
|
self.stdout.write("Seeding brands...")
|
||||||
|
if items:
|
||||||
|
for name, client in items:
|
||||||
|
BrandFactory(name=name, client=client)
|
||||||
|
else:
|
||||||
|
BrandFactory.create_batch(size=20)
|
||||||
|
|
||||||
|
def __make_categories(self, *, items: Iterable[str] = None):
|
||||||
|
self.stdout.write("Seeding categories...")
|
||||||
|
if items:
|
||||||
|
for item in items:
|
||||||
|
CategoryFactory(name=item)
|
||||||
|
else:
|
||||||
|
CategoryFactory.create_batch(size=20)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
self.stdout.write("Seeding the database...")
|
||||||
|
|
||||||
|
if not options["dev"]:
|
||||||
|
packaging_types = (
|
||||||
|
"Carton/gift box",
|
||||||
|
"Header",
|
||||||
|
"Hangtag",
|
||||||
|
"Paper band",
|
||||||
|
"Paper board",
|
||||||
|
"Blister",
|
||||||
|
"Image insert",
|
||||||
|
"Simple band",
|
||||||
|
"Label/sticker",
|
||||||
|
"Simple CDU/Displaybox",
|
||||||
|
)
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
password = "!secret123"
|
||||||
|
|
||||||
|
self.__make_users_and_organizations(password=password)
|
||||||
|
self.__make_product_stages()
|
||||||
|
self.__make_packaging_types()
|
||||||
|
self.__make_brands()
|
||||||
|
self.__make_categories()
|
||||||
|
|
||||||
|
self.stdout.write("Seeding completed.")
|
||||||
|
self.stdout.write("Credentials:")
|
||||||
|
self.stdout.write(f" admin: {password}")
|
||||||
|
self.stdout.write(f" agency: {password}")
|
||||||
|
self.stdout.write(f" client: {password}")
|
||||||
|
self.stdout.write(f" supplier: {password}")
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 2.2.16 on 2021-01-21 14:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="EmailLog",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("sender", models.CharField(max_length=254)),
|
||||||
|
("receiver", models.CharField(max_length=254)),
|
||||||
|
("time", models.DateTimeField()),
|
||||||
|
(
|
||||||
|
"status",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("sending", "sending"),
|
||||||
|
("sent", "sent"),
|
||||||
|
("queued", "queued"),
|
||||||
|
("invalid", "invalid"),
|
||||||
|
("rejected", "rejected"),
|
||||||
|
("failed", "failed"),
|
||||||
|
("unknown", "unknown"),
|
||||||
|
],
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
Whitespace-only changes.
Executable
+117
@@ -0,0 +1,117 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from model_utils import Choices
|
||||||
|
from polymorphic.base import PolymorphicModelBase
|
||||||
|
from polymorphic.managers import PolymorphicManager
|
||||||
|
from polymorphic.models import PolymorphicModel
|
||||||
|
from polymorphic.query import PolymorphicQuerySet
|
||||||
|
from rules.contrib.models import RulesModelBaseMixin, RulesModelMixin
|
||||||
|
|
||||||
|
from outbo.apps.core.fields import (
|
||||||
|
RemovalDateTimeField,
|
||||||
|
CreationDateTimeField,
|
||||||
|
ModificationDateTimeField,
|
||||||
|
)
|
||||||
|
from outbo.apps.core.utils import get_now
|
||||||
|
|
||||||
|
EmailLogStatus = Choices("sending", "sent", "queued", "invalid", "rejected", "failed", "unknown")
|
||||||
|
|
||||||
|
|
||||||
|
class EmailLog(models.Model):
|
||||||
|
sender = models.CharField(max_length=254, blank=False, null=False)
|
||||||
|
receiver = models.CharField(max_length=254, blank=False, null=False)
|
||||||
|
time = models.DateTimeField(blank=False, null=False)
|
||||||
|
status = models.CharField(max_length=20, blank=False, null=False, choices=EmailLogStatus)
|
||||||
|
|
||||||
|
|
||||||
|
if "anymail" in settings.INSTALLED_APPS:
|
||||||
|
from anymail.signals import pre_send, post_send
|
||||||
|
from anymail.message import AnymailStatus
|
||||||
|
from django.core.mail import EmailMessage
|
||||||
|
|
||||||
|
def log_email_message(sender: str, receiver: str, time: datetime, status: str):
|
||||||
|
try:
|
||||||
|
EmailLog.objects.create(sender=sender, receiver=receiver, time=time, status=status)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@receiver(pre_send)
|
||||||
|
def handle_pre_send(sender, message: EmailMessage, **kwargs):
|
||||||
|
log_email_message(
|
||||||
|
sender=str(message.from_email),
|
||||||
|
receiver=", ".join(message.to),
|
||||||
|
time=get_now(),
|
||||||
|
status=EmailLogStatus.sending,
|
||||||
|
)
|
||||||
|
|
||||||
|
@receiver(post_send)
|
||||||
|
def handle_post_send(sender, message: EmailMessage, status: AnymailStatus, esp_name, **kwargs):
|
||||||
|
for email, recipient_status in status.recipients.items():
|
||||||
|
log_email_message(
|
||||||
|
sender=str(message.from_email),
|
||||||
|
receiver=email,
|
||||||
|
time=get_now(),
|
||||||
|
status=recipient_status.status or "failed",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SoftDeletableQuerySet(models.QuerySet):
|
||||||
|
def delete(self):
|
||||||
|
return super().update(deleted_at=get_now())
|
||||||
|
|
||||||
|
def hard_delete(self):
|
||||||
|
return super().delete()
|
||||||
|
|
||||||
|
|
||||||
|
class SoftDeletableManager(models.Manager):
|
||||||
|
queryset_class = SoftDeletableQuerySet
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.alive_only = kwargs.pop("alive_only", True)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
if self.alive_only:
|
||||||
|
return self.queryset_class(self.model).filter(deleted_at=None)
|
||||||
|
return self.queryset_class(self.model)
|
||||||
|
|
||||||
|
|
||||||
|
class TimestampableDeletableMixin(models.Model):
|
||||||
|
objects = SoftDeletableManager()
|
||||||
|
all_objects = SoftDeletableManager(alive_only=False)
|
||||||
|
|
||||||
|
created_at = CreationDateTimeField(_("Created at"))
|
||||||
|
updated_at = ModificationDateTimeField(_("Updated at"))
|
||||||
|
deleted_at = RemovalDateTimeField(_("Deleted at"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def delete(self, using=None, **kwargs):
|
||||||
|
self.deleted_at = get_now()
|
||||||
|
# TODO: handle autodeletion of nested stuff
|
||||||
|
self.save(using=using)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return super().__str__() if self.deleted_at is None else f"{super().__str__()} (deleted)"
|
||||||
|
|
||||||
|
|
||||||
|
class RulesPolymorphicModelBase(RulesModelBaseMixin, PolymorphicModelBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RulesPolymorphicModel(RulesModelMixin, PolymorphicModel, metaclass=RulesPolymorphicModelBase):
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class SoftDeletablePolymorphicQuerySet(SoftDeletableQuerySet, PolymorphicQuerySet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SoftDeletablePolymorphicManager(SoftDeletableManager, PolymorphicManager):
|
||||||
|
queryset_class = SoftDeletablePolymorphicQuerySet
|
||||||
Executable
+26
@@ -0,0 +1,26 @@
|
|||||||
|
from djangorestframework_camel_case.parser import CamelCaseMultiPartParser
|
||||||
|
from formencode.variabledecode import variable_decode
|
||||||
|
from rest_framework import parsers
|
||||||
|
from rest_framework.parsers import MultiPartParser
|
||||||
|
|
||||||
|
|
||||||
|
class NestedMultiPartParser(MultiPartParser):
|
||||||
|
def parse(self, stream, media_type=None, parser_context=None):
|
||||||
|
result = super().parse(
|
||||||
|
stream=stream, media_type=media_type, parser_context=parser_context
|
||||||
|
)
|
||||||
|
data = variable_decode(result.data)
|
||||||
|
files = variable_decode(result.files)
|
||||||
|
|
||||||
|
return parsers.DataAndFiles(data, files)
|
||||||
|
|
||||||
|
|
||||||
|
class CamelCaseNestedMultiPartParser(CamelCaseMultiPartParser):
|
||||||
|
def parse(self, stream, media_type=None, parser_context=None):
|
||||||
|
result = super().parse(
|
||||||
|
stream=stream, media_type=media_type, parser_context=parser_context
|
||||||
|
)
|
||||||
|
data = variable_decode(result.data)
|
||||||
|
files = variable_decode(result.files)
|
||||||
|
|
||||||
|
return parsers.DataAndFiles(data, files)
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
from rules import predicate
|
||||||
|
|
||||||
|
|
||||||
|
@predicate
|
||||||
|
def is_agency(user):
|
||||||
|
return user.is_authenticated and user.is_agency
|
||||||
|
|
||||||
|
|
||||||
|
@predicate
|
||||||
|
def is_client(user):
|
||||||
|
return user.is_authenticated and user.is_client
|
||||||
|
|
||||||
|
|
||||||
|
@predicate
|
||||||
|
def is_supplier(user):
|
||||||
|
return user.is_authenticated and user.is_supplier
|
||||||
Executable
+39
@@ -0,0 +1,39 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
from rest_polymorphic.serializers import (
|
||||||
|
PolymorphicSerializer as BasePolymorphicSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PolymorphicSerializer(BasePolymorphicSerializer):
|
||||||
|
resource_type_field_name = "type"
|
||||||
|
|
||||||
|
def to_resource_type(self, model_or_instance):
|
||||||
|
return model_or_instance._meta.verbose_name
|
||||||
|
|
||||||
|
|
||||||
|
class EnumCharField(serializers.ChoiceField):
|
||||||
|
default_error_messages = {"invalid_choice": _('"{input}" is not a valid choice.')}
|
||||||
|
|
||||||
|
def __init__(self, enum, **kwargs):
|
||||||
|
self.enum = enum
|
||||||
|
kwargs.setdefault("choices", [(item, item.value) for item in enum])
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.enum = enum
|
||||||
|
|
||||||
|
def to_internal_value(self, value):
|
||||||
|
value = super().to_representation(value)
|
||||||
|
if not isinstance(value, self.enum):
|
||||||
|
try:
|
||||||
|
enum_name, enum_value = value.split(".", 1)
|
||||||
|
if enum_name == self.enum.__name__:
|
||||||
|
value = self.enum[enum_value]
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError(self.error_messages["invalid"], code="invalid")
|
||||||
|
return value
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
if isinstance(value, self.enum):
|
||||||
|
return str(value)
|
||||||
|
return value
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
from storages.backends.s3boto3 import S3Boto3Storage
|
||||||
|
from storages.utils import setting
|
||||||
|
|
||||||
|
|
||||||
|
class S3StaticStorage(S3Boto3Storage):
|
||||||
|
bucket_name = setting("AWS_STORAGE_BUCKET_STATIC_NAME")
|
||||||
|
default_acl = "public-read"
|
||||||
|
querystring_auth = False
|
||||||
|
|
||||||
|
|
||||||
|
class S3MediaStorage(S3Boto3Storage):
|
||||||
|
bucket_name = setting("AWS_STORAGE_BUCKET_MEDIA_NAME")
|
||||||
|
default_acl = None
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from outbo.apps.core.views import RootApiView
|
||||||
|
from outbo.apps.products.views import ExportBriefToPDFApiView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", RootApiView.as_view(), name="root"),
|
||||||
|
path("briefs/<int:pk>/export-to-pdf/", ExportBriefToPDFApiView.as_view(), name="export_brief_to_pdf"),
|
||||||
|
]
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
def get_now():
|
||||||
|
return timezone.now()
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_date():
|
||||||
|
return timezone.localtime(get_now()).date()
|
||||||
|
|
||||||
|
|
||||||
|
def is_empty_set(tested_set):
|
||||||
|
return tested_set == set()
|
||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from rest_framework import permissions, views, response
|
||||||
|
|
||||||
|
|
||||||
|
class RootApiView(views.APIView):
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
print(request)
|
||||||
|
return response.Response({"version": settings.APP_VERSION})
|
||||||
Executable
+160
@@ -0,0 +1,160 @@
|
|||||||
|
import six
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import Model
|
||||||
|
from rest_framework import status, mixins
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.settings import api_settings
|
||||||
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
from rest_framework_extensions.mixins import (
|
||||||
|
NestedViewSetMixin as BaseNestedViewSetMixin,
|
||||||
|
)
|
||||||
|
from rest_framework_extensions.settings import extensions_api_settings
|
||||||
|
from rules.contrib.rest_framework import (
|
||||||
|
AutoPermissionViewSetMixin as BaseAutoPermissionViewSetMixin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AutoPermissionViewSetMixin(BaseAutoPermissionViewSetMixin):
|
||||||
|
permission_type_map = {
|
||||||
|
**BaseAutoPermissionViewSetMixin.permission_type_map,
|
||||||
|
"metadata": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CreateModelMixin:
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
if isinstance(request.data, list):
|
||||||
|
with transaction.atomic():
|
||||||
|
response = []
|
||||||
|
for item in request.data:
|
||||||
|
write_serializer = self.get_serializer(data=item)
|
||||||
|
write_serializer.is_valid(raise_exception=True)
|
||||||
|
instance = self.perform_create(serializer=write_serializer)
|
||||||
|
read_serializer = self.get_read_serializer(instance=instance)
|
||||||
|
response.append(read_serializer.data)
|
||||||
|
return Response(response, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
else:
|
||||||
|
write_serializer = self.get_serializer(data=request.data)
|
||||||
|
write_serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
instance = self.perform_create(serializer=write_serializer)
|
||||||
|
|
||||||
|
read_serializer = self.get_read_serializer(instance=instance)
|
||||||
|
headers = self.get_success_headers(read_serializer.data)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
read_serializer.data, status=status.HTTP_201_CREATED, headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
def perform_create(self, *, serializer):
|
||||||
|
return serializer.save()
|
||||||
|
|
||||||
|
def get_success_headers(self, data):
|
||||||
|
try:
|
||||||
|
return {"Location": str(data[api_settings.URL_FIELD_NAME])}
|
||||||
|
except (TypeError, KeyError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateModelMixin:
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
write_serializer = self.get_serializer(
|
||||||
|
instance, data=request.data, partial=False
|
||||||
|
)
|
||||||
|
write_serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
instance = write_serializer.save()
|
||||||
|
|
||||||
|
if getattr(instance, "_prefetched_objects_cache", None):
|
||||||
|
# If 'prefetch_related' has been applied to a queryset, we need to
|
||||||
|
# forcibly invalidate the prefetch cache on the instance.
|
||||||
|
instance._prefetched_objects_cache = {}
|
||||||
|
|
||||||
|
read_serializer = self.get_read_serializer(instance=instance)
|
||||||
|
|
||||||
|
return Response(read_serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class PartialUpdateModelMixin:
|
||||||
|
def partial_update(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
write_serializer = self.get_serializer(
|
||||||
|
instance, data=request.data, partial=True
|
||||||
|
)
|
||||||
|
write_serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
instance = write_serializer.save()
|
||||||
|
|
||||||
|
if getattr(instance, "_prefetched_objects_cache", None):
|
||||||
|
# If 'prefetch_related' has been applied to a queryset, we need to
|
||||||
|
# forcibly invalidate the prefetch cache on the instance.
|
||||||
|
instance._prefetched_objects_cache = {}
|
||||||
|
|
||||||
|
read_serializer = self.get_read_serializer(instance=instance)
|
||||||
|
|
||||||
|
return Response(read_serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class RetrieveModelMixin(mixins.RetrieveModelMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ListModelMixin(mixins.ListModelMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DestroyModelMixin(mixins.DestroyModelMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleSerializerViewSet(GenericViewSet):
|
||||||
|
serializer_class_set = None
|
||||||
|
serializer_map = {
|
||||||
|
"list": "List",
|
||||||
|
"retrieve": "Retrieve",
|
||||||
|
"create": "Create",
|
||||||
|
"partial_update": "Update",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
assert (
|
||||||
|
self.serializer_class_set is not None
|
||||||
|
), "serializer_class_set should be defined"
|
||||||
|
|
||||||
|
if self.action not in self.serializer_map:
|
||||||
|
raise KeyError(
|
||||||
|
f"""serializer_map does not contain serializer for "{self.action}" method"""
|
||||||
|
)
|
||||||
|
return getattr(self.serializer_class_set, self.serializer_map[self.action])
|
||||||
|
|
||||||
|
def get_read_serializer(self, *args, **kwargs):
|
||||||
|
assert (
|
||||||
|
self.serializer_class_set is not None
|
||||||
|
), "serializer_class_set should be defined"
|
||||||
|
if self.action != "create":
|
||||||
|
if getattr(self.serializer_class_set, "Retrieve", None) is None:
|
||||||
|
raise KeyError(
|
||||||
|
f"""serializer_class_set does not contain "Retrieve" serializer"""
|
||||||
|
)
|
||||||
|
serializer_class = self.serializer_class_set.Retrieve
|
||||||
|
else:
|
||||||
|
if (
|
||||||
|
getattr(self.serializer_class_set, "Retrieve", None) is None
|
||||||
|
and getattr(self.serializer_class_set, "AfterCreate", None) is None
|
||||||
|
):
|
||||||
|
raise KeyError(
|
||||||
|
f"""serializer_class_set does not contain "Retrieve" nor "AfterCreate" serializer and is used in "create" action."""
|
||||||
|
)
|
||||||
|
if getattr(self.serializer_class_set, "AfterCreate", None) is not None:
|
||||||
|
serializer_class = self.serializer_class_set.AfterCreate
|
||||||
|
else:
|
||||||
|
serializer_class = self.serializer_class_set.Retrieve
|
||||||
|
|
||||||
|
kwargs["context"] = self.get_serializer_context()
|
||||||
|
return serializer_class(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class NestedViewSetMixin(BaseNestedViewSetMixin):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class FinalPhotobriefingAppConfig(AppConfig):
|
||||||
|
name = "outbo.apps.final_photobriefing"
|
||||||
|
label = "final_photobriefing"
|
||||||
|
verbose_name = _("Final photo briefing")
|
||||||
|
|
||||||
|
|
||||||
|
default_app_config = "outbo.apps.final_photobriefing.FinalPhotobriefingAppConfig"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class PhotoType(Enum):
|
||||||
|
INVALID = "<invalid>"
|
||||||
|
ARRANGEMENT_PHOTOS = "arrangement"
|
||||||
|
CLIPPING = "clipping"
|
||||||
|
PACKSHOT = "packshot"
|
||||||
|
EXTRA_PHOTO = "extraPhoto"
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Generated by Django 2.2.8 on 2019-12-18 13:54
|
||||||
|
|
||||||
|
import rules.contrib.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import outbo.apps.core.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="FinalBrief",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
outbo.apps.core.fields.CreationDateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="Created at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
outbo.apps.core.fields.ModificationDateTimeField(
|
||||||
|
auto_now=True, verbose_name="Updated at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"deleted_at",
|
||||||
|
outbo.apps.core.fields.RemovalDateTimeField(
|
||||||
|
blank=True, editable=False, null=True, verbose_name="Deleted at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"abstract": False,},
|
||||||
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
]
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
# Generated by Django 2.2.8 on 2019-12-18 14:03
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("final_photobriefing", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(model_name="finalbrief", name="id",),
|
||||||
|
]
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 2.2.8 on 2019-12-18 14:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("photobriefing", "0001_initial"),
|
||||||
|
("final_photobriefing", "0002_remove_finalbrief_id"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="finalbrief",
|
||||||
|
name="brief",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
default=0,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="photobriefing.Brief",
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 2.2.8 on 2019-12-18 14:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("final_photobriefing", "0003_finalbrief_brief"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="finalbrief",
|
||||||
|
name="brief",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="photobriefing.Brief",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 2.2.8 on 2019-12-18 14:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("final_photobriefing", "0004_auto_20191218_1505"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="finalbrief",
|
||||||
|
name="brief",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
primary_key=True,
|
||||||
|
related_name="final",
|
||||||
|
related_query_name="final",
|
||||||
|
serialize=False,
|
||||||
|
to="photobriefing.Brief",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
# Generated by Django 2.2.8 on 2019-12-19 08:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import outbo.apps.core.fields
|
||||||
|
import outbo.apps.final_photobriefing.enums
|
||||||
|
import outbo.apps.final_photobriefing.models
|
||||||
|
import rules.contrib.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("final_photobriefing", "0005_auto_20191218_1506"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="FinalBriefPhoto",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
outbo.apps.core.fields.CreationDateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="Created at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
outbo.apps.core.fields.ModificationDateTimeField(
|
||||||
|
auto_now=True, verbose_name="Updated at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"deleted_at",
|
||||||
|
outbo.apps.core.fields.RemovalDateTimeField(
|
||||||
|
blank=True, editable=False, null=True, verbose_name="Deleted at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"file",
|
||||||
|
models.FileField(
|
||||||
|
upload_to=outbo.apps.final_photobriefing.models.FormatFinalPhotoBriefPhotoFilename()
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"category",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
(
|
||||||
|
outbo.apps.final_photobriefing.enums.PhotoType(
|
||||||
|
"<invalid>"
|
||||||
|
),
|
||||||
|
"<invalid>",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
outbo.apps.final_photobriefing.enums.PhotoType(
|
||||||
|
"arrangement"
|
||||||
|
),
|
||||||
|
"arrangement",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
outbo.apps.final_photobriefing.enums.PhotoType(
|
||||||
|
"clipping"
|
||||||
|
),
|
||||||
|
"clipping",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
outbo.apps.final_photobriefing.enums.PhotoType(
|
||||||
|
"packshot"
|
||||||
|
),
|
||||||
|
"packshot",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
outbo.apps.final_photobriefing.enums.PhotoType(
|
||||||
|
"extraPhoto"
|
||||||
|
),
|
||||||
|
"extraPhoto",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
max_length=30,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"brief",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="final_photobriefing.FinalBrief",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
]
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
# Generated by Django 2.2.8 on 2019-12-20 10:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import outbo.apps.final_photobriefing.enums
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("final_photobriefing", "0006_finalbriefphoto"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(model_name="finalbriefphoto", name="category",),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="finalbriefphoto",
|
||||||
|
name="type",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
(
|
||||||
|
outbo.apps.final_photobriefing.enums.PhotoType("<invalid>"),
|
||||||
|
"<invalid>",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
outbo.apps.final_photobriefing.enums.PhotoType("arrangement"),
|
||||||
|
"arrangement",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
outbo.apps.final_photobriefing.enums.PhotoType("clipping"),
|
||||||
|
"clipping",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
outbo.apps.final_photobriefing.enums.PhotoType("packshot"),
|
||||||
|
"packshot",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
outbo.apps.final_photobriefing.enums.PhotoType("extraPhoto"),
|
||||||
|
"extraPhoto",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
default=outbo.apps.final_photobriefing.enums.PhotoType("<invalid>"),
|
||||||
|
max_length=30,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="finalbriefphoto",
|
||||||
|
name="brief",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="photos",
|
||||||
|
related_query_name="photo",
|
||||||
|
to="final_photobriefing.FinalBrief",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 2.2.10 on 2020-05-25 08:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("final_photobriefing", "0007_auto_20191220_1114"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="finalbrief",
|
||||||
|
name="brief",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
primary_key=True,
|
||||||
|
related_name="final",
|
||||||
|
related_query_name="final",
|
||||||
|
serialize=False,
|
||||||
|
to="photobriefing.Brief",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
Whitespace-only changes.
+63
@@ -0,0 +1,63 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.deconstruct import deconstructible
|
||||||
|
from rules.contrib.models import RulesModel
|
||||||
|
|
||||||
|
from outbo.apps.core.models import TimestampableDeletableMixin
|
||||||
|
from outbo.apps.final_photobriefing import rules
|
||||||
|
from outbo.apps.final_photobriefing.enums import PhotoType
|
||||||
|
|
||||||
|
|
||||||
|
@deconstructible
|
||||||
|
class FormatFinalPhotoBriefPhotoFilename:
|
||||||
|
def __call__(self, instance, filename):
|
||||||
|
obj = instance.brief.brief
|
||||||
|
return "/".join(
|
||||||
|
("projects", obj.product.project.slug[1:], "final-photobriefing", filename)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FinalBrief(TimestampableDeletableMixin, RulesModel):
|
||||||
|
class Meta:
|
||||||
|
rules_permissions = {
|
||||||
|
"add": rules.can_create_final_brief,
|
||||||
|
"view": rules.can_read_final_brief,
|
||||||
|
"change": rules.can_update_final_brief,
|
||||||
|
"delete": rules.can_delete_final_brief,
|
||||||
|
}
|
||||||
|
|
||||||
|
brief = models.OneToOneField(
|
||||||
|
to="photobriefing.Brief",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="final",
|
||||||
|
related_query_name="final",
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FinalBriefPhoto(TimestampableDeletableMixin, RulesModel):
|
||||||
|
class Meta:
|
||||||
|
rules_permissions = {
|
||||||
|
"add": rules.can_create_final_brief_photo,
|
||||||
|
"view": rules.can_read_final_brief_photo,
|
||||||
|
"change": rules.can_update_final_brief_photo,
|
||||||
|
"delete": rules.can_delete_final_brief_photo,
|
||||||
|
}
|
||||||
|
|
||||||
|
brief = models.ForeignKey(
|
||||||
|
to=FinalBrief,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_query_name="photo",
|
||||||
|
related_name="photos",
|
||||||
|
)
|
||||||
|
file = models.FileField(upload_to=FormatFinalPhotoBriefPhotoFilename())
|
||||||
|
type = models.CharField(
|
||||||
|
max_length=30,
|
||||||
|
choices=[(type, type.value) for type in PhotoType],
|
||||||
|
default=PhotoType.INVALID,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filename(self):
|
||||||
|
return os.path.basename(self.file.name)
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
from rules import always_deny, always_allow
|
||||||
|
|
||||||
|
from outbo.apps.core.rules import is_agency
|
||||||
|
|
||||||
|
can_create_final_brief = always_deny
|
||||||
|
can_read_final_brief = always_allow
|
||||||
|
can_update_final_brief = is_agency
|
||||||
|
can_delete_final_brief = always_deny
|
||||||
|
|
||||||
|
can_create_final_brief_photo = always_deny
|
||||||
|
can_read_final_brief_photo = always_allow
|
||||||
|
can_update_final_brief_photo = is_agency
|
||||||
|
can_delete_final_brief_photo = is_agency
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
from rest_framework_extensions.routers import ExtendedSimpleRouter
|
||||||
|
|
||||||
|
from outbo.apps.final_photobriefing.viewsets import (
|
||||||
|
FinalBriefViewSet,
|
||||||
|
FinalBriefPhotoViewSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
app_name = "final_photobriefing"
|
||||||
|
router = ExtendedSimpleRouter()
|
||||||
|
router.register(prefix="final-photobriefing", viewset=FinalBriefViewSet)
|
||||||
|
router.register(prefix="final-photobriefing-photo", viewset=FinalBriefPhotoViewSet)
|
||||||
|
|
||||||
|
urlpatterns = router.urls
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
|
|
||||||
|
from outbo.apps.core.viewsets import (
|
||||||
|
AutoPermissionViewSetMixin,
|
||||||
|
RetrieveModelMixin,
|
||||||
|
MultipleSerializerViewSet,
|
||||||
|
PartialUpdateModelMixin,
|
||||||
|
DestroyModelMixin,
|
||||||
|
)
|
||||||
|
from outbo.apps.final_photobriefing.enums import PhotoType
|
||||||
|
from outbo.apps.final_photobriefing.models import FinalBrief, FinalBriefPhoto
|
||||||
|
|
||||||
|
|
||||||
|
class FinalBriefPhotoSerializer:
|
||||||
|
class List(serializers.ModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(
|
||||||
|
view_name="final_photobriefing:finalbriefphoto-detail"
|
||||||
|
)
|
||||||
|
type = serializers.SerializerMethodField()
|
||||||
|
filename = serializers.ReadOnlyField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = FinalBriefPhoto
|
||||||
|
fields = ["url", "type", "filename"]
|
||||||
|
|
||||||
|
def get_type(self, instance):
|
||||||
|
t, val = instance.type.split(".", 1)
|
||||||
|
return PhotoType[val].value
|
||||||
|
|
||||||
|
class Create(serializers.ModelSerializer):
|
||||||
|
type = serializers.CharField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = FinalBriefPhoto
|
||||||
|
fields = ["type", "file"]
|
||||||
|
|
||||||
|
def validate_type(self, value):
|
||||||
|
try:
|
||||||
|
return PhotoType(value)
|
||||||
|
except ValueError:
|
||||||
|
raise serializers.ValidationError("Invalid photo type value")
|
||||||
|
|
||||||
|
|
||||||
|
class BaseFinalBriefSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = FinalBrief
|
||||||
|
|
||||||
|
|
||||||
|
class FinalBriefSerializer:
|
||||||
|
class Retrieve(BaseFinalBriefSerializer):
|
||||||
|
photos = FinalBriefPhotoSerializer.List(many=True)
|
||||||
|
|
||||||
|
class Meta(BaseFinalBriefSerializer.Meta):
|
||||||
|
fields = ["photos", "created_at", "updated_at"]
|
||||||
|
|
||||||
|
class Update(BaseFinalBriefSerializer):
|
||||||
|
photos = FinalBriefPhotoSerializer.Create(many=True)
|
||||||
|
|
||||||
|
class Meta(BaseFinalBriefSerializer.Meta):
|
||||||
|
fields = ["photos"]
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
photos = validated_data.pop("photos", None)
|
||||||
|
if photos is not None:
|
||||||
|
for photo in photos:
|
||||||
|
FinalBriefPhoto.objects.create(brief=instance, **photo)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class FinalBriefViewSet(
|
||||||
|
AutoPermissionViewSetMixin,
|
||||||
|
RetrieveModelMixin,
|
||||||
|
PartialUpdateModelMixin,
|
||||||
|
MultipleSerializerViewSet,
|
||||||
|
):
|
||||||
|
queryset = FinalBrief.objects.all()
|
||||||
|
serializer_class_set = FinalBriefSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class FinalBriefPhotoViewSet(
|
||||||
|
AutoPermissionViewSetMixin,
|
||||||
|
RetrieveModelMixin,
|
||||||
|
DestroyModelMixin,
|
||||||
|
MultipleSerializerViewSet,
|
||||||
|
):
|
||||||
|
queryset = FinalBriefPhoto.objects.all()
|
||||||
|
serializer_class_set = FinalBriefPhotoSerializer
|
||||||
|
retrieve_permission_classes = [AllowAny]
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.action == "retrieve":
|
||||||
|
return [permission() for permission in self.retrieve_permission_classes]
|
||||||
|
return super().get_permissions()
|
||||||
|
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
photo = self.get_object()
|
||||||
|
return HttpResponseRedirect(redirect_to=photo.file.url)
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationsAppConfig(AppConfig):
|
||||||
|
name = "outbo.apps.organizations"
|
||||||
|
label = "organizations"
|
||||||
|
verbose_name = _("Organizations")
|
||||||
|
|
||||||
|
|
||||||
|
default_app_config = "outbo.apps.organizations.OrganizationsAppConfig"
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from polymorphic.admin import (
|
||||||
|
PolymorphicChildModelAdmin,
|
||||||
|
PolymorphicChildModelFilter,
|
||||||
|
)
|
||||||
|
|
||||||
|
from outbo.apps.core.admin import PolymorphicParentModelAdmin
|
||||||
|
from outbo.apps.organizations.models import (
|
||||||
|
Organization,
|
||||||
|
AgencyOrganization,
|
||||||
|
ClientOrganization,
|
||||||
|
SupplierOrganization,
|
||||||
|
Brand,
|
||||||
|
Category,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _OrganizationChildAdmin(PolymorphicChildModelAdmin):
|
||||||
|
base_model = Organization
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(AgencyOrganization)
|
||||||
|
class AgencyOrganizationAdmin(_OrganizationChildAdmin):
|
||||||
|
base_model = AgencyOrganization
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ClientOrganization)
|
||||||
|
class ClientOrganizationAdmin(_OrganizationChildAdmin):
|
||||||
|
base_model = ClientOrganization
|
||||||
|
search_fields = ["name"]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(SupplierOrganization)
|
||||||
|
class SupplierOrganizationAdmin(_OrganizationChildAdmin):
|
||||||
|
base_model = SupplierOrganization
|
||||||
|
search_fields = ["name"]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Organization)
|
||||||
|
class OrganizationAdmin(PolymorphicParentModelAdmin):
|
||||||
|
base_model = Organization
|
||||||
|
child_models = (AgencyOrganization, ClientOrganization, SupplierOrganization)
|
||||||
|
list_filter = (PolymorphicChildModelFilter,)
|
||||||
|
search_fields = ["name"]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Brand)
|
||||||
|
class BrandAdmin(admin.ModelAdmin):
|
||||||
|
autocomplete_fields = ["client", "suppliers", "categories"]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Category)
|
||||||
|
class CategoryAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ["name"]
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: https://stackoverflow.com/questions/9879687/adding-a-manytomanywidget-to-the-reverse-of-a-manytomanyfield-in-the-django-admi/13189954
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
# Generated by Django 2.2.7 on 2019-11-29 08:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import outbo.apps.core.fields
|
||||||
|
import rules.contrib.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("contenttypes", "0002_remove_content_type_name"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Category",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
outbo.apps.core.fields.CreationDateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="Created at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
outbo.apps.core.fields.ModificationDateTimeField(
|
||||||
|
auto_now=True, verbose_name="Updated at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"deleted_at",
|
||||||
|
outbo.apps.core.fields.RemovalDateTimeField(
|
||||||
|
blank=True, editable=False, null=True, verbose_name="Deleted at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=254, verbose_name="name")),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "category",
|
||||||
|
"verbose_name_plural": "categories",
|
||||||
|
"ordering": ["name"],
|
||||||
|
},
|
||||||
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Organization",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
outbo.apps.core.fields.CreationDateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="Created at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
outbo.apps.core.fields.ModificationDateTimeField(
|
||||||
|
auto_now=True, verbose_name="Updated at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"deleted_at",
|
||||||
|
outbo.apps.core.fields.RemovalDateTimeField(
|
||||||
|
blank=True, editable=False, null=True, verbose_name="Deleted at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=100)),
|
||||||
|
(
|
||||||
|
"polymorphic_ctype",
|
||||||
|
models.ForeignKey(
|
||||||
|
editable=False,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="polymorphic_organizations.organization_set+",
|
||||||
|
to="contenttypes.ContentType",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"abstract": False,},
|
||||||
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="AgencyOrganization",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"organization_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="organizations.Organization",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "agency", "verbose_name_plural": "agencies",},
|
||||||
|
bases=("organizations.organization",),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ClientOrganization",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"organization_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="organizations.Organization",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("uses_simple_project_flow", models.BooleanField(default=False)),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "client", "verbose_name_plural": "clients",},
|
||||||
|
bases=("organizations.organization",),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="SupplierOrganization",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"organization_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="organizations.Organization",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "supplier", "verbose_name_plural": "suppliers",},
|
||||||
|
bases=("organizations.organization",),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Brand",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
outbo.apps.core.fields.CreationDateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="Created at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
outbo.apps.core.fields.ModificationDateTimeField(
|
||||||
|
auto_now=True, verbose_name="Updated at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"deleted_at",
|
||||||
|
outbo.apps.core.fields.RemovalDateTimeField(
|
||||||
|
blank=True, editable=False, null=True, verbose_name="Deleted at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=50, verbose_name="name")),
|
||||||
|
(
|
||||||
|
"categories",
|
||||||
|
models.ManyToManyField(
|
||||||
|
related_name="brands",
|
||||||
|
related_query_name="brand",
|
||||||
|
to="organizations.Category",
|
||||||
|
verbose_name="categories",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"client",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="brands",
|
||||||
|
related_query_name="brand",
|
||||||
|
to="organizations.ClientOrganization",
|
||||||
|
verbose_name="client",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"suppliers",
|
||||||
|
models.ManyToManyField(
|
||||||
|
related_name="brands",
|
||||||
|
related_query_name="brand",
|
||||||
|
to="organizations.SupplierOrganization",
|
||||||
|
verbose_name="suppliers",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "brand", "verbose_name_plural": "brands",},
|
||||||
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
]
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 2.2.10 on 2020-05-25 08:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("organizations", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="brand",
|
||||||
|
name="client",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="brands",
|
||||||
|
related_query_name="brand",
|
||||||
|
to="organizations.ClientOrganization",
|
||||||
|
verbose_name="client",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
Whitespace-only changes.
@@ -0,0 +1,8 @@
|
|||||||
|
from .organization import (
|
||||||
|
Organization,
|
||||||
|
AgencyOrganization,
|
||||||
|
ClientOrganization,
|
||||||
|
SupplierOrganization,
|
||||||
|
)
|
||||||
|
from .brand import Brand
|
||||||
|
from .category import Category
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.db import models
|
||||||
|
from rules.contrib.models import RulesModel
|
||||||
|
|
||||||
|
from outbo.apps.core.models import TimestampableDeletableMixin
|
||||||
|
from outbo.apps.organizations import rules
|
||||||
|
|
||||||
|
|
||||||
|
class Brand(TimestampableDeletableMixin, RulesModel):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("brand")
|
||||||
|
verbose_name_plural = _("brands")
|
||||||
|
rules_permissions = {"read": rules.can_read_brand}
|
||||||
|
|
||||||
|
name = models.CharField(verbose_name=_("name"), max_length=50)
|
||||||
|
client = models.ForeignKey(
|
||||||
|
verbose_name=_("client"),
|
||||||
|
to="ClientOrganization",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="brands",
|
||||||
|
related_query_name="brand",
|
||||||
|
null=False,
|
||||||
|
)
|
||||||
|
suppliers = models.ManyToManyField(
|
||||||
|
verbose_name=_("suppliers"),
|
||||||
|
to="SupplierOrganization",
|
||||||
|
related_name="brands",
|
||||||
|
related_query_name="brand",
|
||||||
|
)
|
||||||
|
categories = models.ManyToManyField(
|
||||||
|
verbose_name=_("categories"),
|
||||||
|
to="Category",
|
||||||
|
related_name="brands",
|
||||||
|
related_query_name="brand",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rules.contrib.models import RulesModel
|
||||||
|
|
||||||
|
from outbo.apps.core.models import TimestampableDeletableMixin
|
||||||
|
|
||||||
|
|
||||||
|
class Category(TimestampableDeletableMixin, RulesModel):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("category")
|
||||||
|
verbose_name_plural = _("categories")
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
name = models.CharField(verbose_name=_("name"), max_length=254)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from outbo.apps.core.models import (
|
||||||
|
RulesPolymorphicModel,
|
||||||
|
TimestampableDeletableMixin,
|
||||||
|
SoftDeletablePolymorphicManager,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Organization(TimestampableDeletableMixin, RulesPolymorphicModel):
|
||||||
|
objects = SoftDeletablePolymorphicManager()
|
||||||
|
all_objects = SoftDeletablePolymorphicManager(alive_only=False)
|
||||||
|
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"[{str(self.polymorphic_ctype).title()}] {self.name}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self):
|
||||||
|
return self.polymorphic_ctype
|
||||||
|
|
||||||
|
@type.setter
|
||||||
|
def type(self, value):
|
||||||
|
self.polymorphic_ctype = value
|
||||||
|
|
||||||
|
@type.deleter
|
||||||
|
def type(self):
|
||||||
|
del self.polymorphic_ctype
|
||||||
|
|
||||||
|
|
||||||
|
class AgencyOrganization(Organization):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("agency")
|
||||||
|
verbose_name_plural = _("agencies")
|
||||||
|
|
||||||
|
|
||||||
|
class ClientOrganization(Organization):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("client")
|
||||||
|
verbose_name_plural = _("clients")
|
||||||
|
|
||||||
|
uses_simple_project_flow = models.BooleanField(null=False, default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierOrganization(Organization):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("supplier")
|
||||||
|
verbose_name_plural = _("suppliers")
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
from rules import always_deny, predicate
|
||||||
|
|
||||||
|
|
||||||
|
@predicate
|
||||||
|
def can_read_brand(user, brand):
|
||||||
|
return always_deny
|
||||||
|
|
||||||
|
|
||||||
|
@predicate
|
||||||
|
def can_read_category(user, category):
|
||||||
|
return always_deny
|
||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from outbo.apps.core.serializers import PolymorphicSerializer
|
||||||
|
from outbo.apps.organizations.models import (
|
||||||
|
AgencyOrganization,
|
||||||
|
ClientOrganization,
|
||||||
|
SupplierOrganization,
|
||||||
|
Brand,
|
||||||
|
Category,
|
||||||
|
Organization,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Organizations
|
||||||
|
class _BaseOrganizationSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
fields = (
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _OrganizationSerializer:
|
||||||
|
class List(_BaseOrganizationSerializer):
|
||||||
|
class Meta(_BaseOrganizationSerializer.Meta):
|
||||||
|
model = Organization
|
||||||
|
|
||||||
|
|
||||||
|
class _AgencyOrganizationSerializer:
|
||||||
|
class List(_BaseOrganizationSerializer):
|
||||||
|
class Meta(_BaseOrganizationSerializer.Meta):
|
||||||
|
model = AgencyOrganization
|
||||||
|
|
||||||
|
|
||||||
|
class _ClientOrganizationSerializer:
|
||||||
|
class List(_BaseOrganizationSerializer):
|
||||||
|
class Meta(_BaseOrganizationSerializer.Meta):
|
||||||
|
model = ClientOrganization
|
||||||
|
|
||||||
|
|
||||||
|
class _SupplierOrganizationSerializer:
|
||||||
|
class List(_BaseOrganizationSerializer):
|
||||||
|
class Meta(_BaseOrganizationSerializer.Meta):
|
||||||
|
model = SupplierOrganization
|
||||||
|
|
||||||
|
|
||||||
|
class _BaseOrganizationPolymorphicSerializer(PolymorphicSerializer):
|
||||||
|
model_serializer_mapping = {
|
||||||
|
AgencyOrganization: _AgencyOrganizationSerializer.List,
|
||||||
|
ClientOrganization: _ClientOrganizationSerializer.List,
|
||||||
|
SupplierOrganization: _SupplierOrganizationSerializer.List,
|
||||||
|
Organization: _OrganizationSerializer.List,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationSerializer:
|
||||||
|
class Retrieve(_BaseOrganizationPolymorphicSerializer):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class List(_BaseOrganizationPolymorphicSerializer):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Brands
|
||||||
|
class _BaseBrandSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Brand
|
||||||
|
fields = ["name"]
|
||||||
|
|
||||||
|
|
||||||
|
class BrandSerializer(serializers.ModelSerializer):
|
||||||
|
# Create & Update not needed
|
||||||
|
class Retrieve(_BaseBrandSerializer):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class List(_BaseBrandSerializer):
|
||||||
|
class Meta(_BaseBrandSerializer.Meta):
|
||||||
|
fields = ["id"] + _BaseBrandSerializer.Meta.fields
|
||||||
|
|
||||||
|
|
||||||
|
# Categories
|
||||||
|
class _BaseCategorySerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Category
|
||||||
|
fields = ["name"]
|
||||||
|
|
||||||
|
|
||||||
|
class CategorySerializer:
|
||||||
|
class Retrieve(_BaseCategorySerializer):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class List(_BaseCategorySerializer):
|
||||||
|
class Meta(_BaseCategorySerializer.Meta):
|
||||||
|
fields = ["id"] + _BaseCategorySerializer.Meta.fields
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
from rest_framework import routers
|
||||||
|
|
||||||
|
from outbo.apps.organizations.viewsets import BrandViewSet, CategoryViewSet
|
||||||
|
|
||||||
|
app_name = "organizations"
|
||||||
|
router = routers.SimpleRouter()
|
||||||
|
router.register(prefix="brands", viewset=BrandViewSet)
|
||||||
|
router.register(prefix="categories", viewset=CategoryViewSet)
|
||||||
|
|
||||||
|
urlpatterns = router.urls
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
from outbo.apps.core.viewsets import (
|
||||||
|
MultipleSerializerViewSet,
|
||||||
|
AutoPermissionViewSetMixin,
|
||||||
|
ListModelMixin,
|
||||||
|
RetrieveModelMixin,
|
||||||
|
)
|
||||||
|
from outbo.apps.organizations.models import Brand, Category
|
||||||
|
from outbo.apps.organizations.serializers import BrandSerializer, CategorySerializer
|
||||||
|
|
||||||
|
|
||||||
|
class BrandViewSet(
|
||||||
|
RetrieveModelMixin,
|
||||||
|
ListModelMixin,
|
||||||
|
AutoPermissionViewSetMixin,
|
||||||
|
MultipleSerializerViewSet,
|
||||||
|
):
|
||||||
|
queryset = Brand.objects.all()
|
||||||
|
serializer_class_set = BrandSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryViewSet(
|
||||||
|
RetrieveModelMixin,
|
||||||
|
ListModelMixin,
|
||||||
|
AutoPermissionViewSetMixin,
|
||||||
|
MultipleSerializerViewSet,
|
||||||
|
):
|
||||||
|
|
||||||
|
queryset = Category.objects.all()
|
||||||
|
serializer_class_set = CategorySerializer
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PhotobriefingAppConfig(AppConfig):
|
||||||
|
name = "outbo.apps.photobriefing"
|
||||||
|
label = "photobriefing"
|
||||||
|
verbose_name = _("Photo briefing")
|
||||||
|
|
||||||
|
|
||||||
|
default_app_config = "outbo.apps.photobriefing.PhotobriefingAppConfig"
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from nested_admin import nested
|
||||||
|
|
||||||
|
from outbo.apps.photobriefing.models import Brief, BriefItem, BriefItemPhoto, Category
|
||||||
|
|
||||||
|
|
||||||
|
class BriefItemPhotoInline(nested.NestedStackedInline):
|
||||||
|
model = BriefItemPhoto
|
||||||
|
|
||||||
|
|
||||||
|
class BriefItemInline(nested.NestedStackedInline):
|
||||||
|
model = BriefItem
|
||||||
|
inlines = [BriefItemPhotoInline]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Brief)
|
||||||
|
class BriefAdmin(nested.NestedModelAdmin):
|
||||||
|
inlines = [BriefItemInline]
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Category)
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class ChoiceRole(Enum):
|
||||||
|
INVALID = "<invalid>"
|
||||||
|
SUPPLIER = "supplier"
|
||||||
|
CLIENT = "client"
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
# Generated by Django 2.2.7 on 2019-11-29 08:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django_fsm
|
||||||
|
import outbo.apps.core.fields
|
||||||
|
import outbo.apps.core.utils
|
||||||
|
import outbo.apps.photobriefing.enums
|
||||||
|
import outbo.apps.photobriefing.models.brief_item_photo
|
||||||
|
import rules.contrib.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("products", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Brief",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
outbo.apps.core.fields.CreationDateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="Created at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
outbo.apps.core.fields.ModificationDateTimeField(
|
||||||
|
auto_now=True, verbose_name="Updated at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"deleted_at",
|
||||||
|
outbo.apps.core.fields.RemovalDateTimeField(
|
||||||
|
blank=True, editable=False, null=True, verbose_name="Deleted at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"product",
|
||||||
|
models.OneToOneField(
|
||||||
|
editable=False,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
primary_key=True,
|
||||||
|
related_name="photo_brief",
|
||||||
|
related_query_name="photo_brief",
|
||||||
|
serialize=False,
|
||||||
|
to="products.Product",
|
||||||
|
verbose_name="product",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"session_date",
|
||||||
|
models.DateField(
|
||||||
|
default=outbo.apps.core.utils.get_current_date,
|
||||||
|
verbose_name="photo session date",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("concept_description", models.TextField(blank=True, default="")),
|
||||||
|
(
|
||||||
|
"stage",
|
||||||
|
django_fsm.FSMField(
|
||||||
|
default="created", max_length=50, protected=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"abstract": False,},
|
||||||
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="BriefItem",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
outbo.apps.core.fields.CreationDateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="Created at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
outbo.apps.core.fields.ModificationDateTimeField(
|
||||||
|
auto_now=True, verbose_name="Updated at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"deleted_at",
|
||||||
|
outbo.apps.core.fields.RemovalDateTimeField(
|
||||||
|
blank=True, editable=False, null=True, verbose_name="Deleted at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("title", models.CharField(max_length=100)),
|
||||||
|
("description", models.TextField()),
|
||||||
|
(
|
||||||
|
"brief",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="items",
|
||||||
|
related_query_name="item",
|
||||||
|
to="photobriefing.Brief",
|
||||||
|
verbose_name="brief",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"ordering": ["id"],},
|
||||||
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Category",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
outbo.apps.core.fields.CreationDateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="Created at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
outbo.apps.core.fields.ModificationDateTimeField(
|
||||||
|
auto_now=True, verbose_name="Updated at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"deleted_at",
|
||||||
|
outbo.apps.core.fields.RemovalDateTimeField(
|
||||||
|
blank=True, editable=False, null=True, verbose_name="Deleted at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"label",
|
||||||
|
models.CharField(
|
||||||
|
max_length=30,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="label",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"description",
|
||||||
|
models.CharField(max_length=30, verbose_name="description"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "category", "verbose_name_plural": "categories",},
|
||||||
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="BriefItemPhoto",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
outbo.apps.core.fields.CreationDateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="Created at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
outbo.apps.core.fields.ModificationDateTimeField(
|
||||||
|
auto_now=True, verbose_name="Updated at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"deleted_at",
|
||||||
|
outbo.apps.core.fields.RemovalDateTimeField(
|
||||||
|
blank=True, editable=False, null=True, verbose_name="Deleted at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"file",
|
||||||
|
models.ImageField(
|
||||||
|
upload_to=outbo.apps.photobriefing.models.brief_item_photo.filename,
|
||||||
|
verbose_name="file",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"item",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="photos",
|
||||||
|
related_query_name="photo",
|
||||||
|
to="photobriefing.BriefItem",
|
||||||
|
verbose_name="brief item",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"abstract": False,},
|
||||||
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="briefitem",
|
||||||
|
name="category",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="photobriefing.Category",
|
||||||
|
verbose_name="category",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="BriefItemChoice",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
outbo.apps.core.fields.CreationDateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="Created at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
outbo.apps.core.fields.ModificationDateTimeField(
|
||||||
|
auto_now=True, verbose_name="Updated at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"deleted_at",
|
||||||
|
outbo.apps.core.fields.RemovalDateTimeField(
|
||||||
|
blank=True, editable=False, null=True, verbose_name="Deleted at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"role",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
(
|
||||||
|
outbo.apps.photobriefing.enums.ChoiceRole("<invalid>"),
|
||||||
|
"<invalid>",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
outbo.apps.photobriefing.enums.ChoiceRole("supplier"),
|
||||||
|
"supplier",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
outbo.apps.photobriefing.enums.ChoiceRole("client"),
|
||||||
|
"client",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
default=outbo.apps.photobriefing.enums.ChoiceRole("<invalid>"),
|
||||||
|
editable=False,
|
||||||
|
max_length=30,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"brief",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="choices",
|
||||||
|
related_query_name="choice",
|
||||||
|
to="photobriefing.Brief",
|
||||||
|
verbose_name="brief",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"category",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="choices+",
|
||||||
|
related_query_name="choice",
|
||||||
|
to="photobriefing.Category",
|
||||||
|
verbose_name="category",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"item",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="choices",
|
||||||
|
related_query_name="choice",
|
||||||
|
to="photobriefing.BriefItem",
|
||||||
|
verbose_name="item",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"unique_together": {("brief", "category", "role")},},
|
||||||
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
]
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.8 on 2019-12-18 14:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("photobriefing", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="brief",
|
||||||
|
name="concept_description",
|
||||||
|
field=models.TextField(blank=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
Loaded 100 of 454 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user