Initial commit

This commit is contained in:
Alexander Karpov 2023-08-26 01:17:47 +03:00
commit 2cc1736951
134 changed files with 7430 additions and 0 deletions

10
.dockerignore Normal file
View File

@ -0,0 +1,10 @@
orconfig
.gitattributes
.github
.gitignore
.gitlab-ci.yml
.idea
.pre-commit-config.yaml
.readthedocs.yml
.travis.yml
venv

27
.editorconfig Normal file
View File

@ -0,0 +1,27 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{py,rst,ini}]
indent_style = space
indent_size = 4
[*.{html,css,scss,json,yml,xml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab
[nginx.conf]
indent_style = space
indent_size = 2

5
.env Normal file
View File

@ -0,0 +1,5 @@
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/pitch_deck_generator
CELERY_BROKER_URL=redis://localhost:6379/0
REDIS_URL=redis://localhost:6379/1
USE_DOCKER=no
OPENAI_KEY=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

5
.env.template Normal file
View File

@ -0,0 +1,5 @@
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/pitch_deck_generator
CELERY_BROKER_URL=redis://localhost:6379/0
REDIS_URL=redis://localhost:6379/1
USE_DOCKER=no
OPENAI_KEY=

25
.gitattributes vendored Normal file
View File

@ -0,0 +1,25 @@
# Source files
# ============
*.pxd text diff=python
*.py text diff=python
*.py3 text diff=python
*.pyw text diff=python
*.pyx text diff=python
*.pyz text diff=python
*.pyi text diff=python
# Binary files
# ============
*.db binary
*.p binary
*.pkl binary
*.pickle binary
*.pyc binary export-ignore
*.pyo binary export-ignore
*.pyd binary
# Jupyter notebook
*.ipynb text eol=lf
# ignore static
pitch_deck_generator/static/** linguist-vendored

80
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,80 @@
# Config for Dependabot updates. See Documentation here:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
# Update GitHub actions in workflows
- package-ecosystem: "github-actions"
directory: "/"
# Check for updates to GitHub Actions every weekday
schedule:
interval: "daily"
# Enable version updates for Docker
# We need to specify each Dockerfile in a separate entry because Dependabot doesn't
# support wildcards or recursively checking subdirectories. Check this issue for updates:
# https://github.com/dependabot/dependabot-core/issues/2178
- package-ecosystem: "docker"
# Look for a `Dockerfile` in the `compose/local/django` directory
directory: "compose/local/django/"
# Check for updates to GitHub Actions every weekday
schedule:
interval: "daily"
# Enable version updates for Docker
- package-ecosystem: "docker"
# Look for a `Dockerfile` in the `compose/local/docs` directory
directory: "compose/local/docs/"
# Check for updates to GitHub Actions every weekday
schedule:
interval: "daily"
# Enable version updates for Docker
- package-ecosystem: "docker"
# Look for a `Dockerfile` in the `compose/local/node` directory
directory: "compose/local/node/"
# Check for updates to GitHub Actions every weekday
schedule:
interval: "daily"
# Enable version updates for Docker
- package-ecosystem: "docker"
# Look for a `Dockerfile` in the `compose/production/aws` directory
directory: "compose/production/aws/"
# Check for updates to GitHub Actions every weekday
schedule:
interval: "daily"
# Enable version updates for Docker
- package-ecosystem: "docker"
# Look for a `Dockerfile` in the `compose/production/django` directory
directory: "compose/production/django/"
# Check for updates to GitHub Actions every weekday
schedule:
interval: "daily"
# Enable version updates for Docker
- package-ecosystem: "docker"
# Look for a `Dockerfile` in the `compose/production/postgres` directory
directory: "compose/production/postgres/"
# Check for updates to GitHub Actions every weekday
schedule:
interval: "daily"
# Enable version updates for Docker
- package-ecosystem: "docker"
# Look for a `Dockerfile` in the `compose/production/traefik` directory
directory: "compose/production/traefik/"
# Check for updates to GitHub Actions every weekday
schedule:
interval: "daily"
# Enable version updates for Python/Pip - Production
- package-ecosystem: "pip"
# Look for a `requirements.txt` in the `root` directory
# also 'setup.cfg', 'runtime.txt' and 'requirements/*.txt'
directory: "/"
# Check for updates to GitHub Actions every weekday
schedule:
interval: "daily"

57
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,57 @@
name: CI
# Enable Buildkit and let compose use it to speed up image building
env:
DOCKER_BUILDKIT: 1
COMPOSE_DOCKER_CLI_BUILD: 1
on:
pull_request:
branches: [ "master", "main" ]
paths-ignore: [ "docs/**" ]
push:
branches: [ "master", "main" ]
paths-ignore: [ "docs/**" ]
concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
linter:
runs-on: ubuntu-latest
steps:
- name: Checkout Code Repository
uses: actions/checkout@v3
- uses: actions/checkout@v3
- name: Install poetry
run: pipx install poetry
- uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'poetry'
- run: poetry install
- name: Run pre-commit
uses: pre-commit/action@v2.0.3
# With no caching at all the entire ci process takes 4m 30s to complete!
pytest:
runs-on: ubuntu-latest
steps:
- name: Checkout Code Repository
uses: actions/checkout@v3
- name: Build the Stack
run: docker-compose -f local.yml build
- name: Run Django Tests
run: docker-compose -f local.yml run django pytest
- name: Tear down the Stack
run: docker-compose -f local.yml down

333
.gitignore vendored Normal file
View File

@ -0,0 +1,333 @@
!.gitkeep
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# 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/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
staticfiles/
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# Environments
.venv
venv/
ENV/
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
### Linux template
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### VisualStudioCode template
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Provided default Pycharm Run/Debug Configurations should be tracked by git
# In case of local modifications made by Pycharm, use update-index command
# for each changed file, like this:
# git update-index --assume-unchanged .idea/pitch_deck_generator.iml
### JetBrains template
# 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/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# 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
### Windows template
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
### macOS template
# General
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### SublimeText template
# Cache files for Sublime Text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# Workspace files are user-specific
*.sublime-workspace
# Project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using Sublime Text
# *.sublime-project
# SFTP configuration file
sftp-config.json
# Package control specific files
Package Control.last-run
Package Control.ca-list
Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
Package Control.merged-ca-bundle
Package Control.user-ca-bundle
oscrypto-ca-bundle.crt
bh_unicode_properties.cache
# Sublime-github package stores a github token in this file
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings
### Vim template
# Swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-v][a-z]
[._]sw[a-p]
# Session
Session.vim
# Temporary
.netrwhist
# Auto-generated tag files
tags
### Project template
pitch_deck_generator/media/
.pytest_cache/
.ipython/

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,24 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PyMethodMayBeStaticInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="9">
<item index="0" class="java.lang.String" itemvalue="pytest-instafail" />
<item index="1" class="java.lang.String" itemvalue="cookiecutter" />
<item index="2" class="java.lang.String" itemvalue="binaryornot" />
<item index="3" class="java.lang.String" itemvalue="isort" />
<item index="4" class="java.lang.String" itemvalue="PyGithub" />
<item index="5" class="java.lang.String" itemvalue="pytest-cookies" />
<item index="6" class="java.lang.String" itemvalue="gitpython" />
<item index="7" class="java.lang.String" itemvalue="tox" />
<item index="8" class="java.lang.String" itemvalue="django" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
.idea/misc.xml Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Poetry (pitch_deck_generator)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/pitch_deck_generator.iml" filepath="$PROJECT_DIR$/.idea/pitch_deck_generator.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$" />
<option name="settingsModule" value="config/settings/local.py" />
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
</component>
</module>

39
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,39 @@
exclude: "^docs/|/migrations/"
default_stages: [commit]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.1
hooks:
- id: pyupgrade
args: [--py310-plus]
- repo: https://github.com/psf/black
rev: 22.12.0
hooks:
- id: black
- repo: https://github.com/PyCQA/isort
rev: 5.11.4
hooks:
- id: isort
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
- id: flake8
args: ["--config=setup.cfg"]
additional_dependencies: [flake8-isort]
# sets up .pre-commit-ci.yaml to ensure pre-commit dependencies stay up to date
ci:
autoupdate_schedule: weekly
skip: []
submodules: false

14
.pylintrc Normal file
View File

@ -0,0 +1,14 @@
[MASTER]
load-plugins=pylint_django, pylint_celery
django-settings-module=config.settings.local
[FORMAT]
max-line-length=120
[MESSAGES CONTROL]
disable=missing-docstring,invalid-name
[DESIGN]
max-parents=13
[TYPECHECK]
generated-members=REQUEST,acl_users,aq_parent,"[a-zA-Z]+_set{1,2}",save,delete

40
README.md Normal file
View File

@ -0,0 +1,40 @@
# Pitch Deck Generator
## Basic Commands
### Runserver
$ ./manage.py runserver_plus
### Type checks
Running type checks with mypy:
$ mypy pitch_deck_generator
#### Running tests with pytest
$ pytest
### Setting Up Your Users
- To create a **superuser account**, use this command:
$ python manage.py createsuperuser
### Celery
This app comes with Celery.
To run a celery worker:
``` bash
cd pitch_deck_generator
celery -A config.celery_app worker -l info
```
Please note: For Celery's import magic to work, it is important *where* the celery commands are run. If you are in the same folder with *manage.py*, you should be right.
made with [cookiecutter-django](https://github.com/Alexander-D-Karpov/cookiecutter-django)

View File

@ -0,0 +1,79 @@
ARG PYTHON_VERSION=3.11-slim
# define an alias for the specfic python version used in this file.
FROM python:${PYTHON_VERSION} as python
# Python build stage
FROM python as python-build-stage
ARG BUILD_ENVIRONMENT=local
# Install apt packages
RUN apt-get update && apt-get install --no-install-recommends -y \
# dependencies for building Python packages
build-essential \
# psycopg2 dependencies
libpq-dev
# Python 'run' stage
FROM python as python-run-stage
ARG BUILD_ENVIRONMENT=local
ARG APP_HOME=/app
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
ENV BUILD_ENV ${BUILD_ENVIRONMENT}
WORKDIR ${APP_HOME}
# Install required system dependencies
RUN apt-get update && apt-get install --no-install-recommends -y \
# psycopg2 dependencies
libpq-dev \
# Translations dependencies
gettext \
# cleaning up unused files
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
&& rm -rf /var/lib/apt/lists/*
RUN pip install poetry
# Configuring poetry
RUN poetry config virtualenvs.create false
COPY pyproject.toml poetry.lock /
# Installing requirements
RUN poetry install
COPY ./compose/production/django/entrypoint /entrypoint
RUN sed -i 's/\r$//g' /entrypoint
RUN chmod +x /entrypoint
COPY ./compose/production/django/manage /manage
RUN sed -i 's/\r$//g' /manage
RUN chmod +x /manage
COPY ./compose/local/django/start /start
RUN sed -i 's/\r$//g' /start
RUN chmod +x /start
COPY ./compose/local/django/celery/worker/start /start-celeryworker
RUN sed -i 's/\r$//g' /start-celeryworker
RUN chmod +x /start-celeryworker
COPY ./compose/local/django/celery/beat/start /start-celerybeat
RUN sed -i 's/\r$//g' /start-celerybeat
RUN chmod +x /start-celerybeat
COPY ./compose/local/django/celery/flower/start /start-flower
RUN sed -i 's/\r$//g' /start-flower
RUN chmod +x /start-flower
# copy application code to WORKDIR
COPY . ${APP_HOME}
ENTRYPOINT ["/entrypoint"]

View File

@ -0,0 +1,8 @@
#!/bin/bash
set -o errexit
set -o nounset
rm -f './celerybeat.pid'
exec watchfiles celery.__main__.main --args '-A config.celery_app beat -l INFO'

View File

@ -0,0 +1,8 @@
#!/bin/bash
set -o errexit
set -o nounset
exec watchfiles celery.__main__.main \
--args \
"-A config.celery_app -b \"${CELERY_BROKER_URL}\" flower --basic_auth=\"${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}\""

View File

@ -0,0 +1,7 @@
#!/bin/bash
set -o errexit
set -o nounset
exec watchfiles celery.__main__.main --args '-A config.celery_app worker -l INFO'

View File

@ -0,0 +1,9 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
python manage.py migrate
exec python manage.py runserver_plus 0.0.0.0:8000

View File

@ -0,0 +1,86 @@
ARG PYTHON_VERSION=3.11-slim
# define an alias for the specfic python version used in this file.
FROM python:${PYTHON_VERSION} as python
# Python build stage
FROM python as python-build-stage
ARG BUILD_ENVIRONMENT=production
# Install apt packages
RUN apt-get update && apt-get install --no-install-recommends -y \
# dependencies for building Python packages
build-essential \
# psycopg2 dependencies
libpq-dev
# Python 'run' stage
FROM python as python-run-stage
ARG BUILD_ENVIRONMENT=production
ARG APP_HOME=/app
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
ENV BUILD_ENV ${BUILD_ENVIRONMENT}
WORKDIR ${APP_HOME}
# Install required system dependencies
RUN apt-get update && apt-get install --no-install-recommends -y \
# psycopg2 dependencies
libpq-dev \
# Translations dependencies
gettext \
# cleaning up unused files
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
&& rm -rf /var/lib/apt/lists/*
RUN addgroup --system django \
&& adduser --system --ingroup django django
RUN pip install poetry
# Configuring poetry
RUN poetry config virtualenvs.create false
COPY pyproject.toml poetry.lock /
# Installing requirements
RUN poetry install
COPY ./compose/production/django/entrypoint /entrypoint
RUN sed -i 's/\r$//g' /entrypoint
RUN chmod +x /entrypoint
COPY ./compose/production/django/manage /manage
RUN sed -i 's/\r$//g' /manage
RUN chmod +x /manage
COPY ./compose/local/django/start /start
RUN sed -i 's/\r$//g' /start
RUN chmod +x /start
COPY ./compose/local/django/celery/worker/start /start-celeryworker
RUN sed -i 's/\r$//g' /start-celeryworker
RUN chmod +x /start-celeryworker
COPY ./compose/local/django/celery/beat/start /start-celerybeat
RUN sed -i 's/\r$//g' /start-celerybeat
RUN chmod +x /start-celerybeat
COPY ./compose/local/django/celery/flower/start /start-flower
RUN sed -i 's/\r$//g' /start-flower
RUN chmod +x /start-flower
# copy application code to WORKDIR
COPY --chown=django:django . ${APP_HOME}
# make django owner of the WORKDIR directory as well.
RUN chown django:django ${APP_HOME}
USER django

View File

@ -0,0 +1,8 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
exec celery -A config.celery_app beat -l INFO

View File

@ -0,0 +1,11 @@
#!/bin/bash
set -o errexit
set -o nounset
exec celery \
-A config.celery_app \
-b "${CELERY_BROKER_URL}" \
flower \
--basic_auth="${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}"

View File

@ -0,0 +1,8 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
exec celery -A config.celery_app worker -l INFO

View File

@ -0,0 +1,49 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
# N.B. If only .env files supported variable expansion...
export CELERY_BROKER_URL="${REDIS_URL}"
if [ -z "${POSTGRES_USER}" ]; then
base_postgres_image_default_user='postgres'
export POSTGRES_USER="${base_postgres_image_default_user}"
fi
export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
python << END
import sys
import time
import psycopg2
suggest_unrecoverable_after = 30
start = time.time()
while True:
try:
psycopg2.connect(
dbname="${POSTGRES_DB}",
user="${POSTGRES_USER}",
password="${POSTGRES_PASSWORD}",
host="${POSTGRES_HOST}",
port="${POSTGRES_PORT}",
)
break
except psycopg2.OperationalError as error:
sys.stderr.write("Waiting for PostgreSQL to become available...\n")
if time.time() - start > suggest_unrecoverable_after:
sys.stderr.write(" This is taking longer than expected. The following exception may be indicative of an unrecoverable error: '{}'\n".format(error))
time.sleep(1)
END
>&2 echo 'PostgreSQL is available'
exec "$@"

View File

@ -0,0 +1,14 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
if [ -z "${POSTGRES_USER}" ]; then
base_postgres_image_default_user='postgres'
export POSTGRES_USER="${base_postgres_image_default_user}"
fi
export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
exec /venv/bin/python /app/manage.py "$@"

View File

@ -0,0 +1,9 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
python /app/manage.py collectstatic --noinput
exec /usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:5000 --chdir=/app

View File

@ -0,0 +1,6 @@
FROM postgres:14
COPY ./compose/production/postgres/maintenance /usr/local/bin/maintenance
RUN chmod +x /usr/local/bin/maintenance/*
RUN mv /usr/local/bin/maintenance/* /usr/local/bin \
&& rmdir /usr/local/bin/maintenance

View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
BACKUP_DIR_PATH='/backups'
BACKUP_FILE_PREFIX='backup'

View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
countdown() {
declare desc="A simple countdown. Source: https://superuser.com/a/611582"
local seconds="${1}"
local d=$(($(date +%s) + "${seconds}"))
while [ "$d" -ge `date +%s` ]; do
echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r";
sleep 0.1
done
}

View File

@ -0,0 +1,41 @@
#!/usr/bin/env bash
message_newline() {
echo
}
message_debug()
{
echo -e "DEBUG: ${@}"
}
message_welcome()
{
echo -e "\e[1m${@}\e[0m"
}
message_warning()
{
echo -e "\e[33mWARNING\e[0m: ${@}"
}
message_error()
{
echo -e "\e[31mERROR\e[0m: ${@}"
}
message_info()
{
echo -e "\e[37mINFO\e[0m: ${@}"
}
message_suggestion()
{
echo -e "\e[33mSUGGESTION\e[0m: ${@}"
}
message_success()
{
echo -e "\e[32mSUCCESS\e[0m: ${@}"
}

View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
yes_no() {
declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message."
local arg1="${1}"
local response=
read -r -p "${arg1} (y/[n])? " response
if [[ "${response}" =~ ^[Yy]$ ]]
then
exit 0
else
exit 1
fi
}

View File

@ -0,0 +1,38 @@
#!/usr/bin/env bash
### Create a database backup.
###
### Usage:
### $ docker-compose -f <environment>.yml (exec |run --rm) postgres backup
set -o errexit
set -o pipefail
set -o nounset
working_dir="$(dirname ${0})"
source "${working_dir}/_sourced/constants.sh"
source "${working_dir}/_sourced/messages.sh"
message_welcome "Backing up the '${POSTGRES_DB}' database..."
if [[ "${POSTGRES_USER}" == "postgres" ]]; then
message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again."
exit 1
fi
export PGHOST="${POSTGRES_HOST}"
export PGPORT="${POSTGRES_PORT}"
export PGUSER="${POSTGRES_USER}"
export PGPASSWORD="${POSTGRES_PASSWORD}"
export PGDATABASE="${POSTGRES_DB}"
backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz"
pg_dump | gzip > "${BACKUP_DIR_PATH}/${backup_filename}"
message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'."

View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
### View backups.
###
### Usage:
### $ docker-compose -f <environment>.yml (exec |run --rm) postgres backups
set -o errexit
set -o pipefail
set -o nounset
working_dir="$(dirname ${0})"
source "${working_dir}/_sourced/constants.sh"
source "${working_dir}/_sourced/messages.sh"
message_welcome "These are the backups you have got:"
ls -lht "${BACKUP_DIR_PATH}"

View File

@ -0,0 +1,55 @@
#!/usr/bin/env bash
### Restore database from a backup.
###
### Parameters:
### <1> filename of an existing backup.
###
### Usage:
### $ docker-compose -f <environment>.yml (exec |run --rm) postgres restore <1>
set -o errexit
set -o pipefail
set -o nounset
working_dir="$(dirname ${0})"
source "${working_dir}/_sourced/constants.sh"
source "${working_dir}/_sourced/messages.sh"
if [[ -z ${1+x} ]]; then
message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again."
exit 1
fi
backup_filename="${BACKUP_DIR_PATH}/${1}"
if [[ ! -f "${backup_filename}" ]]; then
message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again."
exit 1
fi
message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..."
if [[ "${POSTGRES_USER}" == "postgres" ]]; then
message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again."
exit 1
fi
export PGHOST="${POSTGRES_HOST}"
export PGPORT="${POSTGRES_PORT}"
export PGUSER="${POSTGRES_USER}"
export PGPASSWORD="${POSTGRES_PASSWORD}"
export PGDATABASE="${POSTGRES_DB}"
message_info "Dropping the database..."
dropdb "${PGDATABASE}"
message_info "Creating a new database..."
createdb --owner="${POSTGRES_USER}"
message_info "Applying the backup to the new database..."
gunzip -c "${backup_filename}" | psql "${POSTGRES_DB}"
message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup."

View File

@ -0,0 +1,5 @@
FROM traefik:v2.2.11
RUN mkdir -p /etc/traefik/acme \
&& touch /etc/traefik/acme/acme.json \
&& chmod 600 /etc/traefik/acme/acme.json
COPY ./compose/production/traefik/traefik.yml /etc/traefik

View File

@ -0,0 +1,75 @@
log:
level: INFO
entryPoints:
web:
# http
address: ":80"
http:
# https://docs.traefik.io/routing/entrypoints/#entrypoint
redirections:
entryPoint:
to: web-secure
web-secure:
# https
address: ":443"
flower:
address: ":5555"
certificatesResolvers:
letsencrypt:
# https://docs.traefik.io/master/https/acme/#lets-encrypt
acme:
email: "sanspie@akarpov.ru"
storage: /etc/traefik/acme/acme.json
# https://docs.traefik.io/master/https/acme/#httpchallenge
httpChallenge:
entryPoint: web
http:
routers:
web-secure-router:
rule: "Host(`akarpov.ru`) || Host(`www.akarpov.ru`)"
entryPoints:
- web-secure
middlewares:
- csrf
service: django
tls:
# https://docs.traefik.io/master/routing/routers/#certresolver
certResolver: letsencrypt
flower-secure-router:
rule: "Host(`akarpov.ru`)"
entryPoints:
- flower
service: flower
tls:
# https://docs.traefik.io/master/routing/routers/#certresolver
certResolver: letsencrypt
middlewares:
csrf:
# https://docs.traefik.io/master/middlewares/headers/#hostsproxyheaders
# https://docs.djangoproject.com/en/dev/ref/csrf/#ajax
headers:
hostsProxyHeaders: ["X-CSRFToken"]
services:
django:
loadBalancer:
servers:
- url: http://django:5000
flower:
loadBalancer:
servers:
- url: http://flower:5555
providers:
# https://docs.traefik.io/master/providers/file/
file:
filename: /etc/traefik/traefik.yml
watch: true

5
config/__init__.py Normal file
View File

@ -0,0 +1,5 @@
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery_app import app as celery_app
__all__ = ("celery_app",)

7
config/api_router.py Normal file
View File

@ -0,0 +1,7 @@
from django.urls import include, path
app_name = "api"
urlpatterns = [
path("ticket/", include("pitch_deck_generator.tickets.api.urls")),
path("decks/", include("pitch_deck_generator.decks.api.urls")),
]

17
config/celery_app.py Normal file
View File

@ -0,0 +1,17 @@
import os
from celery import Celery
# set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
app = Celery("pitch_deck_generator")
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object("django.conf:settings", namespace="CELERY")
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()

View File

354
config/settings/base.py Normal file
View File

@ -0,0 +1,354 @@
"""
Base settings to build other settings files upon.
"""
from pathlib import Path
import environ
import structlog
ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent
# pitch_deck_generator/
APPS_DIR = ROOT_DIR / "pitch_deck_generator"
env = environ.Env()
READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=True)
if READ_DOT_ENV_FILE:
# OS environment variables take precedence over variables from .env
env.read_env(str(ROOT_DIR / ".env"))
# GENERAL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
DEBUG = env.bool("DJANGO_DEBUG", False)
# Local time zone. Choices are
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# though not all of them may be available with every OS.
# In Windows, this must be set to your system time zone.
TIME_ZONE = "Europe/Moscow"
# https://docs.djangoproject.com/en/dev/ref/settings/#language-code
LANGUAGE_CODE = "en-us"
# https://docs.djangoproject.com/en/dev/ref/settings/#site-id
SITE_ID = 1
# https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n
USE_I18N = True
# https://docs.djangoproject.com/en/dev/ref/settings/#use-tz
USE_TZ = True
# https://docs.djangoproject.com/en/dev/ref/settings/#locale-paths
LOCALE_PATHS = [str(ROOT_DIR / "locale")]
# CACHES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#caches
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}
# DATABASES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASES = {"default": env.db("DATABASE_URL")}
DATABASES["default"]["ATOMIC_REQUESTS"] = True
# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DEFAULT_AUTO_FIELD
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# URLS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf
ROOT_URLCONF = "config.urls"
# https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application
WSGI_APPLICATION = "config.wsgi.application"
# APPS
# ------------------------------------------------------------------------------
DJANGO_APPS = [
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.sites",
"django.contrib.messages",
"django.contrib.staticfiles",
# "django.contrib.humanize", # Handy template tags
"django.contrib.admin",
"django.forms",
]
THIRD_PARTY_APPS = [
"django_celery_beat",
"rest_framework",
"rest_framework.authtoken",
"corsheaders",
"drf_spectacular",
]
LOCAL_APPS = [
"pitch_deck_generator.users",
"pitch_deck_generator.tickets",
"pitch_deck_generator.decks",
]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
# MIGRATIONS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#migration-modules
MIGRATION_MODULES = {"sites": "pitch_deck_generator.contrib.sites.migrations"}
# AUTHENTICATION
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
]
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model
AUTH_USER_MODEL = "users.User"
# PASSWORDS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
PASSWORD_HASHERS = [
# https://docs.djangoproject.com/en/dev/topics/auth/passwords/#using-argon2-with-django
"django.contrib.auth.hashers.Argon2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
]
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# MIDDLEWARE
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#middleware
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"corsheaders.middleware.CorsMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.common.BrokenLinkEmailsMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
# STATIC
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#static-root
STATIC_ROOT = str(ROOT_DIR / "staticfiles")
# https://docs.djangoproject.com/en/dev/ref/settings/#static-url
STATIC_URL = "/static/"
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
STATICFILES_DIRS = [str(APPS_DIR / "static")]
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
# MEDIA
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#media-root
MEDIA_ROOT = str(APPS_DIR / "media")
# https://docs.djangoproject.com/en/dev/ref/settings/#media-url
MEDIA_URL = "/media/"
# TEMPLATES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#templates
TEMPLATES = [
{
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND
"BACKEND": "django.template.backends.django.DjangoTemplates",
# https://docs.djangoproject.com/en/dev/ref/settings/#dirs
"DIRS": [str(APPS_DIR / "templates")],
# https://docs.djangoproject.com/en/dev/ref/settings/#app-dirs
"APP_DIRS": True,
"OPTIONS": {
# https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.template.context_processors.i18n",
"django.template.context_processors.media",
"django.template.context_processors.static",
"django.template.context_processors.tz",
"django.contrib.messages.context_processors.messages",
],
},
}
]
# https://docs.djangoproject.com/en/dev/ref/settings/#form-renderer
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
# FIXTURES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#fixture-dirs
FIXTURE_DIRS = (str(APPS_DIR / "fixtures"),)
# SECURITY
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly
SESSION_COOKIE_HTTPONLY = True
# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly
CSRF_COOKIE_HTTPONLY = True
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter
SECURE_BROWSER_XSS_FILTER = True
# https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options
X_FRAME_OPTIONS = "DENY"
# ADMIN
# ------------------------------------------------------------------------------
# Django Admin URL.
ADMIN_URL = "admin/"
# https://docs.djangoproject.com/en/dev/ref/settings/#admins
ADMINS = [("""sanspie""", "sanspie@akarpov.ru")]
# https://docs.djangoproject.com/en/dev/ref/settings/#managers
MANAGERS = ADMINS
# LOGGING
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#logging
# See https://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"json_formatter": {
"()": structlog.stdlib.ProcessorFormatter,
"processor": structlog.processors.JSONRenderer(),
},
"plain_console": {
"()": structlog.stdlib.ProcessorFormatter,
"processor": structlog.dev.ConsoleRenderer(),
},
"key_value": {
"()": structlog.stdlib.ProcessorFormatter,
"processor": structlog.processors.KeyValueRenderer(
key_order=["timestamp", "level", "event", "logger"]
),
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "plain_console",
},
"json_file": {
"class": "logging.handlers.WatchedFileHandler",
"filename": "logs/json.log",
"formatter": "json_formatter",
},
"flat_line_file": {
"class": "logging.handlers.WatchedFileHandler",
"filename": "logs/flat_line.log",
"formatter": "key_value",
},
},
"loggers": {
"django_structlog": {
"handlers": ["console", "flat_line_file", "json_file"],
"level": "INFO",
},
# Make sure to replace the following logger's name for yours
"django_structlog_demo_project": {
"handlers": ["console", "flat_line_file", "json_file"],
"level": "INFO",
},
},
}
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.stdlib.filter_by_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
# Celery
# ------------------------------------------------------------------------------
if USE_TZ:
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-timezone
CELERY_TIMEZONE = TIME_ZONE
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-broker_url
CELERY_BROKER_URL = env("CELERY_BROKER_URL")
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-result_backend
CELERY_RESULT_BACKEND = CELERY_BROKER_URL
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#result-extended
CELERY_RESULT_EXTENDED = True
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#result-backend-always-retry
# https://github.com/celery/celery/pull/6122
CELERY_RESULT_BACKEND_ALWAYS_RETRY = True
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#result-backend-max-retries
CELERY_RESULT_BACKEND_MAX_RETRIES = 10
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-accept_content
CELERY_ACCEPT_CONTENT = ["json"]
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-task_serializer
CELERY_TASK_SERIALIZER = "json"
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-result_serializer
CELERY_RESULT_SERIALIZER = "json"
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#task-time-limit
CELERY_TASK_TIME_LIMIT = 5 * 60
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#task-soft-time-limit
CELERY_TASK_SOFT_TIME_LIMIT = 60
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#beat-scheduler
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#worker-send-task-events
CELERY_WORKER_SEND_TASK_EVENTS = True
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std-setting-task_send_sent_event
CELERY_TASK_SEND_SENT_EVENT = True
# DRF
# -------------------------------------------------------------------------------
# django-rest-framework - https://www.django-rest-framework.org/api-guide/settings/
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.AllowAny",),
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
# django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup
CORS_URLS_REGEX = r"^/api/.*$"
# By Default swagger ui is available only to admin user(s). You can change permission classes to change that
# See more configuration options at https://drf-spectacular.readthedocs.io/en/latest/settings.html#settings
SPECTACULAR_SETTINGS = {
"TITLE": "Pitch Deck Generator API",
"DESCRIPTION": "Documentation of API endpoints of Pitch Deck Generator",
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False,
"SERVE_PERMISSIONS": [],
"SERVERS": [
{"url": "http://127.0.0.1:8000", "description": "Local Development server"},
{"url": "http://192.168.83.181:8000/", "description": "Production server"},
],
}
OPENAI_KEY = env("OPENAI_KEY")

58
config/settings/local.py Normal file
View File

@ -0,0 +1,58 @@
from .base import * # noqa
from .base import env
# GENERAL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
DEBUG = True
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
SECRET_KEY = env(
"DJANGO_SECRET_KEY",
default="WxQl8yLX66FwmZ0fEoW3Btmstd842e5lhoiJeQor5O42Vt2n0f3d8P5mdB15LF3a",
)
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = [
"localhost",
"0.0.0.0",
"127.0.0.1",
"192.168.83.181",
"ed68-77-234-219-9.ngrok-free.app",
]
CORS_ORIGIN_ALLOW_ALL = True
CSRF_TRUSTED_ORIGINS = ["https://*.ngrok-free.app"]
# WhiteNoise
# ------------------------------------------------------------------------------
# http://whitenoise.evans.io/en/latest/django.html#using-whitenoise-in-development
INSTALLED_APPS = ["whitenoise.runserver_nostatic"] + INSTALLED_APPS # noqa F405
# django-debug-toolbar
# ------------------------------------------------------------------------------
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites
INSTALLED_APPS += ["debug_toolbar"] # noqa F405
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405
# https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config
DEBUG_TOOLBAR_CONFIG = {
"DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"],
"SHOW_TEMPLATE_CONTEXT": True,
}
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips
INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"]
if env("USE_DOCKER") == "yes":
import socket
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
INTERNAL_IPS += [".".join(ip.split(".")[:-1] + ["1"]) for ip in ips]
# django-extensions
# ------------------------------------------------------------------------------
# https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration
INSTALLED_APPS += ["django_extensions"] # noqa F405
# Celery
# ------------------------------------------------------------------------------
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#task-eager-propagates
CELERY_TASK_EAGER_PROPAGATES = True
# Your stuff...
# ------------------------------------------------------------------------------

View File

@ -0,0 +1,115 @@
from .base import * # noqa
from .base import env
# GENERAL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
SECRET_KEY = env("DJANGO_SECRET_KEY")
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["akarpov.ru"])
# DATABASES
# ------------------------------------------------------------------------------
DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # noqa F405
# CACHES
# ------------------------------------------------------------------------------
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": env("REDIS_URL"),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
# Mimicing memcache behavior.
# https://github.com/jazzband/django-redis#memcached-exceptions-behavior
"IGNORE_EXCEPTIONS": True,
},
}
}
# SECURITY
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-ssl-redirect
SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True)
# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-secure
SESSION_COOKIE_SECURE = True
# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-secure
CSRF_COOKIE_SECURE = True
# https://docs.djangoproject.com/en/dev/topics/security/#ssl-https
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-seconds
SECURE_HSTS_SECONDS = 518400
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains
SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool(
"DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True
)
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload
SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True)
# https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff
SECURE_CONTENT_TYPE_NOSNIFF = env.bool(
"DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True
)
# STATIC
# ------------------------
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# MEDIA
# ------------------------------------------------------------------------------
# ADMIN
# ------------------------------------------------------------------------------
# Django Admin URL regex.
ADMIN_URL = env("DJANGO_ADMIN_URL")
# LOGGING
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#logging
# See https://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error when DEBUG=False.
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}},
"formatters": {
"verbose": {
"format": "%(levelname)s %(asctime)s %(module)s "
"%(process)d %(thread)d %(message)s"
}
},
"handlers": {
"mail_admins": {
"level": "ERROR",
"filters": ["require_debug_false"],
"class": "django.utils.log.AdminEmailHandler",
},
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "verbose",
},
},
"root": {"level": "INFO", "handlers": ["console"]},
"loggers": {
"django.request": {
"handlers": ["mail_admins"],
"level": "ERROR",
"propagate": True,
},
"django.security.DisallowedHost": {
"level": "ERROR",
"handlers": ["console", "mail_admins"],
"propagate": True,
},
},
}
# django-rest-framework
# -------------------------------------------------------------------------------
# Tools that generate code samples can use SERVERS to point to the correct domain
SPECTACULAR_SETTINGS["SERVERS"] = [ # noqa F405
{"url": "https://akarpov.ru", "description": "Production server"}
]

33
config/settings/test.py Normal file
View File

@ -0,0 +1,33 @@
"""
With these settings, tests run faster.
"""
from .base import * # noqa
from .base import env
# GENERAL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
SECRET_KEY = env(
"DJANGO_SECRET_KEY",
default="jbaoCUK8SfX5IW78hhkbeuR0CsZTCZSFeaKU3LXXN4ghpPq30iKgvtNbV06OoDeM",
)
# https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
TEST_RUNNER = "django.test.runner.DiscoverRunner"
# PASSWORDS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
# EMAIL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
# DEBUGGING FOR TEMPLATES
# ------------------------------------------------------------------------------
TEMPLATES[0]["OPTIONS"]["debug"] = True # type: ignore # noqa F405
# Your stuff...
# ------------------------------------------------------------------------------

36
config/urls.py Normal file
View File

@ -0,0 +1,36 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from django.views import defaults as default_views
from django.views.generic import TemplateView
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns = [
# Django Admin, use {% url 'admin:index' %}
path(settings.ADMIN_URL, admin.site.urls),
# User management
# Your stuff: custom urls includes go here
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# API URLS
urlpatterns += [
# API base url
path("api/", include("config.api_router")),
# DRF auth token
path("api/auth/token/", obtain_auth_token),
path("api/schema/", SpectacularAPIView.as_view(), name="api-schema"),
path(
"api/docs/",
SpectacularSwaggerView.as_view(url_name="api-schema"),
name="api-docs",
),
]
if settings.DEBUG:
# This allows the error pages to be debugged during development, just visit
# these url in browser to see how these error pages look like.
if "debug_toolbar" in settings.INSTALLED_APPS:
import debug_toolbar
urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns

38
config/wsgi.py Normal file
View File

@ -0,0 +1,38 @@
"""
WSGI config for Pitch Deck Generator project.
This module contains the WSGI application used by Django's development server
and any production WSGI deployments. It should expose a module-level variable
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
this application via the ``WSGI_APPLICATION`` setting.
Usually you will have the standard Django WSGI application here, but it also
might make sense to replace the whole Django WSGI application with a custom one
that later delegates to the Django one. For example, you could introduce WSGI
middleware here, or combine a Django application with an application of another
framework.
"""
import os
import sys
from pathlib import Path
from django.core.wsgi import get_wsgi_application
# This allows easy placement of apps within the interior
# pitch_deck_generator directory.
ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent
sys.path.append(str(ROOT_DIR / "pitch_deck_generator"))
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
# if running multiple sites in the same mod_wsgi process. To fix this, use
# mod_wsgi daemon mode with each site in its own daemon process, or use
# os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production"
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
# This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
application = get_wsgi_application()
# Apply WSGI middleware here.
# from helloworld.wsgi import HelloWorldApplication
# application = HelloWorldApplication(application)

1
data.json Normal file

File diff suppressed because one or more lines are too long

68
local.yml Normal file
View File

@ -0,0 +1,68 @@
version: '3'
volumes:
pitch_deck_generator_local_postgres_data: {}
pitch_deck_generator_local_postgres_data_backups: {}
services:
django: &django
build:
context: .
dockerfile: ./compose/local/django/Dockerfile
image: pitch_deck_generator_local_django
container_name: pitch_deck_generator_local_django
depends_on:
- postgres
- redis
volumes:
- .:/app:z
env_file:
- ./.envs/.local/.django
- ./.envs/.local/.postgres
ports:
- "8000:8000"
command: /start
postgres:
build:
context: .
dockerfile: ./compose/production/postgres/Dockerfile
image: pitch_deck_generator_production_postgres
container_name: pitch_deck_generator_local_postgres
volumes:
- pitch_deck_generator_local_postgres_data:/var/lib/postgresql/data
- pitch_deck_generator_local_postgres_data_backups:/backups
env_file:
- ./.envs/.local/.postgres
redis:
image: redis:6
container_name: pitch_deck_generator_local_redis
celeryworker:
<<: *django
image: pitch_deck_generator_local_celeryworker
container_name: pitch_deck_generator_local_celeryworker
depends_on:
- redis
- postgres
ports: []
command: /start-celeryworker
celerybeat:
<<: *django
image: pitch_deck_generator_local_celerybeat
container_name: pitch_deck_generator_local_celerybeat
depends_on:
- redis
- postgres
ports: []
command: /start-celerybeat
flower:
<<: *django
image: pitch_deck_generator_local_flower
container_name: pitch_deck_generator_local_flower
ports:
- "5555:5555"
command: /start-flower

6
locale/README.rst Normal file
View File

@ -0,0 +1,6 @@
Translations
============
Translations will be placed in this folder when running::
python manage.py makemessages

31
manage.py Executable file
View File

@ -0,0 +1,31 @@
#!/usr/bin/env python
import os
import sys
from pathlib import Path
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
try:
from django.core.management import execute_from_command_line
except ImportError:
# The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django # noqa
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
)
raise
# This allows easy placement of apps within the interior
# pitch_deck_generator directory.
current_path = Path(__file__).parent.resolve()
sys.path.append(str(current_path / "pitch_deck_generator"))
execute_from_command_line(sys.argv)

0
ml/__init__.py Normal file
View File

141
ml/openai_handle.py Normal file
View File

@ -0,0 +1,141 @@
import openai
import datetime
from ast import literal_eval
from django.conf import settings
KEY = settings.OPENAI_KEY
description = """
🍀 Что такое Pitch-Deck?
Pitch-Deck представляет собой презентацию-тизер проекта/компании для инвесторов, партнеров, журналистов и других заинтересованных лиц. Цель презентации - привлечение дополнительного финансирования (инвестиций).
Почему это проблема?
🍀 Проблема #1. Недостаток средств:
Для многих стартапов ограниченные финансы создают преграду при разработке качественного Pitch Deck. Отсутствие достаточных средств для найма профессиональных консультантов, дизайнеров и копирайтеров, а также для проведения исследований рынка, может привести к созданию менее привлекательной и малоинформативной презентации, что затрудняет привлечение инвестиций.
🍀 Проблема #2. Недостаток экспертизы:
Проблемой для стартапов является недостаток экспертизы для проведения необходимых исследований и корректного отражения их результатов в Pitch Deck. Не всегда у стартапов есть нужные знания в области маркетинга, финансов и анализа рынка, что затрудняет создание убедительной и информативной презентации для привлечения инвестиций.
🍀 Проблема #3. Недостаток времени
Молодым компаниям для привлечения инвестиций требуется подготовить целый пакет документов, одним из которых является Pitch Deck. Особенностью стартапов является сравнительного молодая и небольшая команда, у которой чисто физически не хватает времени на разработку инвестиционных материалов, ведь они полностью погружены в процесс разработки и улучшения продукта или сервиса.
🍀 ИДЕЯ:
Основная идея кейса заключается в создании вспомогательного инструмента на основе ИИ, заточенного под создание Pitch-Deck.
"""
names_prompt = """
По тексту ответь или предположи ответ на вопросы в следющем формате:
{
"names": "Назови 5 имен проекта с данным описанием через запятую"
}
"""
prompts = [
"""
По тексту ответь или предположи ответ на вопросы в следющем формате:
{
'users': 'Кто будет пользоваться продуктом',
'problems': 'Какие проблемы решает продукт',
'actuality': 'Продолжите предложение: Актуальность проблемы подтверждается тем фактом, что...',
'solve': 'Как решаем эти проблемы',
'works': 'Как работает решение',
}
""",
"""
По тексту ответь или предположи ответ на вопросы в следющем формате:
{
'awards': 'Когда проблема будет решена, какова будет ценность для ваших пользователей',
'money': 'На чем проект зарабатывает? сколько и за что ему платят клиенты',
'aims': Напиши 3 цели: на месяц, на полгода и год, формат: {'1': цель на месяц, '2': цель на полгода, '3': цель на год},
'investments_sold': 'На что потратить инвестиции под проект',
'financial_indicators': 'Напиши финансовые показатели проекта'
}
""",
"""
По тексту ответь или предположи ответ на вопросы в следющем формате:
{
'achieve': 'Чего добьется команда после освоения инвестиций',
'competitors_strength': 'Сильные стороны конкурентов',
'competitors_low': 'Слабые стороны конкурентов',
'advantages': 'Какие могут быть преимущества над конкурентами'
}
""",
]
openai.api_key = KEY
assertions = [
[
lambda data: "users" in data.keys(),
lambda data: "problems" in data.keys(),
lambda data: "actuality" in data.keys(),
lambda data: "solve" in data.keys(),
lambda data: "works" in data.keys(),
],
[
lambda data: "awards" in data.keys(),
lambda data: "money" in data.keys(),
lambda data: "aims" in data.keys(),
lambda data: "investments_sold" in data.keys(),
lambda data: "financial_indicators" in data.keys(),
],
[
lambda data: "achieve" in data.keys(),
lambda data: "competitors_strength" in data.keys(),
lambda data: "competitors_low" in data.keys(),
lambda data: "advantages" in data.keys(),
],
]
def create_hints(description: str, stage: int):
global prompts
chat_completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": description + "\n" + prompts[stage]}],
)
content = literal_eval(chat_completion.choices[0].message.content)
for assertion_statement in assertions[stage]:
assert assertion_statement(content)
if stage == 1:
content["aims"] = [
{
"aim": content["aims"]["1"],
"date": datetime.datetime.now() + datetime.timedelta(days=30),
},
{
"aim": content["aims"]["2"],
"date": datetime.datetime.now() + datetime.timedelta(days=180),
},
{
"aim": content["aims"]["3"],
"date": datetime.datetime.now() + datetime.timedelta(days=365),
},
]
result = []
for key, value in content.items():
result.append({"type": key, "value": value})
return result
def create_name_hint(description: str):
global names_prompt
chat_completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": description + "\n" + names_prompt}],
)
answer = literal_eval(chat_completion.choices[0].message.content)["names"].split(
", "
)
assert len(answer) == 5
return {"type": "names", "value": answer}
# print(create_name_hint(description))
# print(create_hints(description, 0))
# print(create_hints(description, 1))
# print(create_hints(description, 2))

View File

@ -0,0 +1,5 @@
__version__ = "0.1.0"
__version_info__ = tuple(
int(num) if num.isdigit() else num
for num in __version__.replace("-", ".", 1).split(".")
)

View File

View File

@ -0,0 +1,19 @@
from rest_framework.pagination import PageNumberPagination
class SmallResultsSetPagination(PageNumberPagination):
page_size = 10
page_size_query_param = "page_size"
max_page_size = 100
class StandardResultsSetPagination(PageNumberPagination):
page_size = 50
page_size_query_param = "page_size"
max_page_size = 200
class BigResultsSetPagination(PageNumberPagination):
page_size = 100
page_size_query_param = "page_size"
max_page_size = 1000

View File

@ -0,0 +1,79 @@
from django.db import models
from pitch_deck_generator.utils.files import user_file_upload_mixin
from pitch_deck_generator.utils.generators import generate_charset
from pitch_deck_generator.common.signals import (
create_cropped_model_image,
delete_cropped_model_image,
update_cropped_model_image,
)
class BaseImageModel(models.Model):
"""
stores user's images in their media folder, creates, updates and deletes preview
"""
image = models.ImageField(upload_to=user_file_upload_mixin, blank=True)
image_cropped = models.ImageField(upload_to="cropped/", blank=True)
@classmethod
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
models.signals.pre_save.connect(update_cropped_model_image, sender=cls)
models.signals.post_save.connect(create_cropped_model_image, sender=cls)
models.signals.post_delete.connect(delete_cropped_model_image, sender=cls)
class Meta:
abstract = True
def create_model_slug(sender, instance, **kwargs):
def _generate_charset():
if private:
return generate_charset(private_slug_length)
return generate_charset(slug_length)
if instance.id is None:
model = sender
slug_length = 5
private_slug_length = 20
private = False
if hasattr(model, "SlugMeta"):
if hasattr(model.SlugMeta, "slug_length"):
slug_length = model.SlugMeta.slug_length
if hasattr(model.SlugMeta, "private_slug_length"):
private_slug_length = model.SlugMeta.private_slug_length
if hasattr(instance, "private"):
if instance.private:
private = True
if hasattr(instance, "public"):
if not instance.public:
private = True
slug = _generate_charset()
while model.objects.filter(slug=slug).exists():
slug = _generate_charset()
instance.slug = slug
class SlugModel(models.Model):
"""
model to store and generate slug for model instances
for custom slug length use: slug_length, private_slug_length SlugMeta options
"""
slug = models.SlugField(max_length=20, blank=True, unique=True)
@classmethod
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
models.signals.pre_save.connect(create_model_slug, sender=cls)
class Meta:
abstract = True
class SlugMeta:
slug_length = 5
private_slug_length = 20

View File

@ -0,0 +1,50 @@
import os
from pitch_deck_generator.common.tasks import crop_model_image
def create_cropped_model_image(sender, instance, created, **kwargs):
model = sender
if created:
if instance.image:
crop_model_image.apply_async(
kwargs={
"pk": instance.pk,
"app_label": model._meta.app_label,
"model_name": model._meta.model_name,
},
countdown=2,
)
def update_cropped_model_image(sender, instance, **kwargs):
model = sender
if instance.id:
previous = model.objects.get(id=instance.id)
if previous.image != instance.image:
# delete previous cropped image
if instance.image_cropped:
if os.path.isfile(instance.image_cropped.path):
os.remove(instance.image_cropped.path)
# run task to create new cropped image
if kwargs["update_fields"] != frozenset({"image_cropped"}) and instance:
if instance.image:
crop_model_image.apply_async(
kwargs={
"pk": instance.pk,
"app_label": model._meta.app_label,
"model_name": model._meta.model_name,
},
countdown=2,
)
else:
instance.image_cropped = None
def delete_cropped_model_image(sender, instance, **kwargs):
if instance.image_cropped:
if os.path.isfile(instance.image_cropped.path):
os.remove(instance.image_cropped.path)

View File

@ -0,0 +1,18 @@
from celery import shared_task
from django.apps import apps
from django.core.files import File
from pitch_deck_generator.utils.files import crop_image
@shared_task()
def crop_model_image(pk: int, app_label: str, model_name: str):
model = apps.get_model(app_label=app_label, model_name=model_name)
instance = model.objects.get(pk=pk)
instance.image_cropped.save(
instance.image.path.split(".")[0].split("/")[-1] + ".png",
File(crop_image(instance.image.path, length=250)),
save=False,
)
instance.save(update_fields=["image_cropped"])
return pk

View File

@ -0,0 +1,14 @@
import pytest
from pitch_deck_generator.users.models import User
from pitch_deck_generator.users.tests.factories import UserFactory
@pytest.fixture(autouse=True)
def media_storage(settings, tmpdir):
settings.MEDIA_ROOT = tmpdir.strpath
@pytest.fixture
def user(db) -> User:
return UserFactory()

View File

View File

@ -0,0 +1,42 @@
import django.contrib.sites.models
from django.contrib.sites.models import _simple_domain_name_validator
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = []
operations = [
migrations.CreateModel(
name="Site",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"domain",
models.CharField(
max_length=100,
verbose_name="domain name",
validators=[_simple_domain_name_validator],
),
),
("name", models.CharField(max_length=50, verbose_name="display name")),
],
options={
"ordering": ("domain",),
"db_table": "django_site",
"verbose_name": "site",
"verbose_name_plural": "sites",
},
bases=(models.Model,),
managers=[("objects", django.contrib.sites.models.SiteManager())],
)
]

View File

@ -0,0 +1,20 @@
import django.contrib.sites.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("sites", "0001_initial")]
operations = [
migrations.AlterField(
model_name="site",
name="domain",
field=models.CharField(
max_length=100,
unique=True,
validators=[django.contrib.sites.models._simple_domain_name_validator],
verbose_name="domain name",
),
)
]

View File

@ -0,0 +1,63 @@
"""
To understand why this file is here, please read:
http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
"""
from django.conf import settings
from django.db import migrations
def _update_or_create_site_with_sequence(site_model, connection, domain, name):
"""Update or create the site with default ID and keep the DB sequence in sync."""
site, created = site_model.objects.update_or_create(
id=settings.SITE_ID,
defaults={
"domain": domain,
"name": name,
},
)
if created:
# We provided the ID explicitly when creating the Site entry, therefore the DB
# sequence to auto-generate them wasn't used and is now out of sync. If we
# don't do anything, we'll get a unique constraint violation the next time a
# site is created.
# To avoid this, we need to manually update DB sequence and make sure it's
# greater than the maximum value.
max_id = site_model.objects.order_by('-id').first().id
with connection.cursor() as cursor:
cursor.execute("SELECT last_value from django_site_id_seq")
(current_id,) = cursor.fetchone()
if current_id <= max_id:
cursor.execute(
"alter sequence django_site_id_seq restart with %s",
[max_id + 1],
)
def update_site_forward(apps, schema_editor):
"""Set site domain and name."""
Site = apps.get_model("sites", "Site")
_update_or_create_site_with_sequence(
Site,
schema_editor.connection,
"pitch_deck_generator",
"",
)
def update_site_backward(apps, schema_editor):
"""Revert site domain and name to default."""
Site = apps.get_model("sites", "Site")
_update_or_create_site_with_sequence(
Site,
schema_editor.connection,
"example.com",
"example.com",
)
class Migration(migrations.Migration):
dependencies = [("sites", "0002_alter_domain_unique")]
operations = [migrations.RunPython(update_site_forward, update_site_backward)]

View File

@ -0,0 +1,21 @@
# Generated by Django 3.1.7 on 2021-02-04 14:49
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("sites", "0003_set_site_domain_and_name"),
]
operations = [
migrations.AlterModelOptions(
name="site",
options={
"ordering": ["domain"],
"verbose_name": "site",
"verbose_name_plural": "sites",
},
),
]

View File

View File

@ -0,0 +1,6 @@
from django.contrib import admin
from pitch_deck_generator.decks.models import Question, PitchDeck
admin.site.register(PitchDeck)
admin.site.register(Question)

View File

@ -0,0 +1,54 @@
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from pitch_deck_generator.decks.models import PitchDeck, Question, QuestionDeckHint
class BasePitchDeckSerializer(serializers.ModelSerializer):
class Meta:
model = PitchDeck
fields = ["id", "name", "description", "logo", "created", "updated"]
extra_kwargs = {
"id": {"read_only": True},
"logo": {"read_only": True},
"name": {"read_only": True},
"created": {"read_only": True},
"updated": {"read_only": True},
}
class PitchDeckSerializer(serializers.ModelSerializer):
class Meta:
model = PitchDeck
fields = "__all__"
class HintSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=Question.QuestionType.choices)
value = serializers.JSONField()
class QuestionSerializer(serializers.ModelSerializer):
hint = serializers.SerializerMethodField(method_name="get_hint")
next_id = serializers.SerializerMethodField(method_name="get_next_id")
@extend_schema_field(HintSerializer)
def get_hint(self, obj):
if obj.hint:
if q := QuestionDeckHint.objects.filter(
question_id=self.context["kwargs"]["question"],
deck_id=self.context["kwargs"]["deck"],
):
return q.first().hint
return ""
return False
@extend_schema_field(serializers.IntegerField)
def get_next_id(self, obj):
if q := Question.objects.filter(order=obj.order + 1):
return q.first().id
return 0
class Meta:
model = Question
fields = ["id", "text", "hint", "required", "params", "next_id"]

View File

@ -0,0 +1,19 @@
from django.urls import path
from pitch_deck_generator.decks.api.views import (
ListDecksApiView,
RetrievePitchApiView,
GetFirstQuestionApiView,
GetDeckQuestionApiView,
GetDeckQuestionHintApiView,
)
app_name = "decks"
urlpatterns = [
path("", ListDecksApiView.as_view()),
path("<int:id>", RetrievePitchApiView.as_view()),
path("question/<int:deck_id>", GetFirstQuestionApiView.as_view()),
path("question/<int:deck_id>/<int:question_id>", GetDeckQuestionApiView.as_view()),
path("hint/<int:deck_id>/<int:question_id>", GetDeckQuestionHintApiView.as_view()),
]

View File

@ -0,0 +1,43 @@
from rest_framework import generics
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response
from pitch_deck_generator.decks.api.serializers import (
BasePitchDeckSerializer,
PitchDeckSerializer,
QuestionSerializer, HintSerializer,
)
from pitch_deck_generator.decks.models import PitchDeck
class ListDecksApiView(generics.ListCreateAPIView):
queryset = PitchDeck.objects.all()
serializer_class = BasePitchDeckSerializer
class RetrievePitchApiView(generics.RetrieveAPIView):
serializer_class = PitchDeckSerializer
def get_object(self):
return get_object_or_404(PitchDeck, id=self.kwargs["id"])
class GetFirstQuestionApiView(generics.GenericAPIView):
serializer_class = QuestionSerializer
def get(self, request, *args, **kwargs):
return Response()
class GetDeckQuestionApiView(generics.GenericAPIView):
serializer_class = QuestionSerializer
def get(self, request, *args, **kwargs):
return Response()
class GetDeckQuestionHintApiView(generics.GenericAPIView):
serializer_class = HintSerializer
def get(self, request, *args, **kwargs):
return Response()

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class DecksConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "pitch_deck_generator.decks"

View File

@ -0,0 +1,158 @@
# Generated by Django 4.2.4 on 2023-08-25 18:30
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="PitchDeck",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=250)),
("description", models.TextField()),
("questions", models.JSONField(blank=True, default=dict, null=True)),
("logo", models.ImageField(blank=True, null=True, upload_to="logos/")),
(
"styles",
models.FileField(blank=True, null=True, upload_to="styles/"),
),
(
"presentation",
models.FileField(blank=True, null=True, upload_to="logos/"),
),
("meta_info", models.JSONField(blank=True, default=dict, null=True)),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
],
options={
"ordering": ["-updated"],
},
),
migrations.CreateModel(
name="Question",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("order", models.IntegerField(unique=True)),
("text", models.CharField(max_length=300)),
(
"type",
models.CharField(
choices=[
("text", "Text"),
("scroll", "Scroll"),
("select", "Select"),
("link", "Link"),
("time", "Time"),
("photo", "Photo"),
("multiple_links", "Multiple Links"),
],
max_length=14,
),
),
("hint", models.BooleanField(default=True)),
("required", models.BooleanField(default=True)),
("condition", models.CharField(blank=True, max_length=250, null=True)),
("params", models.JSONField(blank=True, null=True)),
],
options={
"ordering": ["order"],
},
),
migrations.CreateModel(
name="QuestionAnswer",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("deck", models.FloatField(verbose_name="PitchDeck")),
("answer", models.JSONField(default={"answer": "", "type": "text"})),
(
"question",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="decks.question"
),
),
],
),
migrations.CreateModel(
name="QuestionDeckHint",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("hint", models.CharField(max_length=500)),
(
"deck",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="hints",
to="decks.pitchdeck",
),
),
(
"question",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="decks.question"
),
),
],
),
migrations.CreateModel(
name="QuestionAnswerPhoto",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("file", models.ImageField(upload_to="uploads/")),
(
"answer",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="photos",
to="decks.questionanswer",
),
),
],
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.4 on 2023-08-25 18:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("decks", "0001_initial"),
]
operations = [
migrations.RenameField(
model_name="question",
old_name="condition",
new_name="inner_tag",
),
migrations.AlterField(
model_name="questionanswer",
name="answer",
field=models.JSONField(default=dict),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 4.2.4 on 2023-08-25 19:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("decks", "0002_rename_condition_question_inner_tag_and_more"),
]
operations = [
migrations.AlterField(
model_name="question",
name="type",
field=models.CharField(
choices=[
("text", "Text"),
("text_array", "Text Array"),
("scroll", "Scroll"),
("select", "Select"),
("link", "Link"),
("time", "Time"),
("photo", "Photo"),
("multiple_links", "Multiple Links"),
],
max_length=14,
),
),
migrations.AlterField(
model_name="questiondeckhint",
name="hint",
field=models.JSONField(blank=True, null=True),
),
]

View File

@ -0,0 +1,31 @@
# Generated by Django 4.2.4 on 2023-08-25 19:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("decks", "0003_alter_question_type_alter_questiondeckhint_hint"),
]
operations = [
migrations.AlterField(
model_name="question",
name="type",
field=models.CharField(
choices=[
("text", "Text"),
("text_array", "Text Array"),
("scroll", "Scroll"),
("multiple_scroll", "Multiple Scroll"),
("select", "Select"),
("link", "Link"),
("time", "Time"),
("photo", "Photo"),
("multiple_links", "Multiple Links"),
],
max_length=15,
),
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 4.2.4 on 2023-08-25 20:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("decks", "0004_alter_question_type"),
]
operations = [
migrations.AlterField(
model_name="pitchdeck",
name="name",
field=models.CharField(blank=True, max_length=250, null=True),
),
migrations.AlterField(
model_name="question",
name="type",
field=models.CharField(
choices=[
("text", "Text"),
("number", "Number"),
("text_array", "Text Array"),
("scroll", "Scroll"),
("multiple_scroll", "Multiple Scroll"),
("select", "Select"),
("link", "Link"),
("date", "Date"),
("photo", "Photo"),
("photo_description", "Photo Description"),
("multiple_links", "Multiple Links"),
],
max_length=17,
),
),
]

View File

@ -0,0 +1,34 @@
# Generated by Django 4.2.4 on 2023-08-25 20:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("decks", "0005_alter_pitchdeck_name_alter_question_type"),
]
operations = [
migrations.AlterField(
model_name="question",
name="type",
field=models.CharField(
choices=[
("text", "Text"),
("number", "Number"),
("text_array", "Text Array"),
("range", "Range"),
("multiple_range", "Multiple Range"),
("select", "Select"),
("link", "Link"),
("date", "Date"),
("photo", "Photo"),
("photo_description", "Photo Description"),
("multiple_photo_description", "Multiple Photo Description"),
("multiple_links", "Multiple Links"),
],
max_length=26,
),
),
]

View File

@ -0,0 +1,77 @@
from django.db import models
from pitch_deck_generator.utils.choices import count_max_length
class PitchDeck(models.Model):
# user put in info
name = models.CharField(max_length=250, blank=True, null=True)
description = models.TextField()
questions = models.JSONField(default=dict, null=True, blank=True)
# generated info
logo = models.ImageField(upload_to="logos/", null=True, blank=True)
styles = models.FileField(upload_to="styles/", null=True, blank=True)
presentation = models.FileField(upload_to="logos/", null=True, blank=True)
meta_info = models.JSONField(default=dict, null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-updated"]
class Question(models.Model):
class QuestionType(models.TextChoices):
text = "text"
number = "number"
text_array = "text_array"
range = "range"
multiple_range = "multiple_range"
select = "select"
link = "link"
date = "date"
photo = "photo"
multiple_photo = "multiple_photo"
photo_description = "photo_description"
multiple_link_description = "multiple link description"
multiple_photo_description = "multiple photo description"
multiple_links = "multiple_links"
order = models.IntegerField(unique=True)
text = models.CharField(max_length=300)
type = models.CharField(
choices=QuestionType.choices, max_length=count_max_length(QuestionType.choices)
)
hint = models.BooleanField(default=True)
required = models.BooleanField(default=True)
params = models.JSONField(blank=True, null=True)
inner_tag = models.CharField(blank=True, null=True, max_length=250)
def __str__(self):
return f"{self.order} - {self.text} - {self.type}"
class Meta:
ordering = ["order"]
class QuestionDeckHint(models.Model):
question = models.ForeignKey("Question", on_delete=models.CASCADE)
deck = models.ForeignKey(
"PitchDeck", related_name="hints", on_delete=models.CASCADE
)
hint = models.JSONField(null=True, blank=True)
class QuestionAnswer(models.Model):
deck = models.FloatField("PitchDeck")
question = models.ForeignKey("Question", on_delete=models.CASCADE)
answer = models.JSONField(default=dict)
class QuestionAnswerPhoto(models.Model):
answer = models.ForeignKey(
"QuestionAnswer", related_name="photos", on_delete=models.CASCADE
)
file = models.ImageField(upload_to="uploads/")

View File

@ -0,0 +1,30 @@
from celery import shared_task
from ml.openai_handle import create_name_hint, create_hints
from pitch_deck_generator.decks.models import PitchDeck, Question, QuestionDeckHint
@shared_task
def run_pitch_deck_calculation(pk: int):
generate_pitch_deck_name.apply_async(kwargs={"pk": pk})
for i in range(3):
generate_pitch_deck_name.apply_async(kwargs={"pk": pk, "num": pk})
@shared_task
def generate_pitch_deck_name(pk: int):
pitch_deck = PitchDeck.objects.get(pk=pk)
data = create_name_hint(pitch_deck.description)
question = Question.objects.get(inner_tag=data["type"])
QuestionDeckHint.objects.create(
question=question,
deck=pitch_deck,
hint={"type": "text_array", "value": data["value"]},
)
@shared_task
def generate_batch_hints(pk: int, num: int):
pitch_deck = PitchDeck.objects.get(pk=pk)
data = create_hints(pitch_deck.description, num)
print(data)

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

0
pitch_deck_generator/static/.gitkeep vendored Normal file
View File

View File

View File

@ -0,0 +1,5 @@
from django.contrib import admin
from pitch_deck_generator.tickets.models import Ticket
admin.site.register(Ticket)

View File

@ -0,0 +1,11 @@
from rest_framework import serializers
from pitch_deck_generator.tickets.models import Ticket
class TicketSerializer(serializers.ModelSerializer):
current = serializers.IntegerField()
class Meta:
model = Ticket
fields = ["name", "current", "max", "next"]

View File

@ -0,0 +1,7 @@
from django.urls import path
from pitch_deck_generator.tickets.api.views import RetrieveTicketSerializer
urlpatterns = [
path("<str:uuid>", RetrieveTicketSerializer.as_view(), name="ticket"),
]

View File

@ -0,0 +1,15 @@
from rest_framework import generics
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from pitch_deck_generator.tickets.api.serializers import TicketSerializer
from pitch_deck_generator.tickets.services import get_ticket_data
class RetrieveTicketSerializer(generics.RetrieveAPIView):
serializer_class = TicketSerializer
permission_classes = [AllowAny]
def get(self, request, *args, **kwargs):
data = get_ticket_data(self.kwargs["uuid"])
return Response(data)

View File

@ -0,0 +1,12 @@
from django.apps import AppConfig
class TicketsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "pitch_deck_generator.tickets"
def ready(self):
try:
import pitch_deck_generator.tickets.signals # noqa F401
except ImportError:
pass

View File

@ -0,0 +1,31 @@
# Generated by Django 4.2.4 on 2023-08-24 21:09
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Ticket",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(default="processing...", max_length=250)),
("max", models.IntegerField(default=0)),
("next", models.URLField(null=True)),
],
),
]

View File

@ -0,0 +1,17 @@
import uuid
from django.db import models
from django.urls import reverse
class Ticket(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=250, default="processing...")
max = models.IntegerField(default=0)
next = models.URLField(null=True)
def __str__(self):
return f"{self.id} - {self.name}"
def get_absolute_url(self):
return reverse("api:ticket", kwargs={"uuid": self.id})

View File

@ -0,0 +1,28 @@
from django.core.cache import cache
from rest_framework.exceptions import NotFound
def get_ticket_data(uuid: str):
t = cache.get(uuid)
if t is None:
raise NotFound(
detail={
"name": "not found",
"current": 0,
"max": 0,
"next": None,
}
)
data = cache.get_many(
[f"{uuid}-max", f"{uuid}-current", f"{uuid}-name", f"{uuid}-next"]
)
return {
"name": data[f"{uuid}-name"],
"current": data[f"{uuid}-current"],
"max": data[f"{uuid}-max"],
"next": data[f"{uuid}-next"],
}
def increase_ticket(uuid: str):
cache.incr(f"{uuid}-current", 1)

View File

@ -0,0 +1,42 @@
from django.core.cache import cache
from django.db.models.signals import post_save, post_delete, pre_save
from django.dispatch import receiver
from pitch_deck_generator.tickets.models import Ticket
@receiver(post_save, sender=Ticket)
def ticket_to_cache(sender, instance: Ticket, created, **kwargs):
if created:
cache.set_many(
{
f"{instance.id}": True,
f"{instance.id}-current": 0,
f"{instance.id}-name": instance.name,
f"{instance.id}-max": instance.max,
f"{instance.id}-next": instance.next,
},
3600,
)
@receiver(pre_save, sender=Ticket)
def update_ticket(sender, instance: Ticket, **kwargs):
if instance.id is not None:
cache.set_many(
{
f"{instance.id}": True,
f"{instance.id}-name": instance.name,
f"{instance.id}-max": instance.max,
f"{instance.id}-next": instance.next,
},
3600,
)
@receiver(post_delete, sender=Ticket)
def delete_ticket(sender, instance: Ticket, **kwargs):
uuid = instance.id
cache.delete_many(
[f"{uuid}", f"{uuid}-max", f"{uuid}-current", f"{uuid}-name", f"{uuid}-next"]
)

Some files were not shown because too many files have changed in this diff Show More