commit
50a5e016c8
155 changed files with 8148 additions and 0 deletions
-
19.env.dev
-
26.env.prod
-
419.gitignore
-
32Dockerfile
-
58Dockerfile.prod
-
35Jenkinsfile
-
0README.md
-
0apps/account/__init__.py
-
4apps/account/admin/__init__.py
-
56apps/account/admin/professor.py
-
56apps/account/admin/student.py
-
94apps/account/admin/user.py
-
7apps/account/apps.py
-
25apps/account/custom_user_login.py
-
0apps/account/doc.py
-
0apps/account/management/__init__.py
-
0apps/account/management/commands/__init__,py
-
52apps/account/management/commands/create_groups.py
-
83apps/account/manager.py
-
116apps/account/migrations/0001_initial.py
-
18apps/account/migrations/0002_alter_user_birthdate.py
-
22apps/account/migrations/0003_auto_20241120_1741.py
-
0apps/account/migrations/__init__.py
-
4apps/account/models/__init__.py
-
95apps/account/models/groups.py
-
84apps/account/models/user.py
-
12apps/account/permissions.py
-
2apps/account/serializers/__init__.py
-
152apps/account/serializers/user.py
-
61apps/account/tasks.py
-
3apps/account/tests.py
-
35apps/account/urls.py
-
1apps/account/views/__init__.py
-
238apps/account/views/user.py
-
0apps/api/__init__.py
-
3apps/api/admin.py
-
6apps/api/apps.py
-
3apps/api/models.py
-
3apps/api/tests.py
-
9apps/api/urls.py
-
33apps/api/views.py
-
0apps/course/__init__.py
-
2apps/course/admin/__init__.py
-
87apps/course/admin/course.py
-
18apps/course/admin/lesson.py
-
6apps/course/apps.py
-
0apps/course/migrations/__init__.py
-
2apps/course/models/__init__.py
-
166apps/course/models/course.py
-
34apps/course/models/lesson.py
-
1apps/course/serializers/__init__.py
-
107apps/course/serializers/course.py
-
3apps/course/tests.py
-
15apps/course/urls.py
-
1apps/course/views/__init__.py
-
98apps/course/views/course.py
-
0apps/quiz/__init__.py
-
3apps/quiz/admin.py
-
6apps/quiz/apps.py
-
0apps/quiz/migrations/__init__.py
-
0apps/quiz/models/__init__.py
-
68apps/quiz/models/participant.py
-
53apps/quiz/models/quiz.py
-
3apps/quiz/tests.py
-
3apps/quiz/views.py
-
8config/__init__.py
-
16config/asgi.py
-
22config/celery.py
-
19config/language_code_middleware.py
-
15config/redis_config.py
-
0config/settings/__init__.py
-
308config/settings/base.py
-
17config/settings/develop.py
-
120config/settings/production.py
-
29config/test_auth_middleware.py
-
53config/urls.py
-
16config/wsgi.py
-
85docker-compose.prod.yml
-
37docker-compose.yml
-
2dynamic_preferences/__init__.py
-
114dynamic_preferences/admin.py
-
0dynamic_preferences/api/__init__.py
-
71dynamic_preferences/api/serializers.py
-
179dynamic_preferences/api/viewsets.py
-
25dynamic_preferences/apps.py
-
15dynamic_preferences/dynamic_preferences_registry.py
-
32dynamic_preferences/exceptions.py
-
152dynamic_preferences/forms.py
-
60dynamic_preferences/locale/ar/LC_MESSAGES/django.po
-
71dynamic_preferences/locale/de/LC_MESSAGES/django.po
-
71dynamic_preferences/locale/fa/LC_MESSAGES/django.po
-
59dynamic_preferences/locale/fr/LC_MESSAGES/django.po
-
71dynamic_preferences/locale/pl/LC_MESSAGES/django.po
-
1dynamic_preferences/management/__init__.py
-
1dynamic_preferences/management/commands/__init__.py
-
76dynamic_preferences/management/commands/checkpreferences.py
-
239dynamic_preferences/managers.py
-
50dynamic_preferences/migrations/0001_initial.py
-
27dynamic_preferences/migrations/0002_auto_20150712_0332.py
-
33dynamic_preferences/migrations/0003_auto_20151223_1407.py
@ -0,0 +1,19 @@ |
|||||
|
|
||||
|
# DJANGO_ALLOWED_HOSTS=127.0.0.1,* |
||||
|
# DJANGO_SETTINGS_MODULE=config.settings.base |
||||
|
|
||||
|
|
||||
|
# #[database.POSTGRES] |
||||
|
|
||||
|
# POSTGRES_USER=postgres2 |
||||
|
# POSTGRES_DB=aquila |
||||
|
# POSTGRES_PASSWORD=admin |
||||
|
# POSTGRES_PORT=5432 |
||||
|
# POSTGRES_HOST=postgres |
||||
|
# DATABASE=aquila |
||||
|
|
||||
|
|
||||
|
# #[captcha] |
||||
|
# captcha_public_key="6LdkezEdAAAAAHFBxFSL6xJOYHxC66R274uVrqhC" |
||||
|
# captcha_private_key="6LdkezEdAAAAAMw997urKO6dOW8L223ql555KeaO" |
||||
|
|
||||
@ -0,0 +1,26 @@ |
|||||
|
# DJANGO_ALLOWED_HOSTS=127.0.0.1,aqila.nwhco.ir,www.aqila.nwhco.ir,*.nwhco.ir,188.40.92.124,88.99.212.243 |
||||
|
# DJANGO_SETTINGS_MODULE=config.settings.production |
||||
|
|
||||
|
|
||||
|
# #[database.POSTGRES] |
||||
|
# POSTGRES_USER="pg-user" |
||||
|
# POSTGRES_DB="aqila" |
||||
|
# POSTGRES_PASSWORD="fdhd484fgsfddsdaf5@4df8g?90)(dfg78" |
||||
|
# POSTGRES_PORT="5432" |
||||
|
# POSTGRES_HOST="postgres" |
||||
|
|
||||
|
|
||||
|
# REDIS_URL=redis://aqila_redis:6379/0 |
||||
|
# # celery |
||||
|
# CELERY_BROKER=redis://aqila_redis:6379/0 |
||||
|
# CELERY_BACKEND=redis://aqila_redis:6379/0 |
||||
|
# FLOWER_UNAUTHENTICATED_API=true |
||||
|
# TIMEZONE="Asia/Tehran" |
||||
|
# CELERY_TIMEZONE="Asia/Tehran" |
||||
|
|
||||
|
|
||||
|
# #[captcha] |
||||
|
# captcha_public_key="6LdgCjseAAAAAIwg41-kyyulmwDtqD2Gk3THIwy2" |
||||
|
# captcha_private_key="6LdgCjseAAAAAPHMsIHuQgYAGTJ7_QlhqG4G0NyS" |
||||
|
|
||||
|
# FCM_API_KEY="" |
||||
@ -0,0 +1,419 @@ |
|||||
|
settings.json |
||||
|
# migrations/ |
||||
|
.DS_Store |
||||
|
local-cdn/ |
||||
|
# .env-dev |
||||
|
# .env-prod |
||||
|
# Byte-compiled / optimized / DLL files |
||||
|
__pycache__/ |
||||
|
*.py[cod] |
||||
|
*$py.class |
||||
|
|
||||
|
static/ |
||||
|
# C extensions |
||||
|
*.so |
||||
|
|
||||
|
# Distribution / packaging |
||||
|
.Python |
||||
|
build/ |
||||
|
develop-eggs/ |
||||
|
dist/ |
||||
|
downloads/ |
||||
|
eggs/ |
||||
|
.eggs/ |
||||
|
lib/ |
||||
|
lib64/ |
||||
|
parts/ |
||||
|
sdist/ |
||||
|
var/ |
||||
|
wheels/ |
||||
|
pip-wheel-metadata/ |
||||
|
share/python-wheels/ |
||||
|
*.egg-info/ |
||||
|
.installed.cfg |
||||
|
*.egg |
||||
|
MANIFEST |
||||
|
# In the name of Allah |
||||
|
# Byte-compiled / optimized / DLL files |
||||
|
__pycache__/ |
||||
|
*.py[cod] |
||||
|
*$py.class |
||||
|
|
||||
|
# C extensions |
||||
|
*.so |
||||
|
|
||||
|
# Distribution / packaging |
||||
|
.Python |
||||
|
env/ |
||||
|
build/ |
||||
|
develop-eggs/ |
||||
|
dist/ |
||||
|
downloads/ |
||||
|
eggs/ |
||||
|
.eggs/ |
||||
|
lib/ |
||||
|
lib64/ |
||||
|
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: |
||||
|
*.log |
||||
|
local_settings.py |
||||
|
|
||||
|
# Flask stuff: |
||||
|
instance/ |
||||
|
.webassets-cache |
||||
|
|
||||
|
# Scrapy stuff: |
||||
|
.scrapy |
||||
|
|
||||
|
# Sphinx documentation |
||||
|
docs/_build/ |
||||
|
|
||||
|
# PyBuilder |
||||
|
target/ |
||||
|
|
||||
|
# Jupyter Notebook |
||||
|
.ipynb_checkpoints |
||||
|
|
||||
|
# pyenv |
||||
|
.python-version |
||||
|
|
||||
|
# celery beat schedule file |
||||
|
celerybeat-schedule |
||||
|
|
||||
|
# SageMath parsed files |
||||
|
*.sage.py |
||||
|
|
||||
|
# dotenv |
||||
|
.env |
||||
|
|
||||
|
# virtualenv |
||||
|
.venv |
||||
|
venv/ |
||||
|
ENV/ |
||||
|
.vscode |
||||
|
.idea |
||||
|
|
||||
|
*.mp4 |
||||
|
# Spyder project settings |
||||
|
.spyderproject |
||||
|
.spyproject |
||||
|
|
||||
|
# Rope project settings |
||||
|
.ropeproject |
||||
|
|
||||
|
# mkdocs documentation |
||||
|
/site |
||||
|
|
||||
|
# mypy |
||||
|
.mypy_cache/ |
||||
|
|
||||
|
.DS_Store |
||||
|
*.sqlite3 |
||||
|
media/ |
||||
|
*.pyc |
||||
|
*.db |
||||
|
*.pid |
||||
|
|
||||
|
# Ignore Django Migrations in Development if you are working on team |
||||
|
|
||||
|
#Only for Development only |
||||
|
#**/migrations/** |
||||
|
#!**/migrations/__init__.py |
||||
|
|
||||
|
#comment migrations ignorance bcz we need it to be exist |
||||
|
|
||||
|
|
||||
|
#server gitignore |
||||
|
passenger_wsgi.py |
||||
|
.htaccess |
||||
|
static/uploads/ |
||||
|
static/quran_audios |
||||
|
tmp/ |
||||
|
Pipfile.lock |
||||
|
quran-pages-audios/*.zip |
||||
|
quran.sql |
||||
|
tafsir.sql |
||||
|
output_file.sql |
||||
|
|
||||
|
src |
||||
|
calendar.json |
||||
|
apps/mafatih/data/mafatih_indonesia/*.json |
||||
|
apps/mafatih/data/mafatih_indonesia/1 |
||||
|
apps/mafatih/data/Germany Duas/*.xlsx |
||||
|
!apps/mafatih/data/mafatih_indonesia/final_jun_11.json |
||||
|
volumes/ |
||||
|
|
||||
|
apps/mafatih/data/*.json |
||||
|
apps/ahkam/data/*.json |
||||
|
!apps/ahkam/data/makarem_fa_data.json |
||||
|
|
||||
|
mediafiles/* |
||||
|
wabot/ |
||||
|
Sabeel Media Content/ |
||||
|
|
||||
|
|
||||
|
*.lock |
||||
|
*.toml |
||||
|
# PyInstaller |
||||
|
# Usually these files are written by a python script from a template |
||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it. |
||||
|
*.manifest |
||||
|
*.spec |
||||
|
|
||||
|
# Installer logs |
||||
|
pip-log.txt |
||||
|
pip-delete-this-directory.txt |
||||
|
|
||||
|
# Unit test / coverage reports |
||||
|
htmlcov/ |
||||
|
.tox/ |
||||
|
.nox/ |
||||
|
.coverage |
||||
|
.coverage.* |
||||
|
.cache |
||||
|
nosetests.xml |
||||
|
coverage.xml |
||||
|
*.cover |
||||
|
*.py,cover |
||||
|
.hypothesis/ |
||||
|
.pytest_cache/ |
||||
|
|
||||
|
# Translations |
||||
|
*.mo |
||||
|
*.pot |
||||
|
|
||||
|
# Django stuff: |
||||
|
*.log |
||||
|
local_settings.py |
||||
|
db.sqlite3 |
||||
|
db.sqlite3-journal |
||||
|
|
||||
|
# Flask stuff: |
||||
|
instance/ |
||||
|
.webassets-cache |
||||
|
|
||||
|
# Scrapy stuff: |
||||
|
.scrapy |
||||
|
|
||||
|
# Sphinx documentation |
||||
|
docs/_build/ |
||||
|
|
||||
|
# PyBuilder |
||||
|
target/ |
||||
|
|
||||
|
# Jupyter Notebook |
||||
|
.ipynb_checkpoints |
||||
|
|
||||
|
# IPython |
||||
|
profile_default/ |
||||
|
ipython_config.py |
||||
|
|
||||
|
# pyenv |
||||
|
.python-version |
||||
|
|
||||
|
# pipenv |
||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. |
||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies |
||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not |
||||
|
# install all needed dependencies. |
||||
|
#Pipfile.lock |
||||
|
|
||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow |
||||
|
__pypackages__/ |
||||
|
|
||||
|
# Celery stuff |
||||
|
celerybeat-schedule |
||||
|
celerybeat.pid |
||||
|
|
||||
|
# SageMath parsed files |
||||
|
*.sage.py |
||||
|
|
||||
|
# Environments |
||||
|
# # .env |
||||
|
# .venv |
||||
|
# # env/ |
||||
|
# venv/ |
||||
|
# ENV/ |
||||
|
# env.bak/ |
||||
|
# venv.bak/ |
||||
|
|
||||
|
# Spyder project settings |
||||
|
.spyderproject |
||||
|
.spyproject |
||||
|
|
||||
|
# Rope project settings |
||||
|
.ropeproject |
||||
|
|
||||
|
# mkdocs documentation |
||||
|
/site |
||||
|
|
||||
|
# mypy |
||||
|
.mypy_cache/ |
||||
|
.dmypy.json |
||||
|
dmypy.json |
||||
|
|
||||
|
# Pyre type checker |
||||
|
.pyre/ |
||||
|
|
||||
|
|
||||
|
# Logs |
||||
|
logs |
||||
|
*.log |
||||
|
npm-debug.log* |
||||
|
yarn-debug.log* |
||||
|
yarn-error.log* |
||||
|
lerna-debug.log* |
||||
|
.pnpm-debug.log* |
||||
|
|
||||
|
# Diagnostic reports (https://nodejs.org/api/report.html) |
||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json |
||||
|
|
||||
|
# 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 |
||||
|
*.lcov |
||||
|
|
||||
|
# nyc test coverage |
||||
|
.nyc_output |
||||
|
|
||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) |
||||
|
.grunt |
||||
|
|
||||
|
# Bower dependency directory (https://bower.io/) |
||||
|
bower_components |
||||
|
|
||||
|
# node-waf configuration |
||||
|
.lock-wscript |
||||
|
|
||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html) |
||||
|
build/Release |
||||
|
|
||||
|
# Dependency directories |
||||
|
node_modules/ |
||||
|
jspm_packages/ |
||||
|
|
||||
|
# Snowpack dependency directory (https://snowpack.dev/) |
||||
|
web_modules/ |
||||
|
|
||||
|
# TypeScript cache |
||||
|
*.tsbuildinfo |
||||
|
|
||||
|
# Optional npm cache directory |
||||
|
.npm |
||||
|
|
||||
|
# Optional eslint cache |
||||
|
.eslintcache |
||||
|
|
||||
|
# Optional stylelint cache |
||||
|
.stylelintcache |
||||
|
|
||||
|
# Microbundle cache |
||||
|
.rpt2_cache/ |
||||
|
.rts2_cache_cjs/ |
||||
|
.rts2_cache_es/ |
||||
|
.rts2_cache_umd/ |
||||
|
|
||||
|
# Optional REPL history |
||||
|
.node_repl_history |
||||
|
|
||||
|
# Output of 'npm pack' |
||||
|
*.tgz |
||||
|
|
||||
|
# Yarn Integrity file |
||||
|
.yarn-integrity |
||||
|
|
||||
|
# dotenv environment variable files |
||||
|
# .env |
||||
|
# .env.development.local |
||||
|
# .env.test.local |
||||
|
# .env.production.local |
||||
|
# .env.local |
||||
|
|
||||
|
# parcel-bundler cache (https://parceljs.org/) |
||||
|
.cache |
||||
|
.parcel-cache |
||||
|
|
||||
|
# Next.js build output |
||||
|
.next |
||||
|
out |
||||
|
|
||||
|
# Nuxt.js build / generate output |
||||
|
.nuxt |
||||
|
dist |
||||
|
|
||||
|
# Gatsby files |
||||
|
.cache/ |
||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js |
||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support |
||||
|
# public |
||||
|
|
||||
|
# vuepress build output |
||||
|
.vuepress/dist |
||||
|
|
||||
|
# vuepress v2.x temp and cache directory |
||||
|
.temp |
||||
|
.cache |
||||
|
|
||||
|
# Docusaurus cache and generated files |
||||
|
.docusaurus |
||||
|
|
||||
|
# Serverless directories |
||||
|
.serverless/ |
||||
|
|
||||
|
# FuseBox cache |
||||
|
.fusebox/ |
||||
|
|
||||
|
# DynamoDB Local files |
||||
|
.dynamodb/ |
||||
|
|
||||
|
# TernJS port file |
||||
|
.tern-port |
||||
|
|
||||
|
# Stores VSCode versions used for testing VSCode extensions |
||||
|
.vscode-test |
||||
|
|
||||
|
# yarn v2 |
||||
|
.yarn/cache |
||||
|
.yarn/unplugged |
||||
|
.yarn/build-state.yml |
||||
|
.yarn/install-state.gz |
||||
|
.pnp.* |
||||
@ -0,0 +1,32 @@ |
|||||
|
# pull official base image |
||||
|
FROM python:3.9 |
||||
|
|
||||
|
# set work directory |
||||
|
WORKDIR /usr/src/app |
||||
|
|
||||
|
# set environment variables |
||||
|
ENV PYTHONDONTWRITEBYTECODE 1 |
||||
|
ENV PYTHONUNBUFFERED 1 |
||||
|
|
||||
|
RUN apt-get update |
||||
|
# RUN apt-get install -y vim |
||||
|
# RUN apt-get install -y ffmpeg |
||||
|
# RUN apt-get install -y cron |
||||
|
# install dependencies |
||||
|
RUN pip install --upgrade pip |
||||
|
|
||||
|
COPY ./requirements.txt . |
||||
|
COPY .env.dev .env |
||||
|
|
||||
|
RUN --mount=type=cache,target=/root/.cache pip install -r requirements.txt |
||||
|
|
||||
|
# copy entrypoint.sh |
||||
|
COPY ./entrypoint.sh . |
||||
|
RUN sed -i 's/\r$//g' /usr/src/app/entrypoint.sh |
||||
|
RUN chmod +x /usr/src/app/entrypoint.sh |
||||
|
|
||||
|
# copy project |
||||
|
COPY . . |
||||
|
|
||||
|
# run entrypoint.sh |
||||
|
# ENTRYPOINT ["/usr/src/app/entrypoint.sh"] |
||||
@ -0,0 +1,58 @@ |
|||||
|
# pull official base image |
||||
|
FROM python:3.9-alpine |
||||
|
|
||||
|
# set work directory |
||||
|
WORKDIR /usr/src/app |
||||
|
|
||||
|
# set environment variables |
||||
|
ENV PYTHONDONTWRITEBYTECODE 1 |
||||
|
ENV PYTHONUNBUFFERED 1 |
||||
|
|
||||
|
# install psycopg2 dependencies |
||||
|
RUN apk update && apk add --no-cache \ |
||||
|
git \ |
||||
|
wget \ |
||||
|
unzip \ |
||||
|
curl \ |
||||
|
postgresql-dev \ |
||||
|
gcc \ |
||||
|
python3-dev \ |
||||
|
musl-dev \ |
||||
|
jpeg-dev \ |
||||
|
zlib-dev \ |
||||
|
freetype-dev \ |
||||
|
gnupg \ |
||||
|
chromium \ |
||||
|
chromium-chromedriver \ |
||||
|
harfbuzz \ |
||||
|
nss \ |
||||
|
freetype \ |
||||
|
ttf-freefont \ |
||||
|
mesa-gl \ |
||||
|
alsa-lib |
||||
|
|
||||
|
|
||||
|
# Set environment variables for Chrome |
||||
|
ENV CHROME_BIN=/usr/bin/chromium-browser |
||||
|
ENV CHROME_DRIVER=/usr/bin/chromedriver |
||||
|
|
||||
|
# install dependencies |
||||
|
RUN pip install --upgrade pip |
||||
|
#RUN python -m pip install Pillow |
||||
|
|
||||
|
COPY ./requirements.txt . |
||||
|
COPY .env.prod .env |
||||
|
|
||||
|
RUN --mount=type=cache,target=/root/.cache pip install -r requirements.txt |
||||
|
|
||||
|
# copy entrypoint.sh |
||||
|
COPY ./entrypoint.sh . |
||||
|
RUN sed -i 's/\r$//g' /usr/src/app/entrypoint.sh |
||||
|
RUN chmod +x /usr/src/app/entrypoint.sh |
||||
|
|
||||
|
# copy project |
||||
|
COPY . . |
||||
|
# Set display port to avoid crash |
||||
|
ENV DISPLAY=:99 |
||||
|
# run entrypoint.sh |
||||
|
ENTRYPOINT ["/usr/src/app/entrypoint.sh"] |
||||
@ -0,0 +1,35 @@ |
|||||
|
pipeline { |
||||
|
environment { |
||||
|
develop_server_ip = '' |
||||
|
develop_server_name = '' |
||||
|
production_server_ip = "88.99.212.243" |
||||
|
production_server_name = "newhorizon_germany_001_server" |
||||
|
project_path = "/projects/imam-javad/imam-javad_backend" |
||||
|
version = "master" |
||||
|
gitBranch = "origin/master" |
||||
|
} |
||||
|
agent any |
||||
|
stages { |
||||
|
stage('deploy'){ |
||||
|
steps{ |
||||
|
script{ |
||||
|
if(gitBranch=="origin/master"){ |
||||
|
withCredentials([usernamePassword(credentialsId: production_server_name, usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) { |
||||
|
sh 'sshpass -p $PASSWORD ssh -p 1782 $USERNAME@$production_server_ip -o StrictHostKeyChecking=no "cd $project_path && ./runner.sh"' |
||||
|
|
||||
|
def lastCommit = sh(script: 'git log -1 --pretty=format:"%h - %s (%an)"', returnStdout: true).trim() |
||||
|
sh """ |
||||
|
curl -F chat_id=1457670318 \ |
||||
|
-F message_thread_id=6 \ |
||||
|
-F document=@/var/jenkins_home/jobs/${env.JOB_NAME}/builds/${env.BUILD_NUMBER}/log \ |
||||
|
-F caption='Project name: #${env.JOB_NAME} \nBuild status is ${currentBuild.currentResult} \nBuild url: ${BUILD_URL} \nLast Commit: ${lastCommit}' \ |
||||
|
https://api.telegram.org/bot7207581748:AAFeymryw7S44D86LYfWqYK-tSNeV3TOwBs/sendDocument |
||||
|
""" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,4 @@ |
|||||
|
|
||||
|
from .user import * |
||||
|
from .professor import * |
||||
|
from .student import * |
||||
@ -0,0 +1,56 @@ |
|||||
|
from django.contrib import admin |
||||
|
from django.contrib.auth.forms import UserChangeForm, UsernameField |
||||
|
from django.contrib.auth.admin import UserAdmin |
||||
|
from django.utils.translation import gettext_lazy as _ |
||||
|
from rest_framework.authtoken.models import TokenProxy |
||||
|
from ajaxdatatable.admin import AjaxDatatable |
||||
|
|
||||
|
from django.contrib import admin |
||||
|
from apps.account.models import User |
||||
|
from django import forms |
||||
|
from django.contrib import admin |
||||
|
from django.urls import path, reverse |
||||
|
from django.shortcuts import render, redirect |
||||
|
from django.contrib import messages |
||||
|
|
||||
|
from apps.account.models import ProfessorUser |
||||
|
|
||||
|
|
||||
|
|
||||
|
@admin.register(ProfessorUser) |
||||
|
class ProfessorUserAdmin(UserAdmin, AjaxDatatable): |
||||
|
list_display = ( |
||||
|
'email', 'fullname', 'user_type','last_login', 'date_joined', |
||||
|
) |
||||
|
ordering = 'last_login', |
||||
|
readonly_fields = ('date_joined',) |
||||
|
exclude = ('password', 'user_permissions') |
||||
|
add_fieldsets = ( |
||||
|
(None, { |
||||
|
'classes': ('wide',), |
||||
|
'fields': ('email', 'password1', 'password2'), |
||||
|
}), |
||||
|
) |
||||
|
search_fields = ( |
||||
|
'email', 'fullname', 'username', |
||||
|
) |
||||
|
fieldsets = ( |
||||
|
(_('Personal info'), {'fields': ('fullname', 'email', 'phone_number', 'avatar',)}), |
||||
|
(_('Permissions'), { |
||||
|
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'password'), |
||||
|
}), |
||||
|
(_('Important dates'), {'fields': ('last_login', 'date_joined', 'fcm')}), |
||||
|
) |
||||
|
|
||||
|
def save_model(self, request, obj, form, change): |
||||
|
if not change: |
||||
|
obj.set_password(form.cleaned_data['password1']) |
||||
|
obj.user_type = User.UserType.PROFESSOR |
||||
|
super().save_model(request, obj, form, change) |
||||
|
|
||||
|
@admin.display(description='Phone Number') |
||||
|
def _phone_number(self, obj): |
||||
|
return obj.phone_number |
||||
|
|
||||
|
|
||||
|
# admin.site.unregister(TokenProxy) |
||||
@ -0,0 +1,56 @@ |
|||||
|
from django.contrib import admin |
||||
|
from django.contrib.auth.forms import UserChangeForm, UsernameField |
||||
|
from django.contrib.auth.admin import UserAdmin |
||||
|
from django.utils.translation import gettext_lazy as _ |
||||
|
from rest_framework.authtoken.models import TokenProxy |
||||
|
from ajaxdatatable.admin import AjaxDatatable |
||||
|
|
||||
|
from django.contrib import admin |
||||
|
from apps.account.models import User |
||||
|
from django import forms |
||||
|
from django.contrib import admin |
||||
|
from django.urls import path, reverse |
||||
|
from django.shortcuts import render, redirect |
||||
|
from django.contrib import messages |
||||
|
|
||||
|
from apps.account.models import StudentUser |
||||
|
|
||||
|
|
||||
|
|
||||
|
@admin.register(StudentUser) |
||||
|
class StudentUserAdmin(UserAdmin, AjaxDatatable): |
||||
|
list_display = ( |
||||
|
'email', 'fullname', 'user_type','last_login', 'date_joined', |
||||
|
) |
||||
|
ordering = 'last_login', |
||||
|
readonly_fields = ('date_joined',) |
||||
|
exclude = ('password', 'user_permissions') |
||||
|
add_fieldsets = ( |
||||
|
(None, { |
||||
|
'classes': ('wide',), |
||||
|
'fields': ('email', 'password1', 'password2'), |
||||
|
}), |
||||
|
) |
||||
|
search_fields = ( |
||||
|
'email', 'fullname', 'username', |
||||
|
) |
||||
|
fieldsets = ( |
||||
|
(_('Personal info'), {'fields': ('fullname', 'email', 'phone_number', 'avatar',)}), |
||||
|
(_('Permissions'), { |
||||
|
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'password'), |
||||
|
}), |
||||
|
(_('Important dates'), {'fields': ('last_login', 'date_joined', 'fcm')}), |
||||
|
) |
||||
|
|
||||
|
def save_model(self, request, obj, form, change): |
||||
|
if not change: |
||||
|
obj.set_password(form.cleaned_data['password1']) |
||||
|
obj.user_type = User.UserType.PROFESSOR |
||||
|
super().save_model(request, obj, form, change) |
||||
|
|
||||
|
@admin.display(description='Phone Number') |
||||
|
def _phone_number(self, obj): |
||||
|
return obj.phone_number |
||||
|
|
||||
|
|
||||
|
# admin.site.unregister(TokenProxy) |
||||
@ -0,0 +1,94 @@ |
|||||
|
from django.contrib import admin |
||||
|
from django.contrib.auth.forms import UserChangeForm, UsernameField |
||||
|
from django.contrib.auth.admin import UserAdmin |
||||
|
from django.utils.translation import gettext_lazy as _ |
||||
|
from rest_framework.authtoken.models import TokenProxy |
||||
|
from ajaxdatatable.admin import AjaxDatatable |
||||
|
|
||||
|
from apps.account.models import User |
||||
|
from django import forms |
||||
|
from django.contrib import admin |
||||
|
from django.urls import path, reverse |
||||
|
from django.shortcuts import render, redirect |
||||
|
from django.contrib import messages |
||||
|
|
||||
|
from apps.account.models import ClientUser, AdminUser |
||||
|
|
||||
|
|
||||
|
|
||||
|
@admin.register(User) |
||||
|
class UserAdmin(UserAdmin, AjaxDatatable): |
||||
|
list_display = ( |
||||
|
'email', 'fullname', 'user_type','last_login', 'date_joined', |
||||
|
) |
||||
|
ordering = 'last_login', |
||||
|
readonly_fields = ('date_joined',) |
||||
|
exclude = ('password', 'user_permissions') |
||||
|
add_fieldsets = ( |
||||
|
(None, { |
||||
|
'classes': ('wide',), |
||||
|
'fields': ('email', 'password1', 'password2'), |
||||
|
}), |
||||
|
) |
||||
|
search_fields = ( |
||||
|
'email', 'fullname', 'username', |
||||
|
) |
||||
|
fieldsets = ( |
||||
|
(_('Personal info'), {'fields': ('fullname', 'email', 'phone_number', 'avatar',)}), |
||||
|
(_('Permissions'), { |
||||
|
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'password'), |
||||
|
}), |
||||
|
(_('Important dates'), {'fields': ('last_login', 'date_joined', 'fcm')}), |
||||
|
) |
||||
|
|
||||
|
def save_model(self, request, obj, form, change): |
||||
|
if not change: |
||||
|
obj.set_password(form.cleaned_data['password1']) |
||||
|
|
||||
|
# obj.user_type = User.UserType.CLIENT |
||||
|
super().save_model(request, obj, form, change) |
||||
|
|
||||
|
@admin.display(description='Phone Number') |
||||
|
def _phone_number(self, obj): |
||||
|
return obj.phone_number |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
@admin.register(AdminUser) |
||||
|
class AdminUserAdmin(UserAdmin, AjaxDatatable): |
||||
|
list_display = ( |
||||
|
'email', 'fullname', 'user_type','last_login', 'date_joined', |
||||
|
) |
||||
|
ordering = 'last_login', |
||||
|
readonly_fields = ('date_joined',) |
||||
|
exclude = ('password', 'user_permissions') |
||||
|
add_fieldsets = ( |
||||
|
(None, { |
||||
|
'classes': ('wide',), |
||||
|
'fields': ('email', 'password1', 'password2'), |
||||
|
}), |
||||
|
) |
||||
|
search_fields = ( |
||||
|
'email', 'fullname', 'username', |
||||
|
) |
||||
|
fieldsets = ( |
||||
|
(_('Personal info'), {'fields': ('fullname', 'email', 'phone_number', 'avatar',)}), |
||||
|
(_('Permissions'), { |
||||
|
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'password'), |
||||
|
}), |
||||
|
(_('Important dates'), {'fields': ('last_login', 'date_joined', 'fcm')}), |
||||
|
) |
||||
|
|
||||
|
def save_model(self, request, obj, form, change): |
||||
|
if not change: |
||||
|
obj.set_password(form.cleaned_data['password1']) |
||||
|
|
||||
|
# obj.user_type = User.UserType.CLIENT |
||||
|
super().save_model(request, obj, form, change) |
||||
|
|
||||
|
@admin.display(description='Phone Number') |
||||
|
def _phone_number(self, obj): |
||||
|
return obj.phone_number |
||||
|
|
||||
|
admin.site.unregister(TokenProxy) |
||||
@ -0,0 +1,7 @@ |
|||||
|
from django.apps import AppConfig |
||||
|
|
||||
|
|
||||
|
class AccountConfig(AppConfig): |
||||
|
default_auto_field = 'django.db.models.BigAutoField' |
||||
|
name = 'apps.account' |
||||
|
icon = 'mi-person' |
||||
@ -0,0 +1,25 @@ |
|||||
|
from django.contrib.auth.backends import BaseBackend |
||||
|
from django.db.models import Q |
||||
|
|
||||
|
from apps.account.models import User |
||||
|
from utils.exceptions import UserNotFoundException |
||||
|
from rest_framework.exceptions import AuthenticationFailed |
||||
|
|
||||
|
|
||||
|
class CustomLoginBackend(BaseBackend): |
||||
|
""" |
||||
|
Authenticate with username email and phone_number. |
||||
|
""" |
||||
|
|
||||
|
def authenticate(self, request, username=None, password=None): |
||||
|
if user := self.get_user(username): |
||||
|
if user.check_password(password): |
||||
|
return user |
||||
|
|
||||
|
return None |
||||
|
|
||||
|
def get_user(self, username): |
||||
|
try: |
||||
|
return User.objects.filter(Q(email=username) | Q(phone_number=username)).first() |
||||
|
except Exception.DoesNotExist: |
||||
|
return None |
||||
@ -0,0 +1,52 @@ |
|||||
|
from django.core.management.base import BaseCommand |
||||
|
from django.contrib.auth.models import Group, Permission |
||||
|
from django.contrib.contenttypes.models import ContentType |
||||
|
|
||||
|
from apps.account.models import User |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class Command(BaseCommand): |
||||
|
help = 'Create default groups and assign permissions to them' |
||||
|
|
||||
|
def handle(self, *args, **kwargs): |
||||
|
# تعریف گروهها و پرمیشنها |
||||
|
groups_permissions = { |
||||
|
"Professor Group": [ |
||||
|
"view_user", "add_user", "change_user" |
||||
|
], |
||||
|
"Client Group": [ |
||||
|
"view_user" |
||||
|
], |
||||
|
"Admin Group": [ |
||||
|
"view_user", "add_user", "change_user", "delete_user" |
||||
|
], |
||||
|
"Super Admin Group": [ |
||||
|
"view_user", "add_user", "change_user", "delete_user", "manage_permissions" |
||||
|
], |
||||
|
"Student Group": [ |
||||
|
"view_user" |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
content_type = ContentType.objects.get_for_model(User) |
||||
|
|
||||
|
for group_name, permissions in groups_permissions.items(): |
||||
|
group, created = Group.objects.get_or_create(name=group_name) |
||||
|
if created: |
||||
|
self.stdout.write(self.style.SUCCESS(f"Group '{group_name}' created successfully.")) |
||||
|
else: |
||||
|
self.stdout.write(self.style.WARNING(f"Group '{group_name}' already exists.")) |
||||
|
|
||||
|
for perm_codename in permissions: |
||||
|
permission, created = Permission.objects.get_or_create( |
||||
|
codename=perm_codename, |
||||
|
defaults={ |
||||
|
'name': f"Can {perm_codename.replace('_', ' ')} User", |
||||
|
'content_type': content_type |
||||
|
} |
||||
|
) |
||||
|
group.permissions.add(permission) |
||||
|
|
||||
|
self.stdout.write(self.style.SUCCESS("All groups and permissions have been created successfully.")) |
||||
@ -0,0 +1,83 @@ |
|||||
|
|
||||
|
from django.contrib.auth.models import BaseUserManager |
||||
|
|
||||
|
|
||||
|
from django.db.models import Manager |
||||
|
|
||||
|
|
||||
|
|
||||
|
class UserManager(BaseUserManager): |
||||
|
|
||||
|
def create_user( |
||||
|
self, |
||||
|
email: str = None, |
||||
|
fullname: str = None, |
||||
|
password: str = None, |
||||
|
**extra_fields |
||||
|
): |
||||
|
email = UserManager.normalize_email(email) |
||||
|
user = self.model( |
||||
|
email=email, |
||||
|
fullname=fullname, |
||||
|
**extra_fields |
||||
|
) |
||||
|
user.set_password(password) |
||||
|
user.save(using=self._db) |
||||
|
return user |
||||
|
|
||||
|
def create_superuser(self, email, fullname, password): |
||||
|
user = self.create_user( |
||||
|
email=email, |
||||
|
fullname=fullname, |
||||
|
password=password, |
||||
|
) |
||||
|
user.is_admin = True |
||||
|
user.is_staff = True |
||||
|
user.is_superuser = True |
||||
|
user.is_active = True |
||||
|
user.user_type="super_admin" |
||||
|
user.save(using=self._db) |
||||
|
return user |
||||
|
|
||||
|
|
||||
|
def change_user_type(self, new_user_type): |
||||
|
# حذف گروههای فعلی |
||||
|
old_group_name = f"{self.user_type.capitalize()} Group" |
||||
|
old_group = Group.objects.filter(name=old_group_name).first() |
||||
|
if old_group: |
||||
|
self.groups.remove(old_group) |
||||
|
|
||||
|
# تغییر نوع کاربر |
||||
|
self.user_type = new_user_type |
||||
|
|
||||
|
# افزودن گروه جدید |
||||
|
new_group_name = f"{new_user_type.capitalize()} Group" |
||||
|
new_group, _ = Group.objects.get_or_create(name=new_group_name) |
||||
|
self.groups.add(new_group) |
||||
|
|
||||
|
# ذخیره تغییرات |
||||
|
self.save() |
||||
|
|
||||
|
|
||||
|
|
||||
|
class ProfessorUserManager(UserManager): |
||||
|
def get_queryset(self): |
||||
|
return super().get_queryset().filter(user_type="professor") |
||||
|
|
||||
|
|
||||
|
class ClientUserManager(UserManager): |
||||
|
def get_queryset(self): |
||||
|
return super().get_queryset().filter(user_type="client") |
||||
|
|
||||
|
class AdminUserManager(UserManager): |
||||
|
def get_queryset(self): |
||||
|
return super().get_queryset().filter(user_type="admin") |
||||
|
|
||||
|
|
||||
|
class SuperAdminUserManager(UserManager): |
||||
|
def get_queryset(self): |
||||
|
return super().get_queryset().filter(user_type="super_admin") |
||||
|
|
||||
|
class StudentUserManager(UserManager): |
||||
|
def get_queryset(self): |
||||
|
return super().get_queryset().filter(user_type="student") |
||||
@ -0,0 +1,116 @@ |
|||||
|
# Generated by Django 3.2.4 on 2024-11-19 08:43 |
||||
|
|
||||
|
import dj_language.field |
||||
|
from django.db import migrations, models |
||||
|
import django.db.models.deletion |
||||
|
import phonenumber_field.modelfields |
||||
|
import utils.validators |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
initial = True |
||||
|
|
||||
|
dependencies = [ |
||||
|
('dj_language', '0002_auto_20220120_1344'), |
||||
|
('auth', '0012_alter_user_first_name_max_length'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.CreateModel( |
||||
|
name='User', |
||||
|
fields=[ |
||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
|
('password', models.CharField(max_length=128, verbose_name='password')), |
||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), |
||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), |
||||
|
('email', models.EmailField(help_text="Enter the user's email address.", max_length=254, unique=True, verbose_name='Email Address')), |
||||
|
('fullname', models.CharField(help_text='Enter the full name of the user.', max_length=255, verbose_name='Full Name')), |
||||
|
('birthdate', models.DateField(verbose_name='birthdate')), |
||||
|
('avatar', models.ImageField(blank=True, null=True, upload_to='users/avatars/%Y/%m/')), |
||||
|
('phone_number', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None, unique=True, validators=[utils.validators.validate_possible_number], verbose_name='phone')), |
||||
|
('gender', models.CharField(blank=True, choices=[('male', 'Male'), ('female', 'Female')], help_text="Select the user's gender.", max_length=20, null=True, verbose_name='Gender')), |
||||
|
('user_type', models.CharField(choices=[('professor', 'Professor'), ('client', 'Client'), ('student', 'Student'), ('admin', 'Admin'), ('super_admin', 'Super Admin')], default='client', help_text='Type of the user.', max_length=20, verbose_name='User Type')), |
||||
|
('device_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='device id')), |
||||
|
('fcm', models.CharField(blank=True, max_length=512, null=True)), |
||||
|
('date_joined', models.DateTimeField(auto_now_add=True, help_text='The date and time the user registered.', verbose_name='Date Joined')), |
||||
|
('is_staff', models.BooleanField(default=False)), |
||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='Active')), |
||||
|
('deleted_at', models.DateTimeField(blank=True, null=True)), |
||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), |
||||
|
('language', dj_language.field.LanguageField(default=69, limit_choices_to={'status': True}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='dj_language.language', verbose_name='language')), |
||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), |
||||
|
], |
||||
|
options={ |
||||
|
'verbose_name': 'user', |
||||
|
'verbose_name_plural': 'users', |
||||
|
'ordering': ('-id',), |
||||
|
}, |
||||
|
), |
||||
|
migrations.CreateModel( |
||||
|
name='AdminUser', |
||||
|
fields=[ |
||||
|
], |
||||
|
options={ |
||||
|
'verbose_name': 'Admin User', |
||||
|
'verbose_name_plural': 'Admin Users', |
||||
|
'proxy': True, |
||||
|
'indexes': [], |
||||
|
'constraints': [], |
||||
|
}, |
||||
|
bases=('account.user',), |
||||
|
), |
||||
|
migrations.CreateModel( |
||||
|
name='ClientUser', |
||||
|
fields=[ |
||||
|
], |
||||
|
options={ |
||||
|
'verbose_name': 'user', |
||||
|
'verbose_name_plural': 'users', |
||||
|
'ordering': ('-id',), |
||||
|
'proxy': True, |
||||
|
'indexes': [], |
||||
|
'constraints': [], |
||||
|
}, |
||||
|
bases=('account.user',), |
||||
|
), |
||||
|
migrations.CreateModel( |
||||
|
name='ProfessorUser', |
||||
|
fields=[ |
||||
|
], |
||||
|
options={ |
||||
|
'verbose_name': 'Professor User', |
||||
|
'verbose_name_plural': 'Professor Users', |
||||
|
'proxy': True, |
||||
|
'indexes': [], |
||||
|
'constraints': [], |
||||
|
}, |
||||
|
bases=('account.user',), |
||||
|
), |
||||
|
migrations.CreateModel( |
||||
|
name='StudentUser', |
||||
|
fields=[ |
||||
|
], |
||||
|
options={ |
||||
|
'verbose_name': 'Student User', |
||||
|
'verbose_name_plural': 'Student Users', |
||||
|
'proxy': True, |
||||
|
'indexes': [], |
||||
|
'constraints': [], |
||||
|
}, |
||||
|
bases=('account.user',), |
||||
|
), |
||||
|
migrations.CreateModel( |
||||
|
name='SuperAdminUser', |
||||
|
fields=[ |
||||
|
], |
||||
|
options={ |
||||
|
'verbose_name': 'Super Admin User', |
||||
|
'verbose_name_plural': 'Super Admin Users', |
||||
|
'proxy': True, |
||||
|
'indexes': [], |
||||
|
'constraints': [], |
||||
|
}, |
||||
|
bases=('account.user',), |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1,18 @@ |
|||||
|
# Generated by Django 3.2.4 on 2024-11-19 08:50 |
||||
|
|
||||
|
from django.db import migrations, models |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
('account', '0001_initial'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.AlterField( |
||||
|
model_name='user', |
||||
|
name='birthdate', |
||||
|
field=models.DateField(blank=True, null=True, verbose_name='birthdate'), |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1,22 @@ |
|||||
|
# Generated by Django 3.2.4 on 2024-11-20 17:41 |
||||
|
|
||||
|
from django.db import migrations, models |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
('account', '0002_alter_user_birthdate'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.AlterModelOptions( |
||||
|
name='user', |
||||
|
options={'ordering': ('-id',), 'verbose_name': 'All Users', 'verbose_name_plural': 'All Users'}, |
||||
|
), |
||||
|
migrations.AddField( |
||||
|
model_name='user', |
||||
|
name='info', |
||||
|
field=models.TextField(blank=True, null=True, verbose_name='Info'), |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1,4 @@ |
|||||
|
|
||||
|
|
||||
|
from .user import * |
||||
|
from .groups import * |
||||
@ -0,0 +1,95 @@ |
|||||
|
from apps.account.models import User |
||||
|
from apps.account.manager import * |
||||
|
|
||||
|
from django.contrib.auth.models import Group |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class ProfessorUser(User): |
||||
|
objects = ProfessorUserManager() |
||||
|
|
||||
|
def save(self, *args, **kwargs): |
||||
|
self.user_type = User.UserType.PROFESSOR |
||||
|
super().save(*args, **kwargs) |
||||
|
|
||||
|
group, _ = Group.objects.get_or_create(name="Professor Group") |
||||
|
self.groups.add(group) |
||||
|
|
||||
|
class Meta: |
||||
|
proxy = True |
||||
|
verbose_name = "Professor User" |
||||
|
verbose_name_plural = "Professor Users" |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class ClientUser(User): |
||||
|
objects = ClientUserManager() |
||||
|
|
||||
|
def save(self, *args, **kwargs): |
||||
|
self.user_type = User.UserType.CLIENT |
||||
|
super().save(*args, **kwargs) |
||||
|
|
||||
|
group, _ = Group.objects.get_or_create(name="Client Group") |
||||
|
self.groups.add(group) |
||||
|
|
||||
|
|
||||
|
class Meta: |
||||
|
proxy = True |
||||
|
|
||||
|
verbose_name = 'user' |
||||
|
verbose_name_plural = 'users' |
||||
|
ordering = ('-id',) |
||||
|
|
||||
|
|
||||
|
|
||||
|
class AdminUser(User): |
||||
|
objects = AdminUserManager() |
||||
|
|
||||
|
def save(self, *args, **kwargs): |
||||
|
self.user_type = User.UserType.ADMIN |
||||
|
super().save(*args, **kwargs) |
||||
|
|
||||
|
group, _ = Group.objects.get_or_create(name="Admin Group") |
||||
|
self.groups.add(group) |
||||
|
|
||||
|
class Meta: |
||||
|
proxy = True |
||||
|
verbose_name = "Admin User" |
||||
|
verbose_name_plural = "Admin Users" |
||||
|
|
||||
|
|
||||
|
|
||||
|
class SuperAdminUser(User): |
||||
|
objects = SuperAdminUserManager() |
||||
|
|
||||
|
def save(self, *args, **kwargs): |
||||
|
self.user_type = User.UserType.SUPER_ADMIN |
||||
|
self.is_staff = True |
||||
|
super().save(*args, **kwargs) |
||||
|
|
||||
|
|
||||
|
|
||||
|
class Meta: |
||||
|
proxy = True |
||||
|
verbose_name = "Super Admin User" |
||||
|
verbose_name_plural = "Super Admin Users" |
||||
|
|
||||
|
|
||||
|
|
||||
|
class StudentUser(User): |
||||
|
objects = StudentUserManager() |
||||
|
|
||||
|
def save(self, *args, **kwargs): |
||||
|
self.user_type = User.UserType.STUDENT |
||||
|
super().save(*args, **kwargs) |
||||
|
|
||||
|
group, _ = Group.objects.get_or_create(name="Student Group") |
||||
|
self.groups.add(group) |
||||
|
|
||||
|
class Meta: |
||||
|
proxy = True |
||||
|
verbose_name = "Student User" |
||||
|
verbose_name_plural = "Student Users" |
||||
@ -0,0 +1,84 @@ |
|||||
|
import random |
||||
|
from dj_language.field import LanguageField |
||||
|
from django.contrib.auth.models import AbstractUser |
||||
|
from django.db import models |
||||
|
from django.utils.translation import gettext_lazy as _ |
||||
|
from django.utils import timezone |
||||
|
from phonenumber_field.modelfields import PhoneNumberField |
||||
|
from utils.validators import validate_possible_number |
||||
|
from apps.account.manager import UserManager |
||||
|
|
||||
|
|
||||
|
|
||||
|
class User(AbstractUser): |
||||
|
class UserType(models.TextChoices): |
||||
|
PROFESSOR = 'professor', 'Professor' |
||||
|
CLIENT = 'client', 'Client' |
||||
|
STUDENT = 'student', "Student" |
||||
|
ADMIN = 'admin', 'Admin' |
||||
|
SUPER_ADMIN = 'super_admin', 'Super Admin' |
||||
|
|
||||
|
class GenderChoices(models.TextChoices): |
||||
|
MALE = 'male', 'Male' |
||||
|
FEMALE = 'female', 'Female' |
||||
|
|
||||
|
email = models.EmailField(unique=True, verbose_name="Email Address", help_text="Enter the user's email address.") |
||||
|
fullname = models.CharField(max_length=255, verbose_name="Full Name", help_text="Enter the full name of the user.") |
||||
|
birthdate = models.DateField(verbose_name=_('birthdate'), null=True, blank=True) |
||||
|
|
||||
|
avatar = models.ImageField(null=True, blank=True, upload_to='users/avatars/%Y/%m/') |
||||
|
phone_number = PhoneNumberField(unique=True, validators=[validate_possible_number], null=True, blank=True, verbose_name=_('phone')) |
||||
|
language = LanguageField(null=True) |
||||
|
username = None |
||||
|
last_name = None |
||||
|
first_name = None |
||||
|
gender = models.CharField( |
||||
|
max_length=20, choices=GenderChoices.choices, null=True, blank=True, verbose_name=_('Gender'), help_text="Select the user's gender." |
||||
|
) |
||||
|
user_type = models.CharField( |
||||
|
max_length=20, |
||||
|
choices=UserType.choices, |
||||
|
default=UserType.CLIENT, |
||||
|
verbose_name="User Type", |
||||
|
help_text="Type of the user." |
||||
|
) |
||||
|
device_id = models.CharField(verbose_name=_('device id'), max_length=255, null=True, blank=True) |
||||
|
fcm = models.CharField(max_length=512, null=True, blank=True) |
||||
|
date_joined = models.DateTimeField(auto_now_add=True, verbose_name="Date Joined", help_text="The date and time the user registered.") |
||||
|
is_staff = models.BooleanField(default=False) |
||||
|
is_active = models.BooleanField(default=True, verbose_name="Active", help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.") |
||||
|
deleted_at = models.DateTimeField(null=True, blank=True) |
||||
|
info = models.TextField(verbose_name="Info", null=True, blank=True) |
||||
|
objects = UserManager() |
||||
|
|
||||
|
|
||||
|
EMAIL_FIELD = "email" |
||||
|
USERNAME_FIELD = "email" |
||||
|
REQUIRED_FIELDS = ["fullname", ] |
||||
|
|
||||
|
def soft_delete(self): |
||||
|
self.deleted_at = timezone.now() |
||||
|
self.is_active = False |
||||
|
number = str(random.randint(1000000000, 9999999999)) # ایجاد یک عدد رندوم 10 رقمی |
||||
|
self.phone_number = f'{self.phone_number}:deleted{number}' |
||||
|
self.email = f'{self.email}:deleted{number}' if self.email else None |
||||
|
self.save() |
||||
|
|
||||
|
# def clean(self): |
||||
|
# super().clean() |
||||
|
# if self.email == "": |
||||
|
# # fix db uniqueness error bcz of django charfield null to empty string conversion |
||||
|
# self.email = None |
||||
|
|
||||
|
def __str__(self): |
||||
|
return f"{self.email} - {self.get_full_name()}" |
||||
|
|
||||
|
|
||||
|
def get_full_name(self): |
||||
|
return self.fullname |
||||
|
|
||||
|
|
||||
|
class Meta: |
||||
|
ordering = ("-id",) |
||||
|
verbose_name = "All Users" |
||||
|
verbose_name_plural = "All Users" |
||||
@ -0,0 +1,12 @@ |
|||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
from rest_framework.permissions import BasePermission |
||||
|
|
||||
|
|
||||
|
class IsActiveUser(BasePermission): |
||||
|
|
||||
|
def has_permission(self, request, view): |
||||
|
return request.user and request.user.is_active |
||||
@ -0,0 +1,2 @@ |
|||||
|
|
||||
|
from .user import * |
||||
@ -0,0 +1,152 @@ |
|||||
|
|
||||
|
from rest_framework import serializers |
||||
|
from rest_framework.authtoken.models import Token |
||||
|
from django.contrib.auth.password_validation import validate_password |
||||
|
from django.utils.translation import gettext_lazy as _ |
||||
|
from apps.account.models import User |
||||
|
from utils import FileFieldSerializer, absolute_url |
||||
|
from utils.validators import validate_type_code |
||||
|
|
||||
|
|
||||
|
|
||||
|
class UserProfileSerializer(serializers.ModelSerializer): |
||||
|
avatar = FileFieldSerializer(required=False) |
||||
|
password = serializers.CharField(write_only=True, required=False, validators=[validate_password]) |
||||
|
fullname = serializers.CharField(required=False) |
||||
|
class Meta: |
||||
|
model = User |
||||
|
fields = ['id', 'fullname', 'avatar', 'email', 'phone_number', 'password', 'info'] |
||||
|
read_only_fields = ['email', 'info'] |
||||
|
|
||||
|
# def validate_email(self, value): |
||||
|
# if User.objects.filter(email=value).exists(): |
||||
|
# raise serializers.ValidationError("This email is already registered.") |
||||
|
# return value |
||||
|
|
||||
|
def update(self, instance, validated_data): |
||||
|
password = validated_data.pop('password', None) |
||||
|
if password: |
||||
|
instance.set_password(password) |
||||
|
# Update other fields |
||||
|
for attr, value in validated_data.items(): |
||||
|
if value is not None: |
||||
|
setattr(instance, attr, value) |
||||
|
|
||||
|
instance.save() |
||||
|
return instance |
||||
|
|
||||
|
|
||||
|
class UserRegisterSerializer(serializers.ModelSerializer): |
||||
|
password_confirmation = serializers.CharField(write_only=True) |
||||
|
fcm = serializers.CharField(required=False) |
||||
|
device_id = serializers.CharField(required=False) |
||||
|
email = serializers.EmailField() |
||||
|
|
||||
|
class Meta: |
||||
|
model = User |
||||
|
fields = ['id','fullname', 'email', 'password', 'password_confirmation', 'fcm', 'device_id'] |
||||
|
extra_kwargs = { |
||||
|
'fullname': {'required': True,}, |
||||
|
'email': {'required': True,}, |
||||
|
'password': {'required': True,}, |
||||
|
'password_confirmation': {'required': True,}, |
||||
|
} |
||||
|
|
||||
|
def validate_email(self, value): |
||||
|
if User.objects.filter(email=value).exists(): |
||||
|
raise serializers.ValidationError("This email is already registered.") |
||||
|
return value |
||||
|
|
||||
|
|
||||
|
def validate(self, data): |
||||
|
password = data.get('password') |
||||
|
password_confirmation = data.get('password_confirmation') |
||||
|
if password and password_confirmation and password != password_confirmation: |
||||
|
raise serializers.ValidationError("Passwords do not match.") |
||||
|
if len(password) < 8: |
||||
|
raise serializers.ValidationError("Password must be at least 8 characters long.") |
||||
|
|
||||
|
data.pop('password_confirmation', None) |
||||
|
data.pop('fcm', None) |
||||
|
data.pop('device_id', None) |
||||
|
return data |
||||
|
|
||||
|
|
||||
|
|
||||
|
class UserVerifySerializer(serializers.ModelSerializer): |
||||
|
code = serializers.CharField(max_length=5, validators=[validate_type_code]) |
||||
|
email = serializers.EmailField() |
||||
|
|
||||
|
class Meta: |
||||
|
model = User |
||||
|
fields = ["email", "code"] |
||||
|
extra_kwargs = { |
||||
|
'email': {'required': True,}, |
||||
|
'code': {'required': True,}, |
||||
|
} |
||||
|
|
||||
|
|
||||
|
class UserLoginSerializer(serializers.ModelSerializer): |
||||
|
password = serializers.CharField(write_only=True) |
||||
|
token = serializers.CharField(allow_null=True, read_only=True, required=False) |
||||
|
fullname = serializers.CharField(allow_null=True, read_only=True, required=False) |
||||
|
avatar = serializers.CharField(allow_null=True, read_only=True, required=False) |
||||
|
email = serializers.EmailField(write_only=True) |
||||
|
password = serializers.CharField(style={'input_type': 'password'}, trim_whitespace=False) |
||||
|
fcm = serializers.CharField(required=False) |
||||
|
device_id = serializers.CharField(required=False) |
||||
|
|
||||
|
|
||||
|
class Meta: |
||||
|
model = User |
||||
|
fields = ['id', 'phone_number', 'password', 'fullname', 'avatar', 'email', 'token', 'fcm', 'device_id'] |
||||
|
|
||||
|
def get_token(self, obj): |
||||
|
token, created = Token.objects.get_or_create(user=obj) |
||||
|
return token.key |
||||
|
|
||||
|
def validate(self, data): |
||||
|
data.pop('fcm', None) |
||||
|
data.pop('device_id', None) |
||||
|
return data |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class UserRecoverPasswordSerializer(serializers.ModelSerializer): |
||||
|
email = serializers.EmailField() |
||||
|
|
||||
|
class Meta: |
||||
|
model = User |
||||
|
fields = ['email',] |
||||
|
extra_kwargs = { |
||||
|
'email': {'required': True,}, |
||||
|
} |
||||
|
|
||||
|
|
||||
|
class UserResetPasswordSerializer(serializers.ModelSerializer): |
||||
|
password = serializers.CharField(write_only=True) |
||||
|
password_confirmation = serializers.CharField(write_only=True) |
||||
|
|
||||
|
class Meta: |
||||
|
model = User |
||||
|
fields = ['password', 'password_confirmation'] |
||||
|
extra_kwargs = { |
||||
|
'password': {'required': True,}, |
||||
|
'password_confirmation': {'required': True,}, |
||||
|
} |
||||
|
|
||||
|
|
||||
|
def validate(self, data): |
||||
|
password = data.get('password') |
||||
|
password_confirmation = data.get('password_confirmation') |
||||
|
if password and password_confirmation and password != password_confirmation: |
||||
|
raise serializers.ValidationError("Passwords do not match.") |
||||
|
if len(password) < 8: |
||||
|
raise serializers.ValidationError("Password must be at least 8 characters long.") |
||||
|
|
||||
|
data.pop('password_confirmation', None) |
||||
|
|
||||
|
return data |
||||
|
|
||||
|
|
||||
@ -0,0 +1,61 @@ |
|||||
|
import time |
||||
|
from config.settings import base as settings |
||||
|
|
||||
|
from celery import shared_task |
||||
|
import requests |
||||
|
import json |
||||
|
|
||||
|
@shared_task |
||||
|
def send_otp_code(phone_number, code): |
||||
|
BASE_URL_SERVICE = "https://console.melipayamak.com/api/send/simple/" |
||||
|
|
||||
|
phone_number = str(phone_number) |
||||
|
code = str(code) |
||||
|
print(code) |
||||
|
data = {'from': '50004001410202', 'to': phone_number, 'text': code} |
||||
|
response = requests.post(f'{BASE_URL_SERVICE}{settings.OTP_SERIVCE_KEY}', |
||||
|
json=data) |
||||
|
|
||||
|
print(response.json()) |
||||
|
|
||||
|
|
||||
|
def send_otp_code_whatsapp(phone_number, code): |
||||
|
phone = phone_number |
||||
|
if phone.startswith('0'): |
||||
|
phone = phone[1:] |
||||
|
phone = '98' + phone |
||||
|
|
||||
|
urls = [ |
||||
|
"https://7103.api.greenapi.com/waInstance7103107557/sendMessage/dcc7cc469e274389aa3ea4d6dae9d4d126b8b07a09be41c28e", |
||||
|
"https://7103.api.greenapi.com/waInstance7103109151/sendMessage/ed9cbea884cc49fd8032862f1bceca2074f373540dca483382", |
||||
|
"https://7103.api.greenapi.com/waInstance7103109158/sendMessage/92d032caca1541799a4623cfcc86f449ea7f3205b30848eeab", |
||||
|
"https://7103.api.greenapi.com/waInstance7103109163/sendMessage/d31a08b5816c432daa6e256e181274d1d334e4256d3c4555a7", |
||||
|
|
||||
|
] |
||||
|
payload = { |
||||
|
"chatId": f"{phone}@c.us", |
||||
|
"message": f"Habib App --aqila-- {code}" |
||||
|
} |
||||
|
headers = { |
||||
|
'Content-Type': 'application/json' |
||||
|
} |
||||
|
|
||||
|
for url in urls: |
||||
|
response = requests.request("POST", url=url, headers=headers, data=json.dumps(payload)) |
||||
|
response.encoding = 'utf-8' |
||||
|
response_data = response.json() |
||||
|
|
||||
|
invoke_status = response_data.get('invokeStatus', {}) |
||||
|
status = invoke_status.get('status', '') |
||||
|
|
||||
|
print(f'>>>>>>>> {response_data}') |
||||
|
print(f"Response: {status}") |
||||
|
|
||||
|
if status != "QUOTE_ALLOWED": |
||||
|
print("OTP sent successfully.") |
||||
|
break |
||||
|
else: |
||||
|
print("QUOTE_ALLOWED error, trying next URL...") |
||||
|
time.sleep(2) |
||||
|
|
||||
|
|
||||
@ -0,0 +1,3 @@ |
|||||
|
from django.test import TestCase |
||||
|
|
||||
|
# Create your tests here. |
||||
@ -0,0 +1,35 @@ |
|||||
|
|
||||
|
from django.urls import path, include |
||||
|
|
||||
|
from rest_framework.routers import DefaultRouter |
||||
|
|
||||
|
from apps.account import views |
||||
|
|
||||
|
|
||||
|
|
||||
|
urlpatterns = [ |
||||
|
# URL for user registration, accepts POST requests for creating new user instances. |
||||
|
|
||||
|
path('register/', views.UserRegisterView.as_view(), name='user-register'), |
||||
|
path('verify/', views.UserVerifyView.as_view(), name='user-verify'), |
||||
|
path('login/', views.UserLoginView.as_view(), name='user-login'), |
||||
|
|
||||
|
|
||||
|
# path('notif/', views.NotificationListView.as_view(), name='user-notif'), |
||||
|
# path('notif/read', views.NotificationReadAllView.as_view(), name='user-notif-read-all'), |
||||
|
|
||||
|
|
||||
|
# # URL to get user details, supports GET for fetching user profile based on the provided token. |
||||
|
path('profile/', views.UserProfileView.as_view(), name='user-profile'), |
||||
|
|
||||
|
path('recover/', views.UserRecoverPassword.as_view(), name='user-recover'), |
||||
|
path('reset/', views.UserResetPassword.as_view(), name='user-reset'), |
||||
|
|
||||
|
|
||||
|
# # URL to update user details, supports PUT to update user fields like phone or email given a token. |
||||
|
path('profile/update/', views.UserUpdateView.as_view(), name='user-update'), |
||||
|
|
||||
|
# # delete user account |
||||
|
path('profile/delete/', views.UserDeleteView.as_view(), name='user-delete'), |
||||
|
|
||||
|
] |
||||
@ -0,0 +1 @@ |
|||||
|
from .user import * |
||||
@ -0,0 +1,238 @@ |
|||||
|
import logging |
||||
|
import requests |
||||
|
import json |
||||
|
from rest_framework.generics import CreateAPIView, RetrieveUpdateAPIView, GenericAPIView, RetrieveAPIView, UpdateAPIView, ListAPIView |
||||
|
from rest_framework.views import APIView |
||||
|
from rest_framework.response import Response |
||||
|
from rest_framework import status |
||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated |
||||
|
from rest_framework.authtoken.models import Token |
||||
|
from rest_framework.exceptions import AuthenticationFailed |
||||
|
from django.utils.translation import gettext_lazy as _ |
||||
|
from django.shortcuts import get_object_or_404 |
||||
|
from rest_framework.authtoken.models import Token |
||||
|
|
||||
|
from django.utils import timezone |
||||
|
from rest_framework.authentication import TokenAuthentication |
||||
|
from django.contrib.auth import authenticate |
||||
|
from phonenumbers import parse, region_code_for_number |
||||
|
from drf_yasg.utils import swagger_auto_schema |
||||
|
from drf_yasg import openapi |
||||
|
|
||||
|
from utils.exceptions import InvaliedCodeVrify, ExpiredCodeException, ServiceUnavailableException |
||||
|
from apps.account.models import User |
||||
|
from apps.account.serializers import UserRegisterSerializer, UserProfileSerializer, UserVerifySerializer, UserLoginSerializer, UserRecoverPasswordSerializer, UserResetPasswordSerializer |
||||
|
from utils.redis import RedisManager |
||||
|
from utils import send_email, is_valid_email |
||||
|
from config.settings import base as settings |
||||
|
from apps.account.permissions import IsActiveUser |
||||
|
|
||||
|
logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class UserRegisterView(CreateAPIView): |
||||
|
permission_classes = [AllowAny] |
||||
|
serializer_class = UserRegisterSerializer |
||||
|
|
||||
|
|
||||
|
@swagger_auto_schema( |
||||
|
request_body=UserRegisterSerializer, |
||||
|
responses={201: 'User registered successfully', 400: 'Bad request'} |
||||
|
) |
||||
|
def post(self, request): |
||||
|
serializer = self.get_serializer(data=request.data) |
||||
|
serializer.is_valid(raise_exception=True) |
||||
|
data = serializer.data |
||||
|
|
||||
|
code = RedisManager.generate_otp_code() |
||||
|
logger.info(f"phone= {data['email']}") |
||||
|
print(f' send {code}/{data["email"]}') |
||||
|
phone_number = RedisManager().add_to_redis(code, **data) |
||||
|
|
||||
|
send_email([data['email']], code) |
||||
|
password = data.pop('password') |
||||
|
return Response( |
||||
|
data= { |
||||
|
"user": data, |
||||
|
"message": "The otp code was sent to the user's email" |
||||
|
}, |
||||
|
status=status.HTTP_202_ACCEPTED, |
||||
|
) |
||||
|
|
||||
|
|
||||
|
|
||||
|
class UserVerifyView(CreateAPIView): |
||||
|
permission_classes = [AllowAny] |
||||
|
serializer_class = UserVerifySerializer |
||||
|
|
||||
|
def create(self, request, *args, **kwargs): |
||||
|
serializer = self.get_serializer(data=request.data) |
||||
|
serializer.is_valid(raise_exception=True) |
||||
|
data = serializer.data |
||||
|
try: |
||||
|
verify_data = RedisManager().get_by_redis(data['email']) |
||||
|
if not verify_data: |
||||
|
raise ExpiredCodeException("Verification data not found or expired.") |
||||
|
except (ServiceUnavailableException) as e: |
||||
|
return Response({"detail": str(e)}, status=e.status_code) |
||||
|
except ExpiredCodeException: |
||||
|
raise ExpiredCodeException("The verification code has expired.") |
||||
|
|
||||
|
|
||||
|
code = self.valied_code(data['code'], verify_data['code']) |
||||
|
del verify_data['code'] |
||||
|
user = self.perform_create( |
||||
|
email=serializer.data['email'],**verify_data |
||||
|
) |
||||
|
Token.objects.filter(user=user).delete() |
||||
|
token = Token.objects.create(user=user) |
||||
|
return Response(data={ |
||||
|
'token': str(token), |
||||
|
'user_id': user.id, |
||||
|
'phone_number': str(user.phone_number), |
||||
|
'email': str(user.email), |
||||
|
'fullname': str(user.fullname), |
||||
|
'avatar': str(user.avatar) if user.avatar else None |
||||
|
}, status=status.HTTP_201_CREATED) |
||||
|
|
||||
|
def valied_code(self, current_code, save_code): |
||||
|
if (current_code and save_code) and ( current_code != save_code): |
||||
|
raise InvaliedCodeVrify() |
||||
|
return current_code |
||||
|
|
||||
|
def perform_create(self, *args, **kwargs): |
||||
|
email = kwargs.get('email') |
||||
|
user = User.objects.filter(email=email).first() |
||||
|
if user: |
||||
|
if kwargs['password']: |
||||
|
user.is_active = True |
||||
|
user.deletion_date = None |
||||
|
user.last_login = timezone.now() |
||||
|
user.set_password(kwargs['password']) |
||||
|
user.save() |
||||
|
else: |
||||
|
user = User.objects.create(**kwargs) |
||||
|
user.set_password(kwargs['password']) |
||||
|
user.last_login = timezone.now() |
||||
|
user.is_active = True |
||||
|
user.save() |
||||
|
|
||||
|
return user |
||||
|
|
||||
|
|
||||
|
class UserLoginView(CreateAPIView): |
||||
|
permission_classes = [AllowAny] |
||||
|
serializer_class = UserLoginSerializer |
||||
|
|
||||
|
def create(self, request, *args, **kwargs): |
||||
|
serializer = self.get_serializer(data=request.data) |
||||
|
serializer.is_valid(raise_exception=True) |
||||
|
data = serializer.data |
||||
|
user = authenticate(request, username=request.data['email'], password=data['password']) |
||||
|
if not user: |
||||
|
raise AuthenticationFailed(_('Unable to log in with provided credentials.')) |
||||
|
user.last_login = timezone.now() |
||||
|
user.is_active = True |
||||
|
user.save |
||||
|
token, created = Token.objects.get_or_create(user=user) |
||||
|
serializer_data = serializer.data |
||||
|
serializer_data['token'] = token.key |
||||
|
|
||||
|
return Response({ |
||||
|
"id": user.id, |
||||
|
"fullname": user.fullname, |
||||
|
"email": user.email, |
||||
|
"token": token.key, |
||||
|
"avatar": request.build_absolute_uri(user.avatar.url) if user.avatar else None, |
||||
|
}, status=status.HTTP_201_CREATED) |
||||
|
|
||||
|
|
||||
|
class UserProfileView(RetrieveAPIView): |
||||
|
serializer_class = UserProfileSerializer |
||||
|
permission_classes = [IsAuthenticated, IsActiveUser] |
||||
|
queryset = User.objects.all() |
||||
|
|
||||
|
def get_object(self): |
||||
|
return self.request.user |
||||
|
|
||||
|
|
||||
|
class UserUpdateView(UpdateAPIView): |
||||
|
permission_classes = [IsAuthenticated, IsActiveUser] |
||||
|
serializer_class = UserProfileSerializer |
||||
|
|
||||
|
def get_object(self): |
||||
|
return self.request.user |
||||
|
|
||||
|
|
||||
|
class UserRecoverPassword(CreateAPIView): |
||||
|
serializer_class = UserRecoverPasswordSerializer |
||||
|
|
||||
|
def post(self, request): |
||||
|
serializer = self.get_serializer(data=request.data) |
||||
|
serializer.is_valid(raise_exception=True) |
||||
|
data = serializer.data |
||||
|
user = get_object_or_404(User, email=data['email']) |
||||
|
code = RedisManager.generate_otp_code() |
||||
|
print(f' send {code}') |
||||
|
phone_number = RedisManager().add_to_redis(code, fullname=str(user.fullname), password='', email=data['email']) |
||||
|
|
||||
|
send_email([data['email']], code) |
||||
|
|
||||
|
return Response( |
||||
|
data= { |
||||
|
"id": user.id, |
||||
|
"fullname": user.fullname, |
||||
|
"phone_number": str(user.phone_number), |
||||
|
"email": user.email if user.email else None, |
||||
|
"avatar": user.avatar if user.avatar else None, |
||||
|
"message": "Forgot password code sent" |
||||
|
}, |
||||
|
status=status.HTTP_202_ACCEPTED, |
||||
|
) |
||||
|
|
||||
|
|
||||
|
class UserResetPassword(CreateAPIView): |
||||
|
serializer_class = UserResetPasswordSerializer |
||||
|
permission_classes = [IsAuthenticated] |
||||
|
|
||||
|
def post(self, request, *args, **kwargs): |
||||
|
# Get the logged-in user |
||||
|
user = request.user |
||||
|
|
||||
|
# Use the serializer to validate data |
||||
|
serializer = self.get_serializer(data=request.data) |
||||
|
serializer.is_valid(raise_exception=True) |
||||
|
|
||||
|
# Set the new password |
||||
|
user.set_password(serializer.validated_data['password']) |
||||
|
user.save() |
||||
|
|
||||
|
# Return a success response |
||||
|
return Response({"message": "Your password has been changed successfully."}, status=status.HTTP_200_OK) |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class UserDeleteView(APIView): |
||||
|
permission_classes = [IsAuthenticated] |
||||
|
|
||||
|
def delete(self, request, *args, **kwargs): |
||||
|
try: |
||||
|
user = request.user |
||||
|
if user.email == "admin@gmail.com": |
||||
|
return Response({"detail": "admin"}, status=status.HTTP_204_NO_CONTENT) |
||||
|
|
||||
|
user.soft_delete() |
||||
|
if t := Token.objects.filter(user=user).first(): |
||||
|
t.delete() |
||||
|
|
||||
|
return Response({"detail": "Your account has been deleted."}, status=status.HTTP_204_NO_CONTENT) |
||||
|
|
||||
|
except Exception: |
||||
|
# پیام خطای ثابت برای سایر خطاهای غیرمنتظره |
||||
|
return Response({"detail": "User does not exist."}, status=status.HTTP_404_NOT_FOUND) |
||||
|
|
||||
|
|
||||
@ -0,0 +1,3 @@ |
|||||
|
from django.contrib import admin |
||||
|
|
||||
|
# Register your models here. |
||||
@ -0,0 +1,6 @@ |
|||||
|
from django.apps import AppConfig |
||||
|
|
||||
|
|
||||
|
class ApiConfig(AppConfig): |
||||
|
default_auto_field = 'django.db.models.BigAutoField' |
||||
|
name = 'apps.api' |
||||
@ -0,0 +1,3 @@ |
|||||
|
from django.db import models |
||||
|
|
||||
|
# Create your models here. |
||||
@ -0,0 +1,3 @@ |
|||||
|
from django.test import TestCase |
||||
|
|
||||
|
# Create your tests here. |
||||
@ -0,0 +1,9 @@ |
|||||
|
|
||||
|
from django.urls import path |
||||
|
from .views import HomeView |
||||
|
|
||||
|
|
||||
|
|
||||
|
urlpatterns = [ |
||||
|
path('', HomeView.as_view()) |
||||
|
] |
||||
@ -0,0 +1,33 @@ |
|||||
|
import random |
||||
|
from rest_framework.generics import GenericAPIView |
||||
|
from rest_framework.response import Response |
||||
|
from rest_framework import serializers |
||||
|
|
||||
|
from rest_framework.authtoken.models import Token |
||||
|
from apps.account.models import User |
||||
|
|
||||
|
class HomeSerializer(serializers.Serializer): |
||||
|
token = serializers.CharField() |
||||
|
|
||||
|
# test class generate token |
||||
|
class HomeView(GenericAPIView): |
||||
|
serializer_class = HomeSerializer |
||||
|
|
||||
|
def get(self, request): |
||||
|
emails = ["zahra@gmail.com", "john.doe@example.com", "alice@example.com"] |
||||
|
phone_numbers = ["09012037621", "09012037615", "09012045432"] |
||||
|
fullnames = ["Alireza", "John Doe", "Alice Smith"] |
||||
|
# انتخاب رندوم از هر لیست |
||||
|
email = random.choice(emails) |
||||
|
phone_number = random.choice(phone_numbers) |
||||
|
fullname = random.choice(fullnames) |
||||
|
# ساخت کاربر جدید |
||||
|
user = User.objects.create( |
||||
|
email=email, |
||||
|
phone_number=phone_number, |
||||
|
fullname=fullname, |
||||
|
) |
||||
|
# ایجاد توکن برای کاربر |
||||
|
token, created = Token.objects.get_or_create(user=user) |
||||
|
|
||||
|
return Response({'token': token.key}) |
||||
@ -0,0 +1,2 @@ |
|||||
|
from .course import * |
||||
|
from .lesson import * |
||||
@ -0,0 +1,87 @@ |
|||||
|
from django.contrib import admin |
||||
|
from ajaxdatatable.admin import AjaxDatatable |
||||
|
|
||||
|
from apps.course.models import Course, Glossary, Attachment, CourseCategory |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
@admin.register(CourseCategory) |
||||
|
class CourseCategoryAdmin(admin.ModelAdmin): |
||||
|
list_display = ('name', 'slug') |
||||
|
search_fields = ('name',) |
||||
|
exclude = ('slug', ) |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
@admin.register(Course) |
||||
|
class CourseAdmin(AjaxDatatable): |
||||
|
list_display = ('title', 'category', 'level', 'status', 'final_price', 'is_online') |
||||
|
list_filter = ('status', 'level', 'is_online', 'is_free', 'category') |
||||
|
search_fields = ('title', 'description') |
||||
|
exclude = ('slug', ) |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
@admin.register(Glossary) |
||||
|
class GlossaryAdmin(admin.ModelAdmin): |
||||
|
list_display = ('title', 'course', 'description') |
||||
|
list_filter = ('course',) |
||||
|
search_fields = ('title', 'description', 'course__title') |
||||
|
ordering = ('-id',) |
||||
|
|
||||
|
|
||||
|
|
||||
|
from django import forms |
||||
|
import hashlib |
||||
|
import os |
||||
|
|
||||
|
|
||||
|
class AttachmentAdminForm(forms.ModelForm): |
||||
|
class Meta: |
||||
|
model = Attachment |
||||
|
fields = '__all__' |
||||
|
|
||||
|
def __init__(self, *args, **kwargs): |
||||
|
super().__init__(*args, **kwargs) |
||||
|
|
||||
|
if 'file' in self.data or 'file' in self.files: |
||||
|
file = self.files.get('file') |
||||
|
if file: |
||||
|
file.name = self._shorten_file_name(file.name) |
||||
|
|
||||
|
|
||||
|
def _shorten_file_name(self, file_name): |
||||
|
max_length = 100 |
||||
|
if len(file_name) > max_length: |
||||
|
base_name, ext = os.path.splitext(file_name) # جدا کردن نام و پسوند |
||||
|
allowed_length = max_length - len(ext) # طول مجاز نام بدون پسوند |
||||
|
|
||||
|
# 80٪ از نام اصلی و 20٪ هش |
||||
|
base_length = int(allowed_length * 0.8) # 80٪ از طول مجاز |
||||
|
hash_length = allowed_length - base_length # 20٪ از طول مجاز |
||||
|
|
||||
|
base_part = base_name[:base_length] # 80٪ اول نام اصلی |
||||
|
hash_part = hashlib.sha256(base_name.encode('utf-8')).hexdigest()[:hash_length] # 20٪ هش |
||||
|
|
||||
|
return f"{base_part}{hash_part}{ext}" # ترکیب بخش اصلی و هش با پسوند |
||||
|
|
||||
|
return file_name |
||||
|
|
||||
|
|
||||
|
@admin.register(Attachment) |
||||
|
class AttachmentAdmin(admin.ModelAdmin): |
||||
|
form = AttachmentAdminForm |
||||
|
list_display = ('title', 'course', 'file', 'file_size') |
||||
|
list_filter = ('course',) |
||||
|
search_fields = ('title', 'file', 'course__title') |
||||
|
|
||||
|
def save_model(self, request, obj, form, change): |
||||
|
if obj.file: |
||||
|
obj.file_size = obj.file.size |
||||
|
super().save_model(request, obj, form, change) |
||||
|
|
||||
|
|
||||
@ -0,0 +1,18 @@ |
|||||
|
|
||||
|
from django.contrib import admin |
||||
|
from apps.course.models import Lesson |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
@admin.register(Lesson) |
||||
|
class LessonAdmin(admin.ModelAdmin): |
||||
|
list_display = ('title', 'course', 'priority', 'duration', 'content_type') |
||||
|
list_filter = ('course', 'content_type') |
||||
|
search_fields = ('title', 'course__title') |
||||
|
ordering = ('priority', 'title') |
||||
|
|
||||
|
def get_queryset(self, request): |
||||
|
qs = super().get_queryset(request) |
||||
|
return qs.order_by('priority') |
||||
|
|
||||
@ -0,0 +1,6 @@ |
|||||
|
from django.apps import AppConfig |
||||
|
|
||||
|
|
||||
|
class CourseConfig(AppConfig): |
||||
|
default_auto_field = 'django.db.models.BigAutoField' |
||||
|
name = 'apps.course' |
||||
@ -0,0 +1,2 @@ |
|||||
|
from .course import * |
||||
|
from .lesson import * |
||||
@ -0,0 +1,166 @@ |
|||||
|
import os |
||||
|
from decimal import Decimal |
||||
|
import math |
||||
|
from django.db import models |
||||
|
from django.db.models import TextChoices |
||||
|
from django.utils.translation import gettext_lazy as _ |
||||
|
from filer.fields.image import FilerImageField |
||||
|
from filer.fields.file import FilerFileField |
||||
|
|
||||
|
from apps.account.models import ProfessorUser |
||||
|
from utils.schema import default_timing |
||||
|
from utils import generate_slug_for_model |
||||
|
|
||||
|
|
||||
|
|
||||
|
def course_file_upload_to(instance, filename): |
||||
|
return os.path.join(f"courses/{instance.slug}/videos/{filename}") |
||||
|
|
||||
|
|
||||
|
|
||||
|
def attachment_file_upload_to(instance, filename): |
||||
|
return os.path.join(f"courses/{instance.course.slug}/attachments/{filename}") |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class CourseCategory(models.Model): |
||||
|
name = models.CharField(max_length=255, verbose_name='Category Name') |
||||
|
slug = models.SlugField(unique=True, max_length=255) |
||||
|
|
||||
|
def __str__(self): |
||||
|
return self.name |
||||
|
|
||||
|
def save(self, *args, **kwargs): |
||||
|
self.slug = generate_slug_for_model(CourseCategory, self.name) |
||||
|
super().save(*args, **kwargs) |
||||
|
|
||||
|
@property |
||||
|
def course_count(self): |
||||
|
return self.courses.count() |
||||
|
|
||||
|
|
||||
|
|
||||
|
class Course(models.Model): |
||||
|
|
||||
|
class LevelChoices(TextChoices): |
||||
|
BEGINNER = 'beginner', 'Beginner' |
||||
|
MID = 'mid', 'Mid Level' |
||||
|
ADVANCED = 'advanced', 'Advanced' |
||||
|
|
||||
|
class StatusChoices(TextChoices): |
||||
|
INACTIVE = 'inactive', 'Inactive' # Not Active (does not show) |
||||
|
UPCOMING = 'upcoming', 'Upcoming' # Upcoming (visible but registration not allowed) |
||||
|
REGISTERING = 'registering', 'Registering' # Registering (registration is open) |
||||
|
ONGOING = 'ongoing', 'Ongoing' # Ongoing (course has started, registration closed) |
||||
|
FINISHED = 'finished', 'Finished' # Finished (course has ended) |
||||
|
|
||||
|
class VedioTypeChoices(models.TextChoices): |
||||
|
VIDEO_FILE = 'video_file', 'Video File' |
||||
|
VIDEO_LINK = 'video_link', 'Video Link' |
||||
|
|
||||
|
|
||||
|
title = models.CharField(max_length=255, verbose_name='Course Title') |
||||
|
slug = models.SlugField(allow_unicode=True, unique=True) |
||||
|
category = models.ForeignKey(CourseCategory, on_delete=models.CASCADE, related_name='courses', verbose_name='Category') |
||||
|
professor = models.ForeignKey( |
||||
|
ProfessorUser, |
||||
|
on_delete=models.CASCADE, |
||||
|
related_name="courses" |
||||
|
) |
||||
|
|
||||
|
thumbnail = FilerImageField( |
||||
|
related_name='+', on_delete=models.PROTECT, null=True, blank=True, |
||||
|
verbose_name=_('thumbnail') |
||||
|
) |
||||
|
video_type = models.CharField(max_length=20, choices=VedioTypeChoices.choices, verbose_name='Vedio Type') |
||||
|
video_file = models.FileField( |
||||
|
upload_to=course_file_upload_to, |
||||
|
null=True, |
||||
|
blank=True |
||||
|
) |
||||
|
video_link = models.CharField(max_length=500, null=True, blank=True, verbose_name='Video Link') |
||||
|
|
||||
|
is_online = models.BooleanField(default=True, verbose_name='Is Online Course') |
||||
|
level = models.CharField(max_length=10, choices=LevelChoices.choices, verbose_name='Course Level') |
||||
|
duration = models.PositiveIntegerField(verbose_name='Duration (in hours)') |
||||
|
lessons_count = models.PositiveIntegerField(verbose_name='Number of Lessons') |
||||
|
|
||||
|
description = models.TextField(verbose_name='Course Description') |
||||
|
short_description = models.CharField(max_length=500, blank=True, null=True, verbose_name="Short Description") |
||||
|
status = models.CharField(max_length=15, choices=StatusChoices.choices, default=StatusChoices.INACTIVE, verbose_name='Course Status') |
||||
|
is_free = models.BooleanField(default=True, verbose_name='Is Free') |
||||
|
price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='Course Price') |
||||
|
discount_percentage = models.PositiveIntegerField(default=0, verbose_name='Discount Percentage') |
||||
|
final_price = models.DecimalField( |
||||
|
verbose_name=_('Course Final Price'), decimal_places=2, max_digits=10, default=0.00, blank=True, |
||||
|
help_text=_('This field is automatically calculated based on the discount percentage.') |
||||
|
) |
||||
|
|
||||
|
timing = models.JSONField(blank=True, null=True, default=default_timing, verbose_name=_("Timing"), help_text=_("The Timing information in JSON format.")) |
||||
|
features = models.JSONField(verbose_name=_('Course features'), default=dict, blank=True, null=True) |
||||
|
|
||||
|
|
||||
|
def __str__(self): |
||||
|
return self.title |
||||
|
|
||||
|
|
||||
|
def save(self, *args, **kwargs): |
||||
|
self.slug = generate_slug_for_model(Course, self.title) |
||||
|
|
||||
|
if self.discount_percentage > 0: |
||||
|
discount_amount = (self.price * self.discount_percentage) / 100 |
||||
|
final_price = self.price - discount_amount |
||||
|
self.final_price = Decimal(math.ceil(final_price)).quantize(Decimal('0.00')) |
||||
|
else: |
||||
|
self.final_price = Decimal(math.ceil(self.price)).quantize(Decimal('0.00')) |
||||
|
|
||||
|
super().save(*args, **kwargs) |
||||
|
|
||||
|
|
||||
|
class Meta: |
||||
|
verbose_name = "Course" |
||||
|
verbose_name_plural = "Courses" |
||||
|
|
||||
|
|
||||
|
|
||||
|
class Glossary(models.Model): |
||||
|
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='glossaries', verbose_name='Course') |
||||
|
title = models.CharField(max_length=555, verbose_name='Glossary Title') |
||||
|
description = models.TextField(verbose_name='Description') |
||||
|
|
||||
|
def __str__(self): |
||||
|
return f"{self.course.title} - {self.title}" |
||||
|
|
||||
|
|
||||
|
class Meta: |
||||
|
ordering = ("-id",) |
||||
|
verbose_name = "Glossary" |
||||
|
verbose_name_plural = "Glossary" |
||||
|
|
||||
|
|
||||
|
|
||||
|
class Attachment(models.Model): |
||||
|
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='attachments', verbose_name='Course') |
||||
|
title = models.CharField(max_length=255, verbose_name='Attachment Title') |
||||
|
file = models.FileField( |
||||
|
upload_to=attachment_file_upload_to, |
||||
|
verbose_name='Attachment File' |
||||
|
) |
||||
|
|
||||
|
file_size = models.PositiveIntegerField(verbose_name='File Size (in bytes)', null=True, blank=True) |
||||
|
|
||||
|
def save(self, *args, **kwargs): |
||||
|
# Calculate the file size before saving |
||||
|
if self.file and not self.file_size: |
||||
|
self.file_size = self.file.size |
||||
|
super().save(*args, **kwargs) |
||||
|
|
||||
|
|
||||
|
def __str__(self): |
||||
|
return f"{self.course.title} - {self.title}" |
||||
|
|
||||
|
class Meta: |
||||
|
ordering = ("-id",) |
||||
|
verbose_name = "Attachment" |
||||
|
verbose_name_plural = "Attachments" |
||||
@ -0,0 +1,34 @@ |
|||||
|
import os |
||||
|
from django.db import models |
||||
|
|
||||
|
from filer.fields.image import FilerImageField |
||||
|
from filer.fields.file import FilerFileField |
||||
|
|
||||
|
|
||||
|
|
||||
|
def lesson_file_upload_to(instance, filename): |
||||
|
return os.path.join(f"courses/{instance.course.slug}/lessons/{filename}") |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class Lesson(models.Model): |
||||
|
class ContentTypeChoices(models.TextChoices): |
||||
|
LINK = 'link', 'Link' |
||||
|
FILE = 'file', 'File' |
||||
|
|
||||
|
course = models.ForeignKey("course.Course", on_delete=models.CASCADE, related_name='lessons', verbose_name='Course') |
||||
|
title = models.CharField(max_length=255, verbose_name='Lesson Title') |
||||
|
priority = models.IntegerField(null=True, blank=True, verbose_name='Priority') |
||||
|
duration = models.PositiveIntegerField(verbose_name='Duration (in minutes)') |
||||
|
content_type = models.CharField(max_length=10, choices=ContentTypeChoices.choices, verbose_name='Content Type') |
||||
|
content_file = models.FileField( |
||||
|
null=True, |
||||
|
blank=True, |
||||
|
upload_to=lesson_file_upload_to, |
||||
|
) |
||||
|
video_link = models.CharField(max_length=500, null=True, blank=True, verbose_name='Video Link') |
||||
|
|
||||
|
def __str__(self): |
||||
|
return f"{self.course.title} - {self.title}" |
||||
|
|
||||
@ -0,0 +1 @@ |
|||||
|
from .course import * |
||||
@ -0,0 +1,107 @@ |
|||||
|
from rest_framework import serializers |
||||
|
|
||||
|
from dj_filer.admin import get_thumbs |
||||
|
from apps.course.models import Course, CourseCategory, Attachment, Glossary |
||||
|
from apps.account.serializers import UserProfileSerializer |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class CourseCategorySerializer(serializers.ModelSerializer): |
||||
|
course_count = serializers.SerializerMethodField() |
||||
|
|
||||
|
class Meta: |
||||
|
model = CourseCategory |
||||
|
fields = ['name', 'slug', 'course_count'] |
||||
|
|
||||
|
def get_course_count(self, obj): |
||||
|
# return obj.course_count |
||||
|
return 25 |
||||
|
|
||||
|
|
||||
|
class CourseListSerializer(serializers.ModelSerializer): |
||||
|
category = CourseCategorySerializer() |
||||
|
thumbnail = serializers.SerializerMethodField() |
||||
|
participant_count = serializers.SerializerMethodField() |
||||
|
|
||||
|
class Meta: |
||||
|
model = Course |
||||
|
fields = [ |
||||
|
'id', |
||||
|
'title', |
||||
|
'slug', |
||||
|
'participant_count', |
||||
|
'category', |
||||
|
'thumbnail', |
||||
|
'is_online', |
||||
|
'level', |
||||
|
'duration', |
||||
|
'lessons_count', |
||||
|
'short_description', |
||||
|
'status', |
||||
|
'is_free', |
||||
|
'price', |
||||
|
'discount_percentage', |
||||
|
'final_price', |
||||
|
] |
||||
|
|
||||
|
def get_thumbnail(self, obj): |
||||
|
return get_thumbs(obj.thumbnail, self.context.get('request')) |
||||
|
|
||||
|
def get_participant_count(self, obj): |
||||
|
return 120 |
||||
|
|
||||
|
|
||||
|
|
||||
|
class CourseDetailSerializer(serializers.ModelSerializer): |
||||
|
category = CourseCategorySerializer() |
||||
|
professor = UserProfileSerializer() |
||||
|
thumbnail = serializers.SerializerMethodField() |
||||
|
participant_count = serializers.SerializerMethodField() |
||||
|
|
||||
|
class Meta: |
||||
|
model = Course |
||||
|
fields = [ |
||||
|
'id', |
||||
|
'title', |
||||
|
'slug', |
||||
|
'category', |
||||
|
'participant_count', |
||||
|
'professor', |
||||
|
'thumbnail', |
||||
|
'video_type', |
||||
|
'video_file', |
||||
|
'video_link', |
||||
|
'is_online', |
||||
|
'level', |
||||
|
'duration', |
||||
|
'lessons_count', |
||||
|
'short_description', |
||||
|
'status', |
||||
|
'is_free', |
||||
|
'price', |
||||
|
'discount_percentage', |
||||
|
'final_price', |
||||
|
'timing', |
||||
|
'features', |
||||
|
] |
||||
|
|
||||
|
def get_thumbnail(self, obj): |
||||
|
return get_thumbs(obj.thumbnail, self.context.get('request')) |
||||
|
|
||||
|
def get_participant_count(self, obj): |
||||
|
return 120 |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class AttachmentSerializer(serializers.ModelSerializer): |
||||
|
class Meta: |
||||
|
model = Attachment |
||||
|
fields = ['id', 'title', 'file', 'file_size'] |
||||
|
|
||||
|
|
||||
|
class GlossarySerializer(serializers.ModelSerializer): |
||||
|
class Meta: |
||||
|
model = Glossary |
||||
|
fields = ['id', 'title', 'description'] |
||||
@ -0,0 +1,3 @@ |
|||||
|
from django.test import TestCase |
||||
|
|
||||
|
# Create your tests here. |
||||
@ -0,0 +1,15 @@ |
|||||
|
|
||||
|
from django.urls import path |
||||
|
|
||||
|
from . import views |
||||
|
|
||||
|
|
||||
|
|
||||
|
urlpatterns = [ |
||||
|
path('categories/', views.CourseCategoryAPIView.as_view(), name='course-categories'), |
||||
|
path('', views.CourseListAPIView.as_view(), name='course-list'), |
||||
|
path('<slug:slug>/', views.CourseDetailAPIView.as_view(), name='course-detail'), |
||||
|
path('<slug:slug>/attachments/', views.AttachmentListAPIView.as_view(), name='course-attachment-list'), |
||||
|
path('<slug:slug>/glossaries/', views.GlossaryListAPIView.as_view(), name='course-glossary-list'), |
||||
|
|
||||
|
] |
||||
@ -0,0 +1 @@ |
|||||
|
from .course import * |
||||
@ -0,0 +1,98 @@ |
|||||
|
from rest_framework.generics import ListAPIView, RetrieveAPIView |
||||
|
from drf_yasg.utils import swagger_auto_schema |
||||
|
from drf_yasg import openapi |
||||
|
from rest_framework.exceptions import NotFound |
||||
|
|
||||
|
|
||||
|
from apps.course.serializers import ( |
||||
|
CourseListSerializer, CourseCategorySerializer, CourseDetailSerializer, |
||||
|
AttachmentSerializer, GlossarySerializer |
||||
|
) |
||||
|
from apps.course.models import Course, CourseCategory, Attachment, Glossary |
||||
|
|
||||
|
|
||||
|
|
||||
|
class CourseCategoryAPIView(ListAPIView): |
||||
|
queryset = CourseCategory.objects.all() |
||||
|
serializer_class = CourseCategorySerializer |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class CourseListAPIView(ListAPIView): |
||||
|
queryset = Course.objects.all().exclude(status=Course.StatusChoices.INACTIVE) |
||||
|
serializer_class = CourseListSerializer |
||||
|
# filterset_fields = ['category__slug',] |
||||
|
|
||||
|
|
||||
|
@swagger_auto_schema(manual_parameters=[ |
||||
|
openapi.Parameter( |
||||
|
'category_slug', openapi.IN_QUERY, |
||||
|
description="Category of the Course", |
||||
|
type=openapi.TYPE_STRING, |
||||
|
enum=[category.slug for category in CourseCategory.objects.all()] |
||||
|
), |
||||
|
]) |
||||
|
def get(self, request, *args, **kwargs): |
||||
|
return super().get(request, *args, **kwargs) |
||||
|
|
||||
|
def get_queryset(self): |
||||
|
queryset = super().get_queryset() |
||||
|
request = self.request |
||||
|
filters = request.query_params |
||||
|
if category := filters.get('category_slug'): |
||||
|
queryset = queryset.filter(category__slug=category) |
||||
|
|
||||
|
return queryset |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class CourseDetailAPIView(RetrieveAPIView): |
||||
|
queryset = Course.objects.all() |
||||
|
serializer_class = CourseDetailSerializer |
||||
|
lookup_field = "slug" |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class AttachmentListAPIView(ListAPIView): |
||||
|
serializer_class = AttachmentSerializer |
||||
|
|
||||
|
@swagger_auto_schema( |
||||
|
manual_parameters=[ |
||||
|
openapi.Parameter( |
||||
|
'slug', openapi.IN_PATH, |
||||
|
description="Slug of the Course", |
||||
|
type=openapi.TYPE_STRING, |
||||
|
required=True |
||||
|
) |
||||
|
], |
||||
|
operation_description="Retrieve a list of attachments for a given course by its slug." |
||||
|
) |
||||
|
def get(self, request, *args, **kwargs): |
||||
|
return super().get(request, *args, **kwargs) |
||||
|
|
||||
|
def get_queryset(self): |
||||
|
course_slug = self.kwargs.get('slug') |
||||
|
try: |
||||
|
course = Course.objects.get(slug=course_slug) |
||||
|
except Course.DoesNotExist: |
||||
|
raise NotFound("Course not found") |
||||
|
return Attachment.objects.filter(course=course) |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class GlossaryListAPIView(ListAPIView): |
||||
|
serializer_class = GlossarySerializer |
||||
|
|
||||
|
def get_queryset(self): |
||||
|
course_slug = self.kwargs.get('slug') |
||||
|
try: |
||||
|
course = Course.objects.get(slug=course_slug) |
||||
|
except Course.DoesNotExist: |
||||
|
raise NotFound("Course not found") |
||||
|
|
||||
|
return Glossary.objects.filter(course=course) |
||||
@ -0,0 +1,3 @@ |
|||||
|
from django.contrib import admin |
||||
|
|
||||
|
# Register your models here. |
||||
@ -0,0 +1,6 @@ |
|||||
|
from django.apps import AppConfig |
||||
|
|
||||
|
|
||||
|
class QuizConfig(AppConfig): |
||||
|
default_auto_field = 'django.db.models.BigAutoField' |
||||
|
name = 'quiz' |
||||
@ -0,0 +1,68 @@ |
|||||
|
from django.db import models |
||||
|
|
||||
|
from apps.account.models import User |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class Participant(models.Model): |
||||
|
quiz = models.ForeignKey('quiz.Quiz', on_delete=models.CASCADE, related_name='participants') |
||||
|
user = models.ForeignKey('account.User', on_delete=models.CASCADE, verbose_name='user', related_name='uquizzes') |
||||
|
started_at = models.DateTimeField(verbose_name='started at') |
||||
|
ended_at = models.DateTimeField(verbose_name='ended at') |
||||
|
total_timing = models.PositiveIntegerField(help_text='Seconds take to finish the quiz') |
||||
|
|
||||
|
question_score = models.PositiveIntegerField() |
||||
|
timing_score = models.PositiveIntegerField() |
||||
|
total_score = models.PositiveIntegerField() |
||||
|
|
||||
|
class Meta: |
||||
|
verbose_name = "Participant" |
||||
|
verbose_name_plural = "Participants" |
||||
|
ordering = ("-id",) |
||||
|
|
||||
|
def __str__(self): |
||||
|
return f"Participant: {self.id}, ParticipantName: {self.user}, Quiz: {self.quiz.id}" |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return f"Participant(id={self.id})" |
||||
|
|
||||
|
|
||||
|
@staticmethod |
||||
|
def get_user_ranks(quiz_id): |
||||
|
return Participant.objects.filter(quiz_id=quiz_id).annotate( |
||||
|
rank=Window( |
||||
|
expression=Rank(), |
||||
|
order_by=F('total_score').desc() |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
|
||||
|
|
||||
|
class ParticipantAnswer(models.Model): |
||||
|
CHOICES = [ |
||||
|
(1, 'Option 1'), |
||||
|
(2, 'Option 2'), |
||||
|
(3, 'Option 3'), |
||||
|
(4, 'Option 4'), |
||||
|
] |
||||
|
|
||||
|
participant = models.ForeignKey(Participant, on_delete=models.CASCADE, related_name='answers') |
||||
|
question = models.ForeignKey("quiz.Question", on_delete=models.CASCADE) |
||||
|
option_num = models.PositiveSmallIntegerField(choices=CHOICES, verbose_name='selected option') |
||||
|
at_time = models.DateTimeField() |
||||
|
answer_timing = models.PositiveSmallIntegerField(default=0, verbose_name='seconds take to answer') |
||||
|
|
||||
|
|
||||
|
class Meta: |
||||
|
verbose_name = "User Quiz Answer" |
||||
|
verbose_name_plural = "User Quiz Answers" |
||||
|
ordering = ("-id",) |
||||
|
|
||||
|
def __str__(self): |
||||
|
return f"Participant Answer: {self.id}" |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return f"ParticipantAnswer(id={self.id})" |
||||
|
|
||||
|
|
||||
@ -0,0 +1,53 @@ |
|||||
|
from django.db import models |
||||
|
|
||||
|
|
||||
|
|
||||
|
class Quiz(models.Model): |
||||
|
course = models.ForeignKey("course.Course", verbose_name='course', related_name='quizzes', on_delete=models.CASCADE) |
||||
|
each_question_timing = models.PositiveIntegerField() |
||||
|
status = models.BooleanField(default=True) |
||||
|
|
||||
|
|
||||
|
class Meta: |
||||
|
verbose_name = "Quiz" |
||||
|
verbose_name_plural = "Quizzes" |
||||
|
ordering = ("-id",) |
||||
|
|
||||
|
def __str__(self): |
||||
|
return f"Quiz: {self.id}" |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return f"Quiz(id={self.id})" |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class Question(models.Model): |
||||
|
CHOICES = [ |
||||
|
(1, 'Option 1'), |
||||
|
(2, 'Option 2'), |
||||
|
(3, 'Option 3'), |
||||
|
(4, 'Option 4'), |
||||
|
] |
||||
|
|
||||
|
quiz = models.ForeignKey(Quiz, verbose_name='quiz', on_delete=models.CASCADE, related_name='questions') |
||||
|
question = models.CharField(max_length=255) |
||||
|
option1 = models.CharField(max_length=255, verbose_name='option 1') |
||||
|
option2 = models.CharField(max_length=255, verbose_name='option 2') |
||||
|
option3 = models.CharField(max_length=255, verbose_name='option 3') |
||||
|
option4 = models.CharField(max_length=255, verbose_name='option 4') |
||||
|
correct_answer = models.PositiveSmallIntegerField(choices=CHOICES) |
||||
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name='created at') |
||||
|
priority = models.IntegerField(null=True, blank=True) |
||||
|
|
||||
|
|
||||
|
class Meta: |
||||
|
verbose_name = "Question" |
||||
|
verbose_name_plural = "Questions" |
||||
|
ordering = ("-priority", "-id",) |
||||
|
|
||||
|
def __str__(self): |
||||
|
return self.question |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return f"Question(id={self.id})" |
||||
@ -0,0 +1,3 @@ |
|||||
|
from django.test import TestCase |
||||
|
|
||||
|
# Create your tests here. |
||||
@ -0,0 +1,3 @@ |
|||||
|
from django.shortcuts import render |
||||
|
|
||||
|
# Create your views here. |
||||
@ -0,0 +1,8 @@ |
|||||
|
# __init__.py |
||||
|
from __future__ import absolute_import, unicode_literals |
||||
|
|
||||
|
# This will make sure the app is always imported when |
||||
|
# Django starts so that shared_task will use this app. |
||||
|
from .celery import app as celery_app |
||||
|
|
||||
|
__all__ = ('celery_app',) |
||||
@ -0,0 +1,16 @@ |
|||||
|
""" |
||||
|
ASGI config for backend project. |
||||
|
|
||||
|
It exposes the ASGI callable as a module-level variable named ``application``. |
||||
|
|
||||
|
For more information on this file, see |
||||
|
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ |
||||
|
""" |
||||
|
|
||||
|
import os |
||||
|
|
||||
|
from django.core.asgi import get_asgi_application |
||||
|
|
||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') |
||||
|
|
||||
|
application = get_asgi_application() |
||||
@ -0,0 +1,22 @@ |
|||||
|
import os |
||||
|
|
||||
|
import environ |
||||
|
|
||||
|
from celery import Celery |
||||
|
|
||||
|
env = environ.Env() |
||||
|
environ.Env.read_env(os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')) |
||||
|
|
||||
|
# Set the default Django settings module for the 'celery' program. |
||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.production') |
||||
|
|
||||
|
app = Celery('config') |
||||
|
# 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 apps. |
||||
|
app.autodiscover_tasks() |
||||
|
|
||||
@ -0,0 +1,19 @@ |
|||||
|
from django.http import HttpResponse |
||||
|
from apps.account.models import User |
||||
|
|
||||
|
ALLOWED_URLS = [ |
||||
|
"/login", "/admin", "telegram-sentry", 'bot-runner', "auth/google/", "/elalhabib/submit/", '/pay', 'paypal', |
||||
|
'robots.txt', "/.well-known/", "about", "/download", 'dont-kill/' |
||||
|
] |
||||
|
|
||||
|
|
||||
|
|
||||
|
def language_middleware(get_response): |
||||
|
def middleware(request): |
||||
|
request.LANGUAGE_CODE = request.GET.get('language_code') or request.LANGUAGE_CODE |
||||
|
|
||||
|
response = get_response(request) |
||||
|
|
||||
|
return response |
||||
|
|
||||
|
return middleware |
||||
@ -0,0 +1,15 @@ |
|||||
|
from redis import Redis, ConnectionPool |
||||
|
|
||||
|
from config.settings import base as settings |
||||
|
|
||||
|
|
||||
|
|
||||
|
pool = ConnectionPool.from_url(url= settings.REDIS_URL,max_connections=100) |
||||
|
|
||||
|
|
||||
|
class RedisConfig: |
||||
|
|
||||
|
def __init__(self): |
||||
|
self.redis = Redis(connection_pool=pool, decode_responses=True) |
||||
|
|
||||
|
|
||||
@ -0,0 +1,308 @@ |
|||||
|
""" |
||||
|
Django settings for backend project. |
||||
|
|
||||
|
Generated by 'django-admin startproject' using Django 5.0.4. |
||||
|
|
||||
|
For more information on this file, see |
||||
|
https://docs.djangoproject.com/en/5.0/topics/settings/ |
||||
|
|
||||
|
For the full list of settings and their values, see |
||||
|
https://docs.djangoproject.com/en/5.0/ref/settings/ |
||||
|
""" |
||||
|
import os |
||||
|
from pathlib import Path |
||||
|
|
||||
|
import environ |
||||
|
from django.utils.translation import gettext_lazy as _ |
||||
|
|
||||
|
|
||||
|
env = environ.Env( |
||||
|
# set casting, default value |
||||
|
# DEBUG=(bool, False) |
||||
|
) |
||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'. |
||||
|
BASE_DIR = Path(__file__).resolve().parent.parent.parent |
||||
|
|
||||
|
environ.Env.read_env(os.path.join(BASE_DIR, '.env')) |
||||
|
|
||||
|
ALLOWED_HOSTS = env('DJANGO_ALLOWED_HOSTS').split(',') |
||||
|
|
||||
|
|
||||
|
# Quick-start development settings - unsuitable for production |
||||
|
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ |
||||
|
|
||||
|
# SECURITY WARNING: keep the secret key used in production secret! |
||||
|
SECRET_KEY = 'django-insecure-7=3it+m^28^+0c1*9-*c*6g3ej63sz(97rq1^mp=!6e(mhmysh' |
||||
|
|
||||
|
# SECURITY WARNING: don't run with debug turned on in production! |
||||
|
DEBUG = True |
||||
|
|
||||
|
X_FRAME_OPTIONS = 'SAMEORIGIN' |
||||
|
|
||||
|
LOCAL_APPS = [ |
||||
|
'apps.account.apps.AccountConfig', |
||||
|
'apps.api.apps.ApiConfig', |
||||
|
'apps.course.apps.CourseConfig', |
||||
|
] |
||||
|
|
||||
|
THIRD_PARTY_APPS = [ |
||||
|
'rest_framework', |
||||
|
'rest_framework.authtoken', |
||||
|
'drf_yasg', |
||||
|
'easy_thumbnails', |
||||
|
'phonenumber_field', |
||||
|
'dj_language', |
||||
|
'dj_filer', |
||||
|
'ajaxdatatable', |
||||
|
'corsheaders', |
||||
|
'django_filters', |
||||
|
|
||||
|
] |
||||
|
INSTALLED_APPS = [ |
||||
|
'limitless_dashboard.apps.DashboardConfig', |
||||
|
# 'django.contrib.admin', |
||||
|
'django.contrib.auth', |
||||
|
'django.contrib.contenttypes', |
||||
|
'django.contrib.sessions', |
||||
|
'django.contrib.messages', |
||||
|
'django.contrib.staticfiles', |
||||
|
*THIRD_PARTY_APPS, |
||||
|
*LOCAL_APPS, |
||||
|
|
||||
|
] |
||||
|
AUTHENTICATION_BACKENDS = [ |
||||
|
'django.contrib.auth.backends.ModelBackend', # این خط را نگه دارید تا احراز هویت پیشفرض کار کند |
||||
|
'apps.account.custom_user_login.CustomLoginBackend', # مسیر به کلاس سفارشی خود |
||||
|
] |
||||
|
|
||||
|
REDIS_URL = env('REDIS_URL') |
||||
|
|
||||
|
|
||||
|
|
||||
|
OTP_SERIVCE_KEY = "33213d78f1234e99b81f94eefda77e45" |
||||
|
|
||||
|
|
||||
|
PHONENUMBER_DEFAULT_REGION = "IR" |
||||
|
PHONENUMBER_DB_FORMAT = 'INTERNATIONAL' |
||||
|
PHONENUMBER_DEFAULT_FORMAT = 'INTERNATIONAL' |
||||
|
|
||||
|
AUTH_USER_MODEL = "account.User" |
||||
|
|
||||
|
MIDDLEWARE = [ |
||||
|
'django.middleware.security.SecurityMiddleware', |
||||
|
'django.contrib.sessions.middleware.SessionMiddleware', |
||||
|
'corsheaders.middleware.CorsMiddleware', |
||||
|
'django.middleware.locale.LocaleMiddleware', |
||||
|
'django.middleware.common.CommonMiddleware', |
||||
|
'django.middleware.csrf.CsrfViewMiddleware', |
||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware', |
||||
|
'django.contrib.messages.middleware.MessageMiddleware', |
||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware', |
||||
|
'config.language_code_middleware.language_middleware', |
||||
|
'config.test_auth_middleware.test_auth_middleware', |
||||
|
] |
||||
|
|
||||
|
ROOT_URLCONF = 'config.urls' |
||||
|
|
||||
|
TEMPLATES = [ |
||||
|
{ |
||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates', |
||||
|
'DIRS': [ |
||||
|
BASE_DIR / 'templates', |
||||
|
], |
||||
|
'APP_DIRS': True, |
||||
|
'OPTIONS': { |
||||
|
'context_processors': [ |
||||
|
'django.template.context_processors.debug', |
||||
|
'django.template.context_processors.request', |
||||
|
'django.contrib.auth.context_processors.auth', |
||||
|
'django.contrib.messages.context_processors.messages', |
||||
|
'django.template.context_processors.i18n', |
||||
|
|
||||
|
], |
||||
|
}, |
||||
|
}, |
||||
|
] |
||||
|
|
||||
|
WSGI_APPLICATION = 'config.wsgi.application' |
||||
|
|
||||
|
|
||||
|
# django google recaptcha default keys |
||||
|
RECAPTCHA_PUBLIC_KEY = env('captcha_public_key') |
||||
|
RECAPTCHA_PRIVATE_KEY = env('captcha_private_key') |
||||
|
|
||||
|
# custom settings |
||||
|
APPS_REORDER = { |
||||
|
'auth': { |
||||
|
'icon': 'icon-shield-check', |
||||
|
'name': 'Authentication' |
||||
|
}, |
||||
|
'account': { |
||||
|
# 'icon': 'icon-', |
||||
|
'name': 'account' |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
# Database |
||||
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases |
||||
|
|
||||
|
DATABASES = { |
||||
|
'default': { |
||||
|
'ENGINE': 'django.db.backends.postgresql', |
||||
|
'NAME': env('POSTGRES_DB'), |
||||
|
'USER': env('POSTGRES_USER'), |
||||
|
'PASSWORD': env('POSTGRES_PASSWORD'), |
||||
|
'HOST': env('POSTGRES_HOST'), |
||||
|
'PORT': env('POSTGRES_PORT'), |
||||
|
'ATOMIC_REQUESTS': True, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
|
||||
|
CORS_ALLOW_ALL_ORIGINS = True |
||||
|
|
||||
|
THUMBNAIL_ALIASES = { |
||||
|
'': { |
||||
|
'icon': {'size': (50, 50), 'crop': True}, |
||||
|
'large': {'size': (1200, 620), 'crop': False}, |
||||
|
'medium': {'size': (545, 545), 'crop': False}, |
||||
|
'small': {'size': (150, 150), 'crop': False}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
LANGUAGES_MAP = { |
||||
|
'az': ['az', 'tr', 'fa', 'ar'], |
||||
|
'tr': ['tr', 'az', 'fa', 'ar'], |
||||
|
'ru': ['ru', 'az', 'tr', 'fa', 'ar'], |
||||
|
'ar': ['ar', 'fa'], |
||||
|
'ur': ['ur', 'en', 'fa', 'ar'], |
||||
|
'en': ['en', 'ur', 'fa', 'ar'], |
||||
|
'de': ['de', 'en', 'fr', 'es', 'ar'], |
||||
|
'fa': ['fa', 'az', 'ar', 'en', 'ur'], |
||||
|
|
||||
|
'fr': ['fr', 'en', 'ar', 'fa'], |
||||
|
'es': ['es', 'en', 'ar', 'fa'], |
||||
|
'id': ['id', 'en', 'ar', 'fa'], |
||||
|
'sw': ['sw', 'en', 'ar', 'fa'], |
||||
|
} |
||||
|
|
||||
|
|
||||
|
LANGUAGES = [ |
||||
|
('ar', _('Arabic')), |
||||
|
('az', _('Azerbaijani')), |
||||
|
('fr', _('French')), |
||||
|
('in', _('Indonesia')), |
||||
|
('fa', _('Persian')), |
||||
|
('ru', _('Russia')), |
||||
|
('es', _('Spanish')), |
||||
|
('sw', _('Swahili')), |
||||
|
('tr', _('Turkish')), |
||||
|
('de', _('German')), |
||||
|
('en', _('English')), |
||||
|
('fa', _('Persian')), |
||||
|
('ur', _('Urdu')), |
||||
|
('zh', _('Mandarin')), |
||||
|
('zh', _('Chinese')), |
||||
|
('he', _('Hebrew')), |
||||
|
('he', _('Hebrew')), |
||||
|
('bn', _('Bengali')), |
||||
|
] |
||||
|
|
||||
|
CELERY_BROKER_URL = env("REDIS_URL") |
||||
|
CELERY_RESULT_BACKEND = env("REDIS_URL") |
||||
|
CELERY_ACCEPT_CONTENT = ['application/json'] |
||||
|
CELERY_TIMEZONE = 'Asia/Tehran' |
||||
|
CELERY_BROKER_TRANSPORT = 'redis' |
||||
|
CELERY_ACCEPT_CONTENT = ['json'] |
||||
|
CELERY_TASK_SERIALIZER = 'json' |
||||
|
CELERY_RESULT_SERIALIZER = 'json' |
||||
|
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True |
||||
|
|
||||
|
# Password validation |
||||
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators |
||||
|
|
||||
|
|
||||
|
AUTH_PASSWORD_VALIDATORS = [ |
||||
|
{ |
||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', |
||||
|
'OPTIONS': { |
||||
|
'min_length': 6, |
||||
|
} |
||||
|
}, |
||||
|
] |
||||
|
|
||||
|
|
||||
|
REST_FRAMEWORK = { |
||||
|
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', |
||||
|
'PAGE_SIZE': 16, |
||||
|
# 'DEFAULT_AUTHENTICATION_CLASSES': [ |
||||
|
# 'apps.account.auth_back.TokenAuthentication2', |
||||
|
# ], |
||||
|
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], |
||||
|
'DEFAULT_AUTHENTICATION_CLASSES': [ |
||||
|
'rest_framework.authentication.TokenAuthentication', |
||||
|
# 'rest_framework.authentication.SessionAuthentication', |
||||
|
], |
||||
|
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema' # or OpenAPISchema if using drf_yasg |
||||
|
|
||||
|
|
||||
|
} |
||||
|
# Internationalization |
||||
|
# https://docs.djangoproject.com/en/5.0/topics/i18n/ |
||||
|
|
||||
|
LANGUAGE_CODE = 'en' |
||||
|
|
||||
|
TIME_ZONE = 'Asia/Tehran' |
||||
|
|
||||
|
USE_I18N = True |
||||
|
|
||||
|
USE_L10N = True |
||||
|
|
||||
|
USE_TZ = False |
||||
|
|
||||
|
STATIC_URL = '/static/' |
||||
|
MEDIA_URL = '/media/' |
||||
|
|
||||
|
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] |
||||
|
STATIC_ROOT = os.path.join(BASE_DIR, 'static', 'static') |
||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'static', 'media') |
||||
|
|
||||
|
FILER_ADMIN_ICON_SIZES = ('32', '48') |
||||
|
|
||||
|
FILER_ENABLE_LOGGING = True |
||||
|
FILER_DEBUG = True |
||||
|
ADMIN_TITLE = 'Aquilah App' |
||||
|
ADMIN_INDEX_TITLE = 'Aquilah Administration' |
||||
|
|
||||
|
|
||||
|
# Dictionary with phone number ranges and corresponding countries |
||||
|
# If a country is in this dictionary, it indicates that the project's OTP service supports that country |
||||
|
SERVICE_OTP_COUNTRU_API_KEY = { |
||||
|
"Iran": "https://console.melipayamak.com/api/send/simple/33213d78f1234e99b81f94eefda77e45" |
||||
|
} |
||||
|
SERVICE_OTP_COUNTRY_PHONE_RANGE = { |
||||
|
"98": "Iran", |
||||
|
"+98": "Iran" |
||||
|
} |
||||
|
|
||||
|
# Static files (CSS, JavaScript, Images) |
||||
|
# https://docs.djangoproject.com/en/5.0/howto/static-files/ |
||||
|
|
||||
|
|
||||
|
# Default primary key field type |
||||
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field |
||||
|
|
||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' |
||||
|
DEFAULT_SHOW_CITY_GUIDE_CITY = 'mashhad' |
||||
|
FILE_UPLOAD_HANDLERS = [ |
||||
|
'django.core.files.uploadhandler.TemporaryFileUploadHandler', |
||||
|
] |
||||
|
|
||||
|
|
||||
|
|
||||
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' |
||||
|
EMAIL_HOST = 'smtp.gmail.com' |
||||
|
EMAIL_PORT = 587 |
||||
|
EMAIL_USE_TLS = True |
||||
|
EMAIL_HOST_USER = 'aliabdolahi.171@gmail.com' |
||||
|
EMAIL_HOST_PASSWORD = 'rkxb nnhx iave fxxt' |
||||
@ -0,0 +1,17 @@ |
|||||
|
from .base import * |
||||
|
|
||||
|
# DJANGO_REDIS_IGNORE_EXCEPTIONS = True |
||||
|
DEBUG = True |
||||
|
|
||||
|
CORS_ALLOW_ALL_ORIGINS = True |
||||
|
|
||||
|
# CACHES = { |
||||
|
# 'default': { |
||||
|
# "BACKEND": "django.core.cache.backends.dummy.DummyCache", |
||||
|
# }, |
||||
|
# 'memory': { |
||||
|
# 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', |
||||
|
# 'LOCATION': 'unique-snowflake', |
||||
|
# 'TIMEOUT': 5000, |
||||
|
# }, |
||||
|
# } |
||||
@ -0,0 +1,120 @@ |
|||||
|
# import sentry_sdk |
||||
|
|
||||
|
from .base import * |
||||
|
from celery.schedules import crontab |
||||
|
|
||||
|
DEBUG = False |
||||
|
|
||||
|
#It is currently active |
||||
|
CORS_ALLOW_ALL_ORIGINS = True |
||||
|
|
||||
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') |
||||
|
|
||||
|
|
||||
|
|
||||
|
CELERY_BROKER_URL = env("REDIS_URL") |
||||
|
CELERY_RESULT_BACKEND = env("REDIS_URL") |
||||
|
CELERY_ACCEPT_CONTENT = ['application/json'] |
||||
|
CELERY_TASK_SERIALIZER = 'json' |
||||
|
CELERY_RESULT_SERIALIZER = 'json' |
||||
|
CELERY_TIMEZONE = 'Asia/Tehran' |
||||
|
CELERY_BROKER_TRANSPORT = 'redis' |
||||
|
|
||||
|
# زمانبندی Celery Beat |
||||
|
|
||||
|
CELERY_BEAT_SCHEDULE = { |
||||
|
'crawler_website_bonbast_rate_usd_every_half_hour': { |
||||
|
'task': 'apps.tasrif.tasks.crawler_website_bonbast_rate_usd', |
||||
|
'schedule': crontab(minute=0, hour='*/1'), # اجرای هر ساعت یکبار |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
# CORS_ALLOWED_ORIGINS = [ |
||||
|
# 'https://aqila.nwhco.ir', |
||||
|
# 'http://aqila.nwhco.ir', |
||||
|
# 'https://aqila.com', |
||||
|
# 'https://pay.aqila.com', |
||||
|
# 'http://pay.aqila.com', |
||||
|
# 'https://qa.aqila.com', |
||||
|
# 'http://aqila.com', |
||||
|
# 'http://aqila.app', |
||||
|
# 'https://aqila.app', |
||||
|
# ] |
||||
|
|
||||
|
CACHES = { |
||||
|
'default': { |
||||
|
"BACKEND": "django_redis.cache.RedisCache", |
||||
|
"LOCATION": env("REDIS_URL"), |
||||
|
"OPTIONS": { |
||||
|
"CLIENT_CLASS": "django_redis.client.DefaultClient", |
||||
|
} |
||||
|
}, |
||||
|
'memory': { |
||||
|
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', |
||||
|
'LOCATION': 'unique-snowflake', |
||||
|
'TIMEOUT': 5000, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
# sentry_sdk.init( |
||||
|
# dsn="https://4d54a16a5ea997f6dd4859a4d34da230@us.sentry.io/4506682167525376", |
||||
|
# # Set traces_sample_rate to 1.0 to capture 100% |
||||
|
# # of transactions for performance monitoring. |
||||
|
# traces_sample_rate=1.0, |
||||
|
# # Set profiles_sample_rate to 1.0 to profile 100% |
||||
|
# # of sampled transactions. |
||||
|
# # We recommend adjusting this value in production. |
||||
|
# profiles_sample_rate=1.0, |
||||
|
# ) |
||||
|
|
||||
|
REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] = [ |
||||
|
'rest_framework.renderers.JSONRenderer', |
||||
|
] |
||||
|
|
||||
|
|
||||
|
LOGGING = { |
||||
|
"version": 1, |
||||
|
"disable_existing_loggers": False, |
||||
|
"filters": { |
||||
|
"require_debug_false": { |
||||
|
"()": "django.utils.log.RequireDebugFalse", |
||||
|
}, |
||||
|
"require_debug_true": { |
||||
|
"()": "django.utils.log.RequireDebugTrue", |
||||
|
}, |
||||
|
}, |
||||
|
"formatters": { |
||||
|
"django.server": { |
||||
|
"()": "django.utils.log.ServerFormatter", |
||||
|
"format": "[{server_time}] {message}", |
||||
|
"style": "{", |
||||
|
} |
||||
|
}, |
||||
|
"handlers": { |
||||
|
"console": { |
||||
|
"level": "INFO", |
||||
|
"class": "logging.StreamHandler", |
||||
|
}, |
||||
|
"django.server": { |
||||
|
"level": "INFO", |
||||
|
"class": "logging.StreamHandler", |
||||
|
"formatter": "django.server", |
||||
|
}, |
||||
|
"mail_admins": { |
||||
|
"level": "ERROR", |
||||
|
"filters": ["require_debug_false"], |
||||
|
"class": "django.utils.log.AdminEmailHandler", |
||||
|
}, |
||||
|
}, |
||||
|
"loggers": { |
||||
|
"django": { |
||||
|
"handlers": ["console", "mail_admins"], |
||||
|
"level": "INFO", |
||||
|
}, |
||||
|
"django.server": { |
||||
|
"handlers": ["django.server"], |
||||
|
"level": "INFO", |
||||
|
"propagate": False, |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
from django.core.exceptions import PermissionDenied |
||||
|
from rest_framework.authtoken.models import Token |
||||
|
from apps.account.models import User |
||||
|
|
||||
|
|
||||
|
|
||||
|
def test_auth_middleware(get_response): |
||||
|
""" |
||||
|
give access to swagger and api if admin is logged in |
||||
|
""" |
||||
|
|
||||
|
def middleware(request): |
||||
|
if "/admin/" not in request.path and request.META.get('HTTP_AUTHORIZATION') is None: |
||||
|
if request.user.is_authenticated and request.user.is_staff: |
||||
|
token, _ = Token.objects.get_or_create(user=request.user) |
||||
|
request.META['HTTP_AUTHORIZATION'] = "Token " + token.key |
||||
|
|
||||
|
|
||||
|
if "/swagger" in request.path or "/redoc" in request.path: |
||||
|
if not request.META.get('HTTP_AUTHORIZATION'): |
||||
|
user = User.objects.filter(is_staff=True, email="aqila@gmail.com").first() |
||||
|
if user: |
||||
|
t, _ = Token.objects.get_or_create(user=user) |
||||
|
request.META['HTTP_AUTHORIZATION'] = f"Token {t}" |
||||
|
|
||||
|
|
||||
|
return get_response(request) |
||||
|
|
||||
|
return middleware |
||||
@ -0,0 +1,53 @@ |
|||||
|
""" |
||||
|
URL configuration for backend project. |
||||
|
|
||||
|
The `urlpatterns` list routes URLs to views. For more information please see: |
||||
|
https://docs.djangoproject.com/en/5.0/topics/http/urls/ |
||||
|
Examples: |
||||
|
Function views |
||||
|
1. Add an import: from my_app import views |
||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home') |
||||
|
Class-based views |
||||
|
1. Add an import: from other_app.views import Home |
||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') |
||||
|
Including another URLconf |
||||
|
1. Import the include() function: from django.urls import include, path |
||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) |
||||
|
""" |
||||
|
from django.contrib import admin |
||||
|
from django.urls import path, include |
||||
|
from django.conf import settings |
||||
|
from django.conf.urls.static import static |
||||
|
from django.conf.urls.i18n import i18n_patterns |
||||
|
from utils import UploadTmpMedia |
||||
|
from django.conf.urls import url |
||||
|
from django.http import JsonResponse |
||||
|
from django.shortcuts import render |
||||
|
from django.views.decorators.csrf import csrf_exempt |
||||
|
from rest_framework.decorators import api_view |
||||
|
from rest_framework.response import Response |
||||
|
|
||||
|
from utils import absolute_url |
||||
|
|
||||
|
|
||||
|
|
||||
|
api_patterns = [ |
||||
|
path('test/', include('apps.api.urls')), |
||||
|
|
||||
|
path('account/', include('apps.account.urls')), |
||||
|
path('courses/', include('apps.course.urls')), |
||||
|
|
||||
|
] |
||||
|
|
||||
|
|
||||
|
urlpatterns = [ |
||||
|
# path('admin/', admin.site.urls), |
||||
|
path('api/', include(api_patterns)), |
||||
|
# path('test/', include('apps.api.urls')) |
||||
|
] |
||||
|
urlpatterns += i18n_patterns( |
||||
|
path('', include('limitless_dashboard.urls')), |
||||
|
|
||||
|
) |
||||
|
if settings.DEBUG: |
||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) |
||||
@ -0,0 +1,16 @@ |
|||||
|
""" |
||||
|
WSGI config for backend project. |
||||
|
|
||||
|
It exposes the WSGI callable as a module-level variable named ``application``. |
||||
|
|
||||
|
For more information on this file, see |
||||
|
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ |
||||
|
""" |
||||
|
|
||||
|
import os |
||||
|
|
||||
|
from django.core.wsgi import get_wsgi_application |
||||
|
|
||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.production') |
||||
|
|
||||
|
application = get_wsgi_application() |
||||
@ -0,0 +1,85 @@ |
|||||
|
version: '3.8' |
||||
|
|
||||
|
services: |
||||
|
web: |
||||
|
container_name: imam-javad_web |
||||
|
restart: unless-stopped |
||||
|
build: |
||||
|
context: . |
||||
|
dockerfile: Dockerfile.prod |
||||
|
command: gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers=32 --timeout 560 |
||||
|
volumes: |
||||
|
- static_volume:/usr/src/app/static |
||||
|
ports: |
||||
|
- "8019:8000" |
||||
|
env_file: |
||||
|
- .env.prod |
||||
|
depends_on: |
||||
|
- postgres |
||||
|
links: |
||||
|
- postgres |
||||
|
networks: |
||||
|
- imam-javad |
||||
|
|
||||
|
postgres: |
||||
|
container_name: imam-javad_db |
||||
|
ports: |
||||
|
- "5575:5432" |
||||
|
restart: unless-stopped |
||||
|
image: postgres:14.0 |
||||
|
volumes: |
||||
|
- postgres_data:/var/lib/postgresql/data/ |
||||
|
env_file: |
||||
|
- .env.prod |
||||
|
networks: |
||||
|
- imam-javad |
||||
|
imam-javad_redis: |
||||
|
container_name: imam-javad_redis |
||||
|
image: redis:alpine |
||||
|
env_file: .env.prod |
||||
|
volumes: |
||||
|
- redis_data:/data |
||||
|
networks: |
||||
|
- imam-javad |
||||
|
|
||||
|
imam-javad_celery: |
||||
|
container_name: imam-javad_celery |
||||
|
build: |
||||
|
context: . |
||||
|
dockerfile: Dockerfile.celery.prod |
||||
|
env_file: .env.prod |
||||
|
command: celery -A config worker -l info |
||||
|
volumes: |
||||
|
- .:/usr/src/app/ |
||||
|
- static_volume:/usr/src/app/static |
||||
|
|
||||
|
depends_on: |
||||
|
- imam-javad_redis |
||||
|
networks: |
||||
|
- imam-javad |
||||
|
|
||||
|
|
||||
|
imam-javad_celery-beat: |
||||
|
container_name: imam-javad_celery_beat |
||||
|
build: |
||||
|
context: . |
||||
|
dockerfile: Dockerfile.prod |
||||
|
env_file: .env.prod |
||||
|
command: celery -A config beat -l info |
||||
|
volumes: |
||||
|
- .:/usr/src/app/ |
||||
|
depends_on: |
||||
|
- imam-javad_redis |
||||
|
networks: |
||||
|
- imam-javad |
||||
|
|
||||
|
|
||||
|
|
||||
|
volumes: |
||||
|
postgres_data: |
||||
|
static_volume: |
||||
|
redis_data: |
||||
|
|
||||
|
networks: |
||||
|
imam-javad: |
||||
|
driver: bridge |
||||
@ -0,0 +1,37 @@ |
|||||
|
version: '3.8' |
||||
|
|
||||
|
|
||||
|
services: |
||||
|
web: |
||||
|
build: . |
||||
|
command: python manage.py runserver 0.0.0.0:8000 |
||||
|
volumes: |
||||
|
- .:/usr/src/app |
||||
|
- ./volumes/static_data:/usr/src/app/static/ |
||||
|
ports: |
||||
|
- "9000:8000" |
||||
|
env_file: |
||||
|
- .env.dev |
||||
|
depends_on: |
||||
|
- postgres |
||||
|
networks: |
||||
|
- aquilah |
||||
|
|
||||
|
postgres: |
||||
|
ports: |
||||
|
- "5444:5432" |
||||
|
image: postgres:13.7 |
||||
|
|
||||
|
volumes: |
||||
|
- ./volumes/postgres_data:/var/lib/postgresql/data |
||||
|
env_file: |
||||
|
- .env.dev |
||||
|
networks: |
||||
|
- aquilah |
||||
|
|
||||
|
|
||||
|
volumes: |
||||
|
postgres_data: |
||||
|
staticfiles: |
||||
|
networks: |
||||
|
imam-javad: |
||||
@ -0,0 +1,2 @@ |
|||||
|
__version__ = "1.14.0" |
||||
|
default_app_config = "dynamic_preferences.apps.DynamicPreferencesConfig" |
||||
@ -0,0 +1,114 @@ |
|||||
|
from ajaxdatatable.admin import AjaxDatatable |
||||
|
from django.contrib import admin |
||||
|
from django import forms |
||||
|
|
||||
|
from .settings import preferences_settings |
||||
|
from .registries import global_preferences_registry |
||||
|
from .models import GlobalPreferenceModel |
||||
|
from .forms import GlobalSinglePreferenceForm, SinglePerInstancePreferenceForm |
||||
|
from django.utils.translation import gettext_lazy as _ |
||||
|
|
||||
|
|
||||
|
class SectionFilter(admin.AllValuesFieldListFilter): |
||||
|
def __init__(self, field, request, params, model, model_admin, field_path): |
||||
|
super(SectionFilter, self).__init__( |
||||
|
field, request, params, model, model_admin, field_path |
||||
|
) |
||||
|
parent_model, reverse_path = admin.utils.reverse_field_path(model, field_path) |
||||
|
if model == parent_model: |
||||
|
queryset = model_admin.get_queryset |
||||
|
else: |
||||
|
queryset = parent_model._default_manager.all() |
||||
|
self.registries = [] |
||||
|
registry_name_set = set() |
||||
|
for preferenceModel in queryset.distinct(): |
||||
|
l = len(registry_name_set) |
||||
|
registry_name_set.add(preferenceModel.registry.__class__.__name__) |
||||
|
if len(registry_name_set) != l: |
||||
|
self.registries.append(preferenceModel.registry) |
||||
|
|
||||
|
def choices(self, changelist): |
||||
|
choices = super(SectionFilter, self).choices(changelist) |
||||
|
for choice in choices: |
||||
|
display = choice["display"] |
||||
|
try: |
||||
|
for registry in self.registries: |
||||
|
display = registry.section_objects[display].verbose_name |
||||
|
choice["display"] = display |
||||
|
except (KeyError): |
||||
|
pass |
||||
|
yield choice |
||||
|
|
||||
|
|
||||
|
class DynamicPreferenceAdmin(AjaxDatatable): |
||||
|
list_display = ( |
||||
|
"verbose_name", |
||||
|
"help_text", |
||||
|
) |
||||
|
fields = ("raw_value", "default_value",) |
||||
|
readonly_fields = ("default_value",) |
||||
|
change_form_template = "dynamic_preferences/dyna_change_form.html" |
||||
|
|
||||
|
@admin.display(description=_('Verbose name')) |
||||
|
def verbose_name(self, obj): |
||||
|
return obj.verbose_name |
||||
|
|
||||
|
@admin.display(description=_('Help text')) |
||||
|
def help_text(self, obj): |
||||
|
return obj.help_text |
||||
|
|
||||
|
def has_add_permission(self, request): |
||||
|
# if "root@admin" in request.user.username: |
||||
|
# return True |
||||
|
return False |
||||
|
|
||||
|
def has_delete_permission(self, request, obj=None): |
||||
|
if "root@admin" in request.user.email: |
||||
|
return True |
||||
|
return False |
||||
|
|
||||
|
if preferences_settings.ADMIN_ENABLE_CHANGELIST_FORM: |
||||
|
def get_changelist_form(self, request, **kwargs): |
||||
|
return self.changelist_form |
||||
|
|
||||
|
def default_value(self, obj): |
||||
|
return obj.preference.default |
||||
|
|
||||
|
default_value.short_description = _("Default Value") |
||||
|
|
||||
|
def section_name(self, obj): |
||||
|
try: |
||||
|
return obj.registry.section_objects[obj.section].verbose_name |
||||
|
except KeyError: |
||||
|
pass |
||||
|
return obj.section |
||||
|
|
||||
|
section_name.short_description = _("Section Name") |
||||
|
|
||||
|
def save_model(self, request, obj, form, change): |
||||
|
pref = form.instance |
||||
|
manager = pref.registry.manager() |
||||
|
manager.update_db_pref(pref.section, pref.name, form.cleaned_data["raw_value"]) |
||||
|
|
||||
|
|
||||
|
class GlobalPreferenceAdmin(DynamicPreferenceAdmin): |
||||
|
form = GlobalSinglePreferenceForm |
||||
|
changelist_form = GlobalSinglePreferenceForm |
||||
|
|
||||
|
def get_queryset(self, *args, **kwargs): |
||||
|
# Instanciate default prefs |
||||
|
manager = global_preferences_registry.manager() |
||||
|
manager.all() |
||||
|
return super(GlobalPreferenceAdmin, self).get_queryset(*args, **kwargs) |
||||
|
|
||||
|
|
||||
|
admin.site.register(GlobalPreferenceModel, GlobalPreferenceAdmin) |
||||
|
|
||||
|
|
||||
|
class PerInstancePreferenceAdmin(DynamicPreferenceAdmin): |
||||
|
list_display = ("instance",) + DynamicPreferenceAdmin.list_display |
||||
|
fields = ("instance",) + DynamicPreferenceAdmin.fields |
||||
|
raw_id_fields = ("instance",) |
||||
|
form = SinglePerInstancePreferenceForm |
||||
|
changelist_form = SinglePerInstancePreferenceForm |
||||
|
list_select_related = True |
||||
@ -0,0 +1,71 @@ |
|||||
|
from rest_framework import serializers |
||||
|
from dynamic_preferences.models import GlobalPreferenceModel |
||||
|
|
||||
|
|
||||
|
class PreferenceValueField(serializers.Field): |
||||
|
def get_attribute(self, o): |
||||
|
return o |
||||
|
|
||||
|
def to_representation(self, o): |
||||
|
return o.preference.api_repr(o.value) |
||||
|
|
||||
|
def to_internal_value(self, data): |
||||
|
return data |
||||
|
|
||||
|
|
||||
|
class PreferenceSerializer(serializers.Serializer): |
||||
|
|
||||
|
section = serializers.CharField(read_only=True) |
||||
|
name = serializers.CharField(read_only=True) |
||||
|
identifier = serializers.SerializerMethodField() |
||||
|
default = serializers.SerializerMethodField() |
||||
|
value = PreferenceValueField() |
||||
|
verbose_name = serializers.SerializerMethodField() |
||||
|
help_text = serializers.SerializerMethodField() |
||||
|
additional_data = serializers.SerializerMethodField() |
||||
|
field = serializers.SerializerMethodField() |
||||
|
|
||||
|
class Meta: |
||||
|
fields = [ |
||||
|
"default", |
||||
|
"value", |
||||
|
"verbose_name", |
||||
|
"help_text", |
||||
|
] |
||||
|
|
||||
|
def get_default(self, o): |
||||
|
return o.preference.api_repr(o.preference.get("default")) |
||||
|
|
||||
|
def get_verbose_name(self, o): |
||||
|
return o.preference.get("verbose_name") |
||||
|
|
||||
|
def get_identifier(self, o): |
||||
|
return o.preference.identifier() |
||||
|
|
||||
|
def get_help_text(self, o): |
||||
|
return o.preference.get("help_text") |
||||
|
|
||||
|
def get_additional_data(self, o): |
||||
|
return o.preference.get_api_additional_data() |
||||
|
|
||||
|
def get_field(self, o): |
||||
|
return o.preference.get_api_field_data() |
||||
|
|
||||
|
def validate_value(self, value): |
||||
|
""" |
||||
|
We call validation from the underlying form field |
||||
|
""" |
||||
|
field = self.instance.preference.setup_field() |
||||
|
value = field.to_python(value) |
||||
|
field.validate(value) |
||||
|
field.run_validators(value) |
||||
|
return value |
||||
|
|
||||
|
def update(self, instance, validated_data): |
||||
|
instance.value = validated_data["value"] |
||||
|
instance.save() |
||||
|
return instance |
||||
|
|
||||
|
|
||||
|
class GlobalPreferenceSerializer(PreferenceSerializer): |
||||
|
pass |
||||
@ -0,0 +1,179 @@ |
|||||
|
from django.db import transaction |
||||
|
from django.db.models import Q |
||||
|
|
||||
|
from rest_framework import mixins |
||||
|
from rest_framework import viewsets |
||||
|
from rest_framework import permissions |
||||
|
from rest_framework.response import Response |
||||
|
from rest_framework.decorators import action |
||||
|
from rest_framework.generics import get_object_or_404 |
||||
|
|
||||
|
from dynamic_preferences import models |
||||
|
from dynamic_preferences import exceptions |
||||
|
from dynamic_preferences.settings import preferences_settings |
||||
|
|
||||
|
from . import serializers |
||||
|
|
||||
|
|
||||
|
class PreferenceViewSet( |
||||
|
mixins.UpdateModelMixin, |
||||
|
mixins.ListModelMixin, |
||||
|
mixins.RetrieveModelMixin, |
||||
|
viewsets.GenericViewSet, |
||||
|
): |
||||
|
""" |
||||
|
- list preferences |
||||
|
- detail given preference |
||||
|
- batch update preferences |
||||
|
- update a single preference |
||||
|
""" |
||||
|
|
||||
|
def get_queryset(self): |
||||
|
""" |
||||
|
We just ensure preferences are actually populated before fetching |
||||
|
from db |
||||
|
""" |
||||
|
self.init_preferences() |
||||
|
queryset = super(PreferenceViewSet, self).get_queryset() |
||||
|
|
||||
|
section = self.request.query_params.get("section") |
||||
|
if section: |
||||
|
queryset = queryset.filter(section=section) |
||||
|
|
||||
|
return queryset |
||||
|
|
||||
|
def get_manager(self): |
||||
|
return self.queryset.model.registry.manager() |
||||
|
|
||||
|
def init_preferences(self): |
||||
|
manager = self.get_manager() |
||||
|
manager.all() |
||||
|
|
||||
|
def get_object(self): |
||||
|
""" |
||||
|
Returns the object the view is displaying. |
||||
|
You may want to override this if you need to provide non-standard |
||||
|
queryset lookups. Eg if objects are referenced using multiple |
||||
|
keyword arguments in the url conf. |
||||
|
""" |
||||
|
queryset = self.filter_queryset(self.get_queryset()) |
||||
|
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field |
||||
|
identifier = self.kwargs[lookup_url_kwarg] |
||||
|
section, name = self.get_section_and_name(identifier) |
||||
|
filter_kwargs = {"section": section, "name": name} |
||||
|
obj = get_object_or_404(queryset, **filter_kwargs) |
||||
|
|
||||
|
# May raise a permission denied |
||||
|
self.check_object_permissions(self.request, obj) |
||||
|
|
||||
|
return obj |
||||
|
|
||||
|
def get_section_and_name(self, identifier): |
||||
|
try: |
||||
|
section, name = identifier.split(preferences_settings.SECTION_KEY_SEPARATOR) |
||||
|
except ValueError: |
||||
|
# no section given |
||||
|
section, name = None, identifier |
||||
|
|
||||
|
return section, name |
||||
|
|
||||
|
@action(detail=False, methods=["post"]) |
||||
|
@transaction.atomic |
||||
|
def bulk(self, request, *args, **kwargs): |
||||
|
""" |
||||
|
Update multiple preferences at once |
||||
|
|
||||
|
this is a long method because we ensure everything is valid |
||||
|
before actually persisting the changes |
||||
|
""" |
||||
|
manager = self.get_manager() |
||||
|
errors = {} |
||||
|
preferences = [] |
||||
|
payload = request.data |
||||
|
|
||||
|
# first, we check updated preferences actually exists in the registry |
||||
|
try: |
||||
|
for identifier, value in payload.items(): |
||||
|
try: |
||||
|
preferences.append(self.queryset.model.registry.get(identifier)) |
||||
|
except exceptions.NotFoundInRegistry: |
||||
|
errors[identifier] = "invalid preference" |
||||
|
except (TypeError, AttributeError): |
||||
|
return Response("invalid payload", status=400) |
||||
|
|
||||
|
if errors: |
||||
|
return Response(errors, status=400) |
||||
|
|
||||
|
# now, we generate an optimized Q objects to retrieve all matching |
||||
|
# preferences at once from database |
||||
|
queries = [Q(section=p.section.name, name=p.name) for p in preferences] |
||||
|
|
||||
|
query = queries[0] |
||||
|
for q in queries[1:]: |
||||
|
query |= q |
||||
|
preferences_qs = self.get_queryset().filter(query) |
||||
|
|
||||
|
# next, we generate a serializer for each database preference |
||||
|
serializer_objects = [] |
||||
|
for p in preferences_qs: |
||||
|
s = self.get_serializer_class()( |
||||
|
p, data={"value": payload[p.preference.identifier()]} |
||||
|
) |
||||
|
serializer_objects.append(s) |
||||
|
|
||||
|
validation_errors = {} |
||||
|
|
||||
|
# we check if any serializer is invalid |
||||
|
for s in serializer_objects: |
||||
|
if s.is_valid(): |
||||
|
continue |
||||
|
validation_errors[s.instance.preference.identifier()] = s.errors |
||||
|
|
||||
|
if validation_errors: |
||||
|
return Response(validation_errors, status=400) |
||||
|
|
||||
|
for s in serializer_objects: |
||||
|
s.save() |
||||
|
|
||||
|
return Response( |
||||
|
[s.data for s in serializer_objects], |
||||
|
status=200, |
||||
|
) |
||||
|
|
||||
|
|
||||
|
class GlobalPreferencePermission(permissions.DjangoModelPermissions): |
||||
|
perms_map = { |
||||
|
"GET": ["%(app_label)s.change_%(model_name)s"], |
||||
|
"OPTIONS": ["%(app_label)s.change_%(model_name)s"], |
||||
|
"HEAD": ["%(app_label)s.change_%(model_name)s"], |
||||
|
"POST": ["%(app_label)s.change_%(model_name)s"], |
||||
|
"PUT": ["%(app_label)s.change_%(model_name)s"], |
||||
|
"PATCH": ["%(app_label)s.change_%(model_name)s"], |
||||
|
"DELETE": ["%(app_label)s.change_%(model_name)s"], |
||||
|
} |
||||
|
|
||||
|
|
||||
|
class GlobalPreferencesViewSet(PreferenceViewSet): |
||||
|
queryset = models.GlobalPreferenceModel.objects.all() |
||||
|
serializer_class = serializers.GlobalPreferenceSerializer |
||||
|
permission_classes = [GlobalPreferencePermission] |
||||
|
|
||||
|
|
||||
|
class PerInstancePreferenceViewSet(PreferenceViewSet): |
||||
|
def get_manager(self): |
||||
|
return self.queryset.model.registry.manager( |
||||
|
instance=self.get_related_instance() |
||||
|
) |
||||
|
|
||||
|
def get_queryset(self): |
||||
|
return ( |
||||
|
super(PerInstancePreferenceViewSet, self) |
||||
|
.get_queryset() |
||||
|
.filter(instance=self.get_related_instance()) |
||||
|
) |
||||
|
|
||||
|
def get_related_instance(self): |
||||
|
""" |
||||
|
Override this to the instance bound to the preferences |
||||
|
""" |
||||
|
raise NotImplementedError |
||||
@ -0,0 +1,25 @@ |
|||||
|
from django.apps import AppConfig, apps |
||||
|
from django.conf import settings |
||||
|
from django.utils.translation import gettext_lazy as _ |
||||
|
from .registries import preference_models, global_preferences_registry |
||||
|
from .settings import preferences_settings |
||||
|
|
||||
|
|
||||
|
class DynamicPreferencesConfig(AppConfig): |
||||
|
name = "dynamic_preferences" |
||||
|
verbose_name = _("Settings") |
||||
|
default_auto_field = "django.db.models.AutoField" |
||||
|
icon = 'mi-settings' |
||||
|
|
||||
|
def ready(self): |
||||
|
if preferences_settings.ENABLE_GLOBAL_MODEL_AUTO_REGISTRATION: |
||||
|
GlobalPreferenceModel = self.get_model("GlobalPreferenceModel") |
||||
|
|
||||
|
preference_models.register( |
||||
|
GlobalPreferenceModel, global_preferences_registry |
||||
|
) |
||||
|
|
||||
|
# This will load all dynamic_preferences_registry.py files under |
||||
|
# installed apps |
||||
|
app_names = [app.name for app in apps.app_configs.values()] |
||||
|
global_preferences_registry.autodiscover(app_names) |
||||
@ -0,0 +1,15 @@ |
|||||
|
import json |
||||
|
|
||||
|
from django import forms |
||||
|
|
||||
|
from limitless_dashboard.fields.tinyeditor import TinyWidget |
||||
|
|
||||
|
from dynamic_preferences.preferences import Section |
||||
|
from dynamic_preferences.registries import global_preferences_registry |
||||
|
from dynamic_preferences.types import BasePreferenceType, BaseSerializer, LongStringPreference, StringPreference, \ |
||||
|
FilePreference |
||||
|
from utils.json_editor_field import JsonEditorWidget |
||||
|
|
||||
|
|
||||
|
class EditorPreferences(LongStringPreference): |
||||
|
widget = TinyWidget(attrs={'class': 'editor-field'}) |
||||
@ -0,0 +1,32 @@ |
|||||
|
class DynamicPreferencesException(Exception): |
||||
|
detail_default = "An exception occurred with django-dynamic-preferences" |
||||
|
|
||||
|
def __init__(self, detail=None): |
||||
|
if detail is not None: |
||||
|
self.detail = str(detail) |
||||
|
else: |
||||
|
self.detail = str(self.detail_default) |
||||
|
|
||||
|
def __str__(self): |
||||
|
return self.detail |
||||
|
|
||||
|
|
||||
|
class MissingDefault(DynamicPreferencesException): |
||||
|
detail_default = "You must provide a default value for all preferences" |
||||
|
|
||||
|
|
||||
|
class NotFoundInRegistry(DynamicPreferencesException, KeyError): |
||||
|
detail_default = "Preference with this name/section not found in registry" |
||||
|
|
||||
|
|
||||
|
class DoesNotExist(DynamicPreferencesException): |
||||
|
detail_default = "Cannot retrieve preference value, ensure the preference is correctly registered and database is synced" |
||||
|
|
||||
|
|
||||
|
class CachedValueNotFound(DynamicPreferencesException): |
||||
|
detail_default = "Cached value not found" |
||||
|
|
||||
|
|
||||
|
class MissingModel(DynamicPreferencesException): |
||||
|
detail_default = 'You must define a model choice through "model" \ |
||||
|
or "queryset" attribute' |
||||
@ -0,0 +1,152 @@ |
|||||
|
from six import string_types |
||||
|
from django import forms |
||||
|
from django.core.exceptions import ValidationError |
||||
|
from collections import OrderedDict |
||||
|
|
||||
|
from .registries import global_preferences_registry |
||||
|
from .models import GlobalPreferenceModel |
||||
|
from .exceptions import NotFoundInRegistry |
||||
|
|
||||
|
|
||||
|
class AbstractSinglePreferenceForm(forms.ModelForm): |
||||
|
class Meta: |
||||
|
fields = ("section", "name", "raw_value") |
||||
|
|
||||
|
def __init__(self, *args, **kwargs): |
||||
|
|
||||
|
self.instance = kwargs.get("instance") |
||||
|
initial = {} |
||||
|
if self.instance: |
||||
|
initial["raw_value"] = self.instance.value |
||||
|
kwargs["initial"] = initial |
||||
|
super(AbstractSinglePreferenceForm, self).__init__(*args, **kwargs) |
||||
|
|
||||
|
if self.instance.name: |
||||
|
self.fields["raw_value"] = self.instance.preference.setup_field() |
||||
|
|
||||
|
def clean(self): |
||||
|
cleaned_data = super(AbstractSinglePreferenceForm, self).clean() |
||||
|
try: |
||||
|
self.instance.name, self.instance.section = ( |
||||
|
cleaned_data["name"], |
||||
|
cleaned_data["section"], |
||||
|
) |
||||
|
except KeyError: # changelist form |
||||
|
pass |
||||
|
try: |
||||
|
self.instance.preference |
||||
|
except NotFoundInRegistry: |
||||
|
raise ValidationError(NotFoundInRegistry.detail_default) |
||||
|
return self.cleaned_data |
||||
|
|
||||
|
def save(self, *args, **kwargs): |
||||
|
self.instance.value = self.cleaned_data["raw_value"] |
||||
|
return super(AbstractSinglePreferenceForm, self).save(*args, **kwargs) |
||||
|
|
||||
|
|
||||
|
class SinglePerInstancePreferenceForm(AbstractSinglePreferenceForm): |
||||
|
class Meta: |
||||
|
fields = ("instance",) + AbstractSinglePreferenceForm.Meta.fields |
||||
|
|
||||
|
def clean(self): |
||||
|
cleaned_data = super(AbstractSinglePreferenceForm, self).clean() |
||||
|
try: |
||||
|
self.instance.name, self.instance.section = ( |
||||
|
cleaned_data["name"], |
||||
|
cleaned_data["section"], |
||||
|
) |
||||
|
except KeyError: # changelist form |
||||
|
pass |
||||
|
i = cleaned_data.get("instance") |
||||
|
if i: |
||||
|
self.instance.instance = i |
||||
|
try: |
||||
|
self.instance.preference |
||||
|
except NotFoundInRegistry: |
||||
|
raise ValidationError(NotFoundInRegistry.detail_default) |
||||
|
return self.cleaned_data |
||||
|
|
||||
|
|
||||
|
class GlobalSinglePreferenceForm(AbstractSinglePreferenceForm): |
||||
|
class Meta: |
||||
|
model = GlobalPreferenceModel |
||||
|
fields = AbstractSinglePreferenceForm.Meta.fields |
||||
|
|
||||
|
|
||||
|
def preference_form_builder(form_base_class, preferences=[], **kwargs): |
||||
|
""" |
||||
|
Return a form class for updating preferences |
||||
|
:param form_base_class: a Form class used as the base. Must have a ``registry` attribute |
||||
|
:param preferences: a list of :py:class: |
||||
|
:param section: a section where the form builder will load preferences |
||||
|
""" |
||||
|
registry = form_base_class.registry |
||||
|
preferences_obj = [] |
||||
|
if len(preferences) > 0: |
||||
|
# Preferences have been selected explicitly |
||||
|
for pref in preferences: |
||||
|
if isinstance(pref, string_types): |
||||
|
preferences_obj.append(registry.get(name=pref)) |
||||
|
elif type(pref) == tuple: |
||||
|
preferences_obj.append(registry.get(name=pref[0], section=pref[1])) |
||||
|
else: |
||||
|
raise NotImplementedError( |
||||
|
"The data you provide can't be converted to a Preference object" |
||||
|
) |
||||
|
elif kwargs.get("section", None): |
||||
|
# Try to use section param |
||||
|
preferences_obj = registry.preferences(section=kwargs.get("section", None)) |
||||
|
|
||||
|
else: |
||||
|
# display all preferences in the form |
||||
|
preferences_obj = registry.preferences() |
||||
|
|
||||
|
fields = OrderedDict() |
||||
|
instances = [] |
||||
|
if "model" in kwargs: |
||||
|
# backward compat, see #212 |
||||
|
manager_kwargs = kwargs.get("model") |
||||
|
else: |
||||
|
manager_kwargs = {"instance": kwargs.get("instance", None)} |
||||
|
manager = registry.manager(**manager_kwargs) |
||||
|
|
||||
|
for preference in preferences_obj: |
||||
|
f = preference.field |
||||
|
instance = manager.get_db_pref( |
||||
|
section=preference.section.name, name=preference.name |
||||
|
) |
||||
|
f.initial = instance.value |
||||
|
fields[preference.identifier()] = f |
||||
|
instances.append(instance) |
||||
|
|
||||
|
form_class = type("Custom" + form_base_class.__name__, (form_base_class,), {}) |
||||
|
form_class.base_fields = fields |
||||
|
form_class.preferences = preferences_obj |
||||
|
form_class.instances = instances |
||||
|
form_class.manager = manager |
||||
|
return form_class |
||||
|
|
||||
|
|
||||
|
def global_preference_form_builder(preferences=[], **kwargs): |
||||
|
""" |
||||
|
A shortcut :py:func:`preference_form_builder(GlobalPreferenceForm, preferences, **kwargs)` |
||||
|
""" |
||||
|
return preference_form_builder(GlobalPreferenceForm, preferences, **kwargs) |
||||
|
|
||||
|
|
||||
|
class PreferenceForm(forms.Form): |
||||
|
|
||||
|
registry = None |
||||
|
|
||||
|
def update_preferences(self, **kwargs): |
||||
|
for instance in self.instances: |
||||
|
self.manager.update_db_pref( |
||||
|
instance.preference.section.name, |
||||
|
instance.preference.name, |
||||
|
self.cleaned_data[instance.preference.identifier()], |
||||
|
) |
||||
|
|
||||
|
|
||||
|
class GlobalPreferenceForm(PreferenceForm): |
||||
|
|
||||
|
registry = global_preferences_registry |
||||
@ -0,0 +1,60 @@ |
|||||
|
# SOME DESCRIPTIVE TITLE. |
||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER |
||||
|
# This file is distributed under the same license as the PACKAGE package. |
||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. |
||||
|
# |
||||
|
msgid "" |
||||
|
msgstr "" |
||||
|
"Project-Id-Version: \n" |
||||
|
"Report-Msgid-Bugs-To: \n" |
||||
|
"POT-Creation-Date: 2018-11-08 10:37+0100\n" |
||||
|
"PO-Revision-Date: 2018-11-09 17:15+0100\n" |
||||
|
"Last-Translator: \n" |
||||
|
"Language-Team: \n" |
||||
|
"MIME-Version: 1.0\n" |
||||
|
"Content-Type: text/plain; charset=UTF-8\n" |
||||
|
"Content-Transfer-Encoding: 8bit\n" |
||||
|
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " |
||||
|
"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" |
||||
|
"Language: ar\n" |
||||
|
"X-Generator: Poedit 2.1.1\n" |
||||
|
|
||||
|
#: .\admin.py:56 |
||||
|
msgid "Default Value" |
||||
|
msgstr "القيمة الافتراضية" |
||||
|
|
||||
|
#: .\admin.py:65 |
||||
|
msgid "Section Name" |
||||
|
msgstr "إسم القسم" |
||||
|
|
||||
|
#: .\apps.py:9 |
||||
|
msgid "Dynamic Preferences" |
||||
|
msgstr "التفضيلات الديناميكية" |
||||
|
|
||||
|
#: .\models.py:25 |
||||
|
msgid "Name" |
||||
|
msgstr "الاسم" |
||||
|
|
||||
|
#: .\models.py:28 |
||||
|
msgid "Raw Value" |
||||
|
msgstr "القيمة الأولية" |
||||
|
|
||||
|
#: .\models.py:42 |
||||
|
msgid "Verbose Name" |
||||
|
msgstr "اسم مطول" |
||||
|
|
||||
|
#: .\models.py:47 |
||||
|
msgid "Help Text" |
||||
|
msgstr "نص المساعدة" |
||||
|
|
||||
|
#: .\models.py:84 |
||||
|
msgid "Global preference" |
||||
|
msgstr "التفضيل العام" |
||||
|
|
||||
|
#: .\models.py:85 |
||||
|
msgid "Global preferences" |
||||
|
msgstr "التفضيل العام" |
||||
|
|
||||
|
#: .\templates\dynamic_preferences\form.html:11 |
||||
|
msgid "Submit" |
||||
|
msgstr "إرسال" |
||||
@ -0,0 +1,71 @@ |
|||||
|
# SOME DESCRIPTIVE TITLE. |
||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER |
||||
|
# This file is distributed under the same license as the PACKAGE package. |
||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. |
||||
|
# |
||||
|
msgid "" |
||||
|
msgstr "" |
||||
|
"Project-Id-Version: \n" |
||||
|
"Report-Msgid-Bugs-To: \n" |
||||
|
"POT-Creation-Date: 2019-04-15 13:48+0200\n" |
||||
|
"PO-Revision-Date: 2018-11-09 17:14+0100\n" |
||||
|
"Last-Translator: \n" |
||||
|
"Language-Team: \n" |
||||
|
"Language: fr\n" |
||||
|
"MIME-Version: 1.0\n" |
||||
|
"Content-Type: text/plain; charset=UTF-8\n" |
||||
|
"Content-Transfer-Encoding: 8bit\n" |
||||
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n" |
||||
|
"X-Generator: Poedit 2.1.1\n" |
||||
|
|
||||
|
#: .\admin.py:56 |
||||
|
msgid "Default Value" |
||||
|
msgstr "Standardwert" |
||||
|
|
||||
|
#: .\admin.py:65 .\models.py:22 |
||||
|
msgid "Section Name" |
||||
|
msgstr "Abschnitt" |
||||
|
|
||||
|
#: .\apps.py:9 |
||||
|
msgid "Dynamic Preferences" |
||||
|
msgstr "Dynamische Einstellungen" |
||||
|
|
||||
|
#: .\models.py:25 |
||||
|
msgid "Name" |
||||
|
msgstr "Name" |
||||
|
|
||||
|
#: .\models.py:28 |
||||
|
msgid "Raw Value" |
||||
|
msgstr "Wert" |
||||
|
|
||||
|
#: .\models.py:42 |
||||
|
msgid "Verbose Name" |
||||
|
msgstr "Bezeichnung" |
||||
|
|
||||
|
#: .\models.py:47 |
||||
|
msgid "Help Text" |
||||
|
msgstr "Hilfetext" |
||||
|
|
||||
|
#: .\models.py:84 |
||||
|
msgid "Global preference" |
||||
|
msgstr "Globale Einstellung" |
||||
|
|
||||
|
#: .\models.py:85 |
||||
|
msgid "Global preferences" |
||||
|
msgstr "Globale Einstellungen" |
||||
|
|
||||
|
#: .\templates\dynamic_preferences\form.html:11 |
||||
|
msgid "Submit" |
||||
|
msgstr "Absenden" |
||||
|
|
||||
|
#: .\users\apps.py:11 |
||||
|
msgid "Preferences - Users" |
||||
|
msgstr "Einstellungen - Benutzer" |
||||
|
|
||||
|
#: .\users\models.py:15 |
||||
|
msgid "user preference" |
||||
|
msgstr "Benutzer Einstellung" |
||||
|
|
||||
|
#: .\users\models.py:16 |
||||
|
msgid "user preferences" |
||||
|
msgstr "Benutzer Einstellungen" |
||||
@ -0,0 +1,71 @@ |
|||||
|
# SOME DESCRIPTIVE TITLE. |
||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER |
||||
|
# This file is distributed under the same license as the PACKAGE package. |
||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. |
||||
|
# |
||||
|
#, fuzzy |
||||
|
msgid "" |
||||
|
msgstr "" |
||||
|
"Project-Id-Version: PACKAGE VERSION\n" |
||||
|
"Report-Msgid-Bugs-To: \n" |
||||
|
"POT-Creation-Date: 2023-02-16 15:12+0330\n" |
||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" |
||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" |
||||
|
"Language-Team: LANGUAGE <LL@li.org>\n" |
||||
|
"Language: \n" |
||||
|
"MIME-Version: 1.0\n" |
||||
|
"Content-Type: text/plain; charset=UTF-8\n" |
||||
|
"Content-Transfer-Encoding: 8bit\n" |
||||
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n" |
||||
|
|
||||
|
#: admin.py:69 |
||||
|
msgid "Default Value" |
||||
|
msgstr "مقدار پیشفرض" |
||||
|
|
||||
|
#: admin.py:78 models.py:30 |
||||
|
msgid "Section Name" |
||||
|
msgstr "عنوان بخش" |
||||
|
|
||||
|
#: apps.py:10 |
||||
|
msgid "Dynamic Preferences" |
||||
|
msgstr "تنظیمات" |
||||
|
|
||||
|
#: models.py:34 |
||||
|
msgid "Name" |
||||
|
msgstr "نام" |
||||
|
|
||||
|
#: models.py:37 |
||||
|
msgid "Raw Value" |
||||
|
msgstr "مقدار" |
||||
|
|
||||
|
#: models.py:51 |
||||
|
msgid "Verbose Name" |
||||
|
msgstr "نام" |
||||
|
|
||||
|
#: models.py:57 |
||||
|
msgid "Help Text" |
||||
|
msgstr "متن راهنما" |
||||
|
|
||||
|
#: models.py:94 |
||||
|
msgid "Global preference" |
||||
|
msgstr "تنطیمات عمومی" |
||||
|
|
||||
|
#: models.py:95 |
||||
|
msgid "Global preferences" |
||||
|
msgstr "تنطیمات عمومی" |
||||
|
|
||||
|
#: templates/dynamic_preferences/form.html:11 |
||||
|
msgid "Submit" |
||||
|
msgstr "ثبت" |
||||
|
|
||||
|
#: users/apps.py:11 |
||||
|
msgid "Preferences - Users" |
||||
|
msgstr "" |
||||
|
|
||||
|
#: users/models.py:14 |
||||
|
msgid "user preference" |
||||
|
msgstr "" |
||||
|
|
||||
|
#: users/models.py:15 |
||||
|
msgid "user preferences" |
||||
|
msgstr "" |
||||
@ -0,0 +1,59 @@ |
|||||
|
# SOME DESCRIPTIVE TITLE. |
||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER |
||||
|
# This file is distributed under the same license as the PACKAGE package. |
||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. |
||||
|
# |
||||
|
msgid "" |
||||
|
msgstr "" |
||||
|
"Project-Id-Version: \n" |
||||
|
"Report-Msgid-Bugs-To: \n" |
||||
|
"POT-Creation-Date: 2018-11-08 10:37+0100\n" |
||||
|
"PO-Revision-Date: 2018-11-09 17:14+0100\n" |
||||
|
"Last-Translator: \n" |
||||
|
"Language-Team: \n" |
||||
|
"MIME-Version: 1.0\n" |
||||
|
"Content-Type: text/plain; charset=UTF-8\n" |
||||
|
"Content-Transfer-Encoding: 8bit\n" |
||||
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n" |
||||
|
"Language: fr\n" |
||||
|
"X-Generator: Poedit 2.1.1\n" |
||||
|
|
||||
|
#: .\admin.py:56 |
||||
|
msgid "Default Value" |
||||
|
msgstr "Valeur par défaut" |
||||
|
|
||||
|
#: .\admin.py:65 |
||||
|
msgid "Section Name" |
||||
|
msgstr "Nom de la Section" |
||||
|
|
||||
|
#: .\apps.py:9 |
||||
|
msgid "Dynamic Preferences" |
||||
|
msgstr "Préférences dynamiques" |
||||
|
|
||||
|
#: .\models.py:25 |
||||
|
msgid "Name" |
||||
|
msgstr "Nom" |
||||
|
|
||||
|
#: .\models.py:28 |
||||
|
msgid "Raw Value" |
||||
|
msgstr "Valeur RAW" |
||||
|
|
||||
|
#: .\models.py:42 |
||||
|
msgid "Verbose Name" |
||||
|
msgstr "Nom détaillé" |
||||
|
|
||||
|
#: .\models.py:47 |
||||
|
msgid "Help Text" |
||||
|
msgstr "Texte d'Aide" |
||||
|
|
||||
|
#: .\models.py:84 |
||||
|
msgid "Global preference" |
||||
|
msgstr "Préférence globale" |
||||
|
|
||||
|
#: .\models.py:85 |
||||
|
msgid "Global preferences" |
||||
|
msgstr "Préférences globales" |
||||
|
|
||||
|
#: .\templates\dynamic_preferences\form.html:11 |
||||
|
msgid "Submit" |
||||
|
msgstr "Valider" |
||||
@ -0,0 +1,71 @@ |
|||||
|
# SOME DESCRIPTIVE TITLE. |
||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER |
||||
|
# This file is distributed under the same license as the PACKAGE package. |
||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. |
||||
|
# |
||||
|
msgid "" |
||||
|
msgstr "" |
||||
|
"Project-Id-Version: \n" |
||||
|
"Report-Msgid-Bugs-To: \n" |
||||
|
"POT-Creation-Date: 2019-04-15 13:48+0200\n" |
||||
|
"PO-Revision-Date: 2020-09-23 23:59+0200\n" |
||||
|
"Last-Translator: \n" |
||||
|
"Language-Team: \n" |
||||
|
"Language: pl\n" |
||||
|
"MIME-Version: 1.0\n" |
||||
|
"Content-Type: text/plain; charset=UTF-8\n" |
||||
|
"Content-Transfer-Encoding: 8bit\n" |
||||
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n" |
||||
|
"X-Generator: Poedit 2.4.1\n" |
||||
|
|
||||
|
#: .\admin.py:56 |
||||
|
msgid "Default Value" |
||||
|
msgstr "Wartość domyślna" |
||||
|
|
||||
|
#: .\admin.py:65 .\models.py:22 |
||||
|
msgid "Section Name" |
||||
|
msgstr "Nazwa sekcji" |
||||
|
|
||||
|
#: .\apps.py:9 |
||||
|
msgid "Dynamic Preferences" |
||||
|
msgstr "Dynamiczne Preferencje" |
||||
|
|
||||
|
#: .\models.py:25 |
||||
|
msgid "Name" |
||||
|
msgstr "Nazwa" |
||||
|
|
||||
|
#: .\models.py:28 |
||||
|
msgid "Raw Value" |
||||
|
msgstr "Surowa wartość" |
||||
|
|
||||
|
#: .\models.py:42 |
||||
|
msgid "Verbose Name" |
||||
|
msgstr "Nazwa Szczegółowa" |
||||
|
|
||||
|
#: .\models.py:47 |
||||
|
msgid "Help Text" |
||||
|
msgstr "Tekst pomocy" |
||||
|
|
||||
|
#: .\models.py:84 |
||||
|
msgid "Global preference" |
||||
|
msgstr "Globalna preferencja" |
||||
|
|
||||
|
#: .\models.py:85 |
||||
|
msgid "Global preferences" |
||||
|
msgstr "Globalne Preferencje" |
||||
|
|
||||
|
#: .\templates\dynamic_preferences\form.html:11 |
||||
|
msgid "Submit" |
||||
|
msgstr "Wyślij" |
||||
|
|
||||
|
#: .\users\apps.py:11 |
||||
|
msgid "Preferences - Users" |
||||
|
msgstr "Preferencje - Użytkownicy" |
||||
|
|
||||
|
#: .\users\models.py:15 |
||||
|
msgid "user preference" |
||||
|
msgstr "preferencja użytkownika" |
||||
|
|
||||
|
#: .\users\models.py:16 |
||||
|
msgid "user preferences" |
||||
|
msgstr "preferencje użytkownika" |
||||
@ -0,0 +1 @@ |
|||||
|
__author__ = "agateblue" |
||||
@ -0,0 +1 @@ |
|||||
|
__author__ = "agateblue" |
||||
@ -0,0 +1,76 @@ |
|||||
|
from django.core.management.base import BaseCommand |
||||
|
from dynamic_preferences.exceptions import NotFoundInRegistry |
||||
|
from dynamic_preferences.models import GlobalPreferenceModel |
||||
|
from dynamic_preferences.registries import ( |
||||
|
global_preferences_registry, |
||||
|
preference_models, |
||||
|
) |
||||
|
from dynamic_preferences.settings import preferences_settings |
||||
|
|
||||
|
|
||||
|
def delete_preferences(queryset): |
||||
|
""" |
||||
|
Delete preferences objects if they are not present in registry. |
||||
|
Return a list of deleted objects |
||||
|
""" |
||||
|
deleted = [] |
||||
|
|
||||
|
# Iterate through preferences. If an error is raised when accessing |
||||
|
# preference object, just delete it |
||||
|
for p in queryset: |
||||
|
try: |
||||
|
p.registry.get(section=p.section, name=p.name, fallback=False) |
||||
|
except NotFoundInRegistry: |
||||
|
p.delete() |
||||
|
deleted.append(p) |
||||
|
|
||||
|
return deleted |
||||
|
|
||||
|
|
||||
|
class Command(BaseCommand): |
||||
|
help = ( |
||||
|
"Find and delete preferences from database if they don't exist in " |
||||
|
"registries. Create preferences that are not present in database" |
||||
|
"(except when invoked with --skip_create)." |
||||
|
) |
||||
|
|
||||
|
def add_arguments(self, parser): |
||||
|
parser.add_argument( |
||||
|
"--skip_create", |
||||
|
action="store_true", |
||||
|
help="Forces to skip the creation step for missing preferences", |
||||
|
) |
||||
|
|
||||
|
def handle(self, *args, **options): |
||||
|
skip_create = options["skip_create"] |
||||
|
|
||||
|
# Create needed preferences |
||||
|
# Global |
||||
|
if not skip_create: |
||||
|
self.stdout.write("Creating missing global preferences...") |
||||
|
manager = global_preferences_registry.manager() |
||||
|
manager.all() |
||||
|
|
||||
|
deleted = delete_preferences(GlobalPreferenceModel.objects.all()) |
||||
|
message = "Deleted {deleted} global preferences".format(deleted=len(deleted)) |
||||
|
self.stdout.write(message) |
||||
|
|
||||
|
for preference_model, registry in preference_models.items(): |
||||
|
deleted = delete_preferences(preference_model.objects.all()) |
||||
|
message = "Deleted {deleted} {model} preferences".format( |
||||
|
deleted=len(deleted), |
||||
|
model=preference_model.__name__, |
||||
|
) |
||||
|
self.stdout.write(message) |
||||
|
if not hasattr(preference_model, "get_instance_model"): |
||||
|
continue |
||||
|
|
||||
|
if skip_create: |
||||
|
continue |
||||
|
|
||||
|
message = "Creating missing preferences for {model} model...".format( |
||||
|
model=preference_model.get_instance_model().__name__, |
||||
|
) |
||||
|
self.stdout.write(message) |
||||
|
for instance in preference_model.get_instance_model().objects.all(): |
||||
|
getattr(instance, preferences_settings.MANAGER_ATTRIBUTE).all() |
||||
@ -0,0 +1,239 @@ |
|||||
|
try: |
||||
|
from collections.abc import Mapping |
||||
|
except ImportError: |
||||
|
from collections import Mapping |
||||
|
|
||||
|
from .settings import preferences_settings |
||||
|
from .exceptions import CachedValueNotFound, DoesNotExist |
||||
|
from .signals import preference_updated |
||||
|
|
||||
|
|
||||
|
class PreferencesManager(Mapping): |
||||
|
|
||||
|
"""Handle retrieving / caching of preferences""" |
||||
|
|
||||
|
def __init__(self, model, registry, **kwargs): |
||||
|
self.model = model |
||||
|
self.registry = registry |
||||
|
self.instance = kwargs.get("instance") |
||||
|
|
||||
|
@property |
||||
|
def queryset(self): |
||||
|
qs = self.model.objects.all() |
||||
|
if self.instance: |
||||
|
qs = qs.filter(instance=self.instance) |
||||
|
return qs |
||||
|
|
||||
|
@property |
||||
|
def cache(self): |
||||
|
from django.core.cache import caches |
||||
|
|
||||
|
return caches[preferences_settings.CACHE_NAME] |
||||
|
|
||||
|
def __getitem__(self, key): |
||||
|
return self.get(key) |
||||
|
|
||||
|
def __setitem__(self, key, value): |
||||
|
section, name = self.parse_lookup(key) |
||||
|
preference = self.registry.get(section=section, name=name, fallback=False) |
||||
|
preference.validate(value) |
||||
|
self.update_db_pref(section=section, name=name, value=value) |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return repr(self.all()) |
||||
|
|
||||
|
def __iter__(self): |
||||
|
return self.all().__iter__() |
||||
|
|
||||
|
def __len__(self): |
||||
|
return len(self.all()) |
||||
|
|
||||
|
def by_name(self): |
||||
|
"""Return a dictionary with preferences identifiers and values, but without the section name in the identifier""" |
||||
|
return { |
||||
|
key.split(preferences_settings.SECTION_KEY_SEPARATOR)[-1]: value |
||||
|
for key, value in self.all().items() |
||||
|
} |
||||
|
|
||||
|
def get_by_name(self, name): |
||||
|
return self.get(self.registry.get_by_name(name).identifier()) |
||||
|
|
||||
|
def get_cache_key(self, section, name): |
||||
|
"""Return the cache key corresponding to a given preference""" |
||||
|
if not self.instance: |
||||
|
return "dynamic_preferences_{0}_{1}_{2}".format( |
||||
|
self.model.__name__, section, name |
||||
|
) |
||||
|
return "dynamic_preferences_{0}_{1}_{2}_{3}".format( |
||||
|
self.model.__name__, self.instance.pk, section, name, self.instance.pk |
||||
|
) |
||||
|
|
||||
|
def from_cache(self, section, name): |
||||
|
"""Return a preference raw_value from cache""" |
||||
|
cached_value = self.cache.get( |
||||
|
self.get_cache_key(section, name), CachedValueNotFound |
||||
|
) |
||||
|
|
||||
|
if cached_value is CachedValueNotFound: |
||||
|
raise CachedValueNotFound |
||||
|
|
||||
|
if cached_value == preferences_settings.CACHE_NONE_VALUE: |
||||
|
cached_value = None |
||||
|
return self.registry.get(section=section, name=name).serializer.deserialize( |
||||
|
cached_value |
||||
|
) |
||||
|
|
||||
|
def many_from_cache(self, preferences): |
||||
|
""" |
||||
|
Return cached value for given preferences |
||||
|
missing preferences will be skipped |
||||
|
""" |
||||
|
keys = {p: self.get_cache_key(p.section.name, p.name) for p in preferences} |
||||
|
cached = self.cache.get_many(list(keys.values())) |
||||
|
|
||||
|
for k, v in cached.items(): |
||||
|
# we replace dummy cached values by None here, if needed |
||||
|
if v == preferences_settings.CACHE_NONE_VALUE: |
||||
|
cached[k] = None |
||||
|
|
||||
|
# we have to remap returned value since the underlying cached keys |
||||
|
# are not usable for an end user |
||||
|
return { |
||||
|
p.identifier(): p.serializer.deserialize(cached[k]) |
||||
|
for p, k in keys.items() |
||||
|
if k in cached |
||||
|
} |
||||
|
|
||||
|
def to_cache(self, pref): |
||||
|
""" |
||||
|
Update/create the cache value for the given preference model instance |
||||
|
""" |
||||
|
key = self.get_cache_key(pref.section, pref.name) |
||||
|
value = pref.raw_value |
||||
|
if value is None or value == "": |
||||
|
# some cache backends refuse to cache None or empty values |
||||
|
# resulting in more DB queries, so we cache an arbitrary value |
||||
|
# to ensure the cache is hot (even with empty values) |
||||
|
value = preferences_settings.CACHE_NONE_VALUE |
||||
|
self.cache.set(key, value) |
||||
|
|
||||
|
def pref_obj(self, section, name): |
||||
|
return self.registry.get(section=section, name=name) |
||||
|
|
||||
|
def parse_lookup(self, lookup): |
||||
|
try: |
||||
|
section, name = lookup.split(preferences_settings.SECTION_KEY_SEPARATOR) |
||||
|
except ValueError: |
||||
|
name = lookup |
||||
|
section = None |
||||
|
return section, name |
||||
|
|
||||
|
def get(self, key, no_cache=False): |
||||
|
"""Return the value of a single preference using a dotted path key |
||||
|
:arg no_cache: if true, the cache is bypassed |
||||
|
""" |
||||
|
section, name = self.parse_lookup(key) |
||||
|
preference = self.registry.get(section=section, name=name, fallback=False) |
||||
|
if no_cache or not preferences_settings.ENABLE_CACHE: |
||||
|
return self.get_db_pref(section=section, name=name).value |
||||
|
|
||||
|
try: |
||||
|
return self.from_cache(section, name) |
||||
|
except CachedValueNotFound: |
||||
|
pass |
||||
|
|
||||
|
db_pref = self.get_db_pref(section=section, name=name) |
||||
|
self.to_cache(db_pref) |
||||
|
return db_pref.value |
||||
|
|
||||
|
def get_db_pref(self, section, name): |
||||
|
try: |
||||
|
pref = self.queryset.get(section=section, name=name) |
||||
|
except self.model.DoesNotExist: |
||||
|
pref_obj = self.pref_obj(section=section, name=name) |
||||
|
pref = self.create_db_pref( |
||||
|
section=section, name=name, value=pref_obj.get("default") |
||||
|
) |
||||
|
|
||||
|
return pref |
||||
|
|
||||
|
def update_db_pref(self, section, name, value): |
||||
|
try: |
||||
|
db_pref = self.queryset.get(section=section, name=name) |
||||
|
old_value = db_pref.value |
||||
|
db_pref.value = value |
||||
|
db_pref.save() |
||||
|
preference_updated.send( |
||||
|
sender=self.__class__, |
||||
|
section=section, |
||||
|
name=name, |
||||
|
old_value=old_value, |
||||
|
new_value=value, |
||||
|
) |
||||
|
except self.model.DoesNotExist: |
||||
|
return self.create_db_pref(section, name, value) |
||||
|
|
||||
|
return db_pref |
||||
|
|
||||
|
def create_db_pref(self, section, name, value): |
||||
|
kwargs = { |
||||
|
"section": section, |
||||
|
"name": name, |
||||
|
} |
||||
|
if self.instance: |
||||
|
kwargs["instance"] = self.instance |
||||
|
|
||||
|
# this is a just a shortcut to get the raw, serialized value |
||||
|
# so we can pass it to get_or_create |
||||
|
m = self.model(**kwargs) |
||||
|
m.value = value |
||||
|
raw_value = m.raw_value |
||||
|
|
||||
|
db_pref, created = self.model.objects.get_or_create(**kwargs) |
||||
|
if created and db_pref.raw_value != raw_value: |
||||
|
db_pref.raw_value = raw_value |
||||
|
db_pref.save() |
||||
|
|
||||
|
return db_pref |
||||
|
|
||||
|
def all(self): |
||||
|
"""Return a dictionary containing all preferences by section |
||||
|
Loaded from cache or from db in case of cold cache |
||||
|
""" |
||||
|
if not preferences_settings.ENABLE_CACHE: |
||||
|
return self.load_from_db() |
||||
|
|
||||
|
preferences = self.registry.preferences() |
||||
|
|
||||
|
# first we hit the cache once for all existing preferences |
||||
|
a = self.many_from_cache(preferences) |
||||
|
if len(a) == len(preferences): |
||||
|
return a # avoid database hit if not necessary |
||||
|
|
||||
|
# then we fill those that miss, but exist in the database |
||||
|
# (just hit the database for all of them, filtering is complicated, and |
||||
|
# in most cases you'd need to grab the majority of them anyway) |
||||
|
a.update(self.load_from_db(cache=True)) |
||||
|
return a |
||||
|
|
||||
|
def load_from_db(self, cache=False): |
||||
|
"""Return a dictionary of preferences by section directly from DB""" |
||||
|
a = {} |
||||
|
db_prefs = {p.preference.identifier(): p for p in self.queryset} |
||||
|
for preference in self.registry.preferences(): |
||||
|
try: |
||||
|
db_pref = db_prefs[preference.identifier()] |
||||
|
except KeyError: |
||||
|
db_pref = self.create_db_pref( |
||||
|
section=preference.section.name, |
||||
|
name=preference.name, |
||||
|
value=preference.get("default"), |
||||
|
) |
||||
|
else: |
||||
|
# cache if create_db_pref() hasn't already done so |
||||
|
if cache: |
||||
|
self.to_cache(db_pref) |
||||
|
|
||||
|
a[preference.identifier()] = db_pref.value |
||||
|
|
||||
|
return a |
||||
@ -0,0 +1,50 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
from __future__ import unicode_literals |
||||
|
|
||||
|
from django.db import models, migrations |
||||
|
from django.conf import settings |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.CreateModel( |
||||
|
name="GlobalPreferenceModel", |
||||
|
fields=[ |
||||
|
( |
||||
|
"id", |
||||
|
models.AutoField( |
||||
|
primary_key=True, |
||||
|
serialize=False, |
||||
|
verbose_name="ID", |
||||
|
auto_created=True, |
||||
|
), |
||||
|
), |
||||
|
( |
||||
|
"section", |
||||
|
models.CharField( |
||||
|
blank=True, |
||||
|
default=None, |
||||
|
null=True, |
||||
|
max_length=150, |
||||
|
db_index=True, |
||||
|
), |
||||
|
), |
||||
|
("name", models.CharField(max_length=150, db_index=True)), |
||||
|
("raw_value", models.TextField(blank=True, null=True)), |
||||
|
], |
||||
|
options={ |
||||
|
"verbose_name_plural": "global preferences", |
||||
|
"verbose_name": "global preference", |
||||
|
}, |
||||
|
bases=(models.Model,), |
||||
|
), |
||||
|
migrations.AlterUniqueTogether( |
||||
|
name="globalpreferencemodel", |
||||
|
unique_together=set([("section", "name")]), |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1,27 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
from __future__ import unicode_literals |
||||
|
|
||||
|
from django.db import models, migrations |
||||
|
from django.conf import settings |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
("dynamic_preferences", "0001_initial"), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.AlterField( |
||||
|
model_name="globalpreferencemodel", |
||||
|
name="name", |
||||
|
field=models.CharField(max_length=150, db_index=True), |
||||
|
), |
||||
|
migrations.AlterField( |
||||
|
model_name="globalpreferencemodel", |
||||
|
name="section", |
||||
|
field=models.CharField( |
||||
|
max_length=150, blank=True, db_index=True, default=None, null=True |
||||
|
), |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1,33 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
from __future__ import unicode_literals |
||||
|
|
||||
|
from django.db import models, migrations |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
("dynamic_preferences", "0002_auto_20150712_0332"), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.AlterField( |
||||
|
model_name="globalpreferencemodel", |
||||
|
name="name", |
||||
|
field=models.CharField(max_length=150, db_index=True), |
||||
|
preserve_default=True, |
||||
|
), |
||||
|
migrations.AlterField( |
||||
|
model_name="globalpreferencemodel", |
||||
|
name="section", |
||||
|
field=models.CharField( |
||||
|
max_length=150, |
||||
|
null=True, |
||||
|
default=None, |
||||
|
db_index=True, |
||||
|
blank=True, |
||||
|
verbose_name="Section Name", |
||||
|
), |
||||
|
preserve_default=True, |
||||
|
), |
||||
|
] |
||||
Some files were not shown because too many files changed in this diff
Write
Preview
Loading…
Cancel
Save
Reference in new issue