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