mirror of
https://github.com/spbleadersofdigtal/backend.git
synced 2024-11-23 19:53:42 +03:00
Initial commit
This commit is contained in:
commit
2cc1736951
10
.dockerignore
Normal file
10
.dockerignore
Normal 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
27
.editorconfig
Normal 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
5
.env
Normal 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
5
.env.template
Normal 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
25
.gitattributes
vendored
Normal 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
80
.github/dependabot.yml
vendored
Normal 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
57
.github/workflows/ci.yml
vendored
Normal 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
333
.gitignore
vendored
Normal 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
8
.idea/.gitignore
vendored
Normal 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
|
24
.idea/inspectionProfiles/Project_Default.xml
Normal file
24
.idea/inspectionProfiles/Project_Default.xml
Normal 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>
|
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
Normal 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
4
.idea/misc.xml
Normal 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
8
.idea/modules.xml
Normal 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>
|
27
.idea/pitch_deck_generator.iml
Normal file
27
.idea/pitch_deck_generator.iml
Normal 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="<map/>" />
|
||||
<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
39
.pre-commit-config.yaml
Normal 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
14
.pylintrc
Normal 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
40
README.md
Normal 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)
|
79
compose/local/django/Dockerfile
Normal file
79
compose/local/django/Dockerfile
Normal 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"]
|
8
compose/local/django/celery/beat/start
Normal file
8
compose/local/django/celery/beat/start
Normal 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'
|
8
compose/local/django/celery/flower/start
Normal file
8
compose/local/django/celery/flower/start
Normal 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}\""
|
7
compose/local/django/celery/worker/start
Normal file
7
compose/local/django/celery/worker/start
Normal 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'
|
9
compose/local/django/start
Normal file
9
compose/local/django/start
Normal 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
|
86
compose/production/django/Dockerfile
Normal file
86
compose/production/django/Dockerfile
Normal 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
|
8
compose/production/django/celery/beat/start
Normal file
8
compose/production/django/celery/beat/start
Normal 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
|
11
compose/production/django/celery/flower/start
Normal file
11
compose/production/django/celery/flower/start
Normal 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}"
|
8
compose/production/django/celery/worker/start
Normal file
8
compose/production/django/celery/worker/start
Normal 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
|
49
compose/production/django/entrypoint
Normal file
49
compose/production/django/entrypoint
Normal 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 "$@"
|
14
compose/production/django/manage
Normal file
14
compose/production/django/manage
Normal 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 "$@"
|
9
compose/production/django/start
Normal file
9
compose/production/django/start
Normal 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
|
6
compose/production/postgres/Dockerfile
Normal file
6
compose/production/postgres/Dockerfile
Normal 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
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
BACKUP_DIR_PATH='/backups'
|
||||
BACKUP_FILE_PREFIX='backup'
|
|
@ -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
|
||||
}
|
41
compose/production/postgres/maintenance/_sourced/messages.sh
Normal file
41
compose/production/postgres/maintenance/_sourced/messages.sh
Normal 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: ${@}"
|
||||
}
|
16
compose/production/postgres/maintenance/_sourced/yes_no.sh
Normal file
16
compose/production/postgres/maintenance/_sourced/yes_no.sh
Normal 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
|
||||
}
|
38
compose/production/postgres/maintenance/backup
Normal file
38
compose/production/postgres/maintenance/backup
Normal 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}'."
|
22
compose/production/postgres/maintenance/backups
Normal file
22
compose/production/postgres/maintenance/backups
Normal 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}"
|
55
compose/production/postgres/maintenance/restore
Normal file
55
compose/production/postgres/maintenance/restore
Normal 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."
|
5
compose/production/traefik/Dockerfile
Normal file
5
compose/production/traefik/Dockerfile
Normal 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
|
75
compose/production/traefik/traefik.yml
Normal file
75
compose/production/traefik/traefik.yml
Normal 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
5
config/__init__.py
Normal 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
7
config/api_router.py
Normal 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
17
config/celery_app.py
Normal 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()
|
0
config/settings/__init__.py
Normal file
0
config/settings/__init__.py
Normal file
354
config/settings/base.py
Normal file
354
config/settings/base.py
Normal 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
58
config/settings/local.py
Normal 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...
|
||||
# ------------------------------------------------------------------------------
|
115
config/settings/production.py
Normal file
115
config/settings/production.py
Normal 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
33
config/settings/test.py
Normal 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
36
config/urls.py
Normal 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
38
config/wsgi.py
Normal 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)
|
68
local.yml
Normal file
68
local.yml
Normal 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
6
locale/README.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
Translations
|
||||
============
|
||||
|
||||
Translations will be placed in this folder when running::
|
||||
|
||||
python manage.py makemessages
|
31
manage.py
Executable file
31
manage.py
Executable 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
0
ml/__init__.py
Normal file
141
ml/openai_handle.py
Normal file
141
ml/openai_handle.py
Normal 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))
|
5
pitch_deck_generator/__init__.py
Normal file
5
pitch_deck_generator/__init__.py
Normal 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(".")
|
||||
)
|
0
pitch_deck_generator/common/__init__.py
Normal file
0
pitch_deck_generator/common/__init__.py
Normal file
19
pitch_deck_generator/common/api.py
Normal file
19
pitch_deck_generator/common/api.py
Normal 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
|
79
pitch_deck_generator/common/models.py
Normal file
79
pitch_deck_generator/common/models.py
Normal 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
|
50
pitch_deck_generator/common/signals.py
Normal file
50
pitch_deck_generator/common/signals.py
Normal 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)
|
18
pitch_deck_generator/common/tasks.py
Normal file
18
pitch_deck_generator/common/tasks.py
Normal 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
|
14
pitch_deck_generator/conftest.py
Normal file
14
pitch_deck_generator/conftest.py
Normal 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()
|
0
pitch_deck_generator/contrib/__init__.py
Normal file
0
pitch_deck_generator/contrib/__init__.py
Normal file
0
pitch_deck_generator/contrib/sites/__init__.py
Normal file
0
pitch_deck_generator/contrib/sites/__init__.py
Normal 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())],
|
||||
)
|
||||
]
|
|
@ -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",
|
||||
),
|
||||
)
|
||||
]
|
|
@ -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)]
|
|
@ -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",
|
||||
},
|
||||
),
|
||||
]
|
0
pitch_deck_generator/decks/__init__.py
Normal file
0
pitch_deck_generator/decks/__init__.py
Normal file
6
pitch_deck_generator/decks/admin.py
Normal file
6
pitch_deck_generator/decks/admin.py
Normal 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)
|
0
pitch_deck_generator/decks/api/__init__.py
Normal file
0
pitch_deck_generator/decks/api/__init__.py
Normal file
54
pitch_deck_generator/decks/api/serializers.py
Normal file
54
pitch_deck_generator/decks/api/serializers.py
Normal 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"]
|
19
pitch_deck_generator/decks/api/urls.py
Normal file
19
pitch_deck_generator/decks/api/urls.py
Normal 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()),
|
||||
]
|
43
pitch_deck_generator/decks/api/views.py
Normal file
43
pitch_deck_generator/decks/api/views.py
Normal 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()
|
6
pitch_deck_generator/decks/apps.py
Normal file
6
pitch_deck_generator/decks/apps.py
Normal 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"
|
158
pitch_deck_generator/decks/migrations/0001_initial.py
Normal file
158
pitch_deck_generator/decks/migrations/0001_initial.py
Normal 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",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
0
pitch_deck_generator/decks/migrations/__init__.py
Normal file
0
pitch_deck_generator/decks/migrations/__init__.py
Normal file
77
pitch_deck_generator/decks/models.py
Normal file
77
pitch_deck_generator/decks/models.py
Normal 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/")
|
30
pitch_deck_generator/decks/tasks.py
Normal file
30
pitch_deck_generator/decks/tasks.py
Normal 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)
|
3
pitch_deck_generator/decks/tests.py
Normal file
3
pitch_deck_generator/decks/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
3
pitch_deck_generator/decks/views.py
Normal file
3
pitch_deck_generator/decks/views.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
0
pitch_deck_generator/static/.gitkeep
vendored
Normal file
0
pitch_deck_generator/static/.gitkeep
vendored
Normal file
0
pitch_deck_generator/tickets/__init__.py
Normal file
0
pitch_deck_generator/tickets/__init__.py
Normal file
5
pitch_deck_generator/tickets/admin.py
Normal file
5
pitch_deck_generator/tickets/admin.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from pitch_deck_generator.tickets.models import Ticket
|
||||
|
||||
admin.site.register(Ticket)
|
0
pitch_deck_generator/tickets/api/__init__.py
Normal file
0
pitch_deck_generator/tickets/api/__init__.py
Normal file
11
pitch_deck_generator/tickets/api/serializers.py
Normal file
11
pitch_deck_generator/tickets/api/serializers.py
Normal 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"]
|
7
pitch_deck_generator/tickets/api/urls.py
Normal file
7
pitch_deck_generator/tickets/api/urls.py
Normal 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"),
|
||||
]
|
15
pitch_deck_generator/tickets/api/views.py
Normal file
15
pitch_deck_generator/tickets/api/views.py
Normal 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)
|
12
pitch_deck_generator/tickets/apps.py
Normal file
12
pitch_deck_generator/tickets/apps.py
Normal 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
|
31
pitch_deck_generator/tickets/migrations/0001_initial.py
Normal file
31
pitch_deck_generator/tickets/migrations/0001_initial.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
0
pitch_deck_generator/tickets/migrations/__init__.py
Normal file
0
pitch_deck_generator/tickets/migrations/__init__.py
Normal file
17
pitch_deck_generator/tickets/models.py
Normal file
17
pitch_deck_generator/tickets/models.py
Normal 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})
|
28
pitch_deck_generator/tickets/services.py
Normal file
28
pitch_deck_generator/tickets/services.py
Normal 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)
|
42
pitch_deck_generator/tickets/signals.py
Normal file
42
pitch_deck_generator/tickets/signals.py
Normal 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
Loading…
Reference in New Issue
Block a user