adjango 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adjango-0.0.1/LICENSE +21 -0
- adjango-0.0.1/MANIFEST.in +2 -0
- adjango-0.0.1/PKG-INFO +65 -0
- adjango-0.0.1/README.md +37 -0
- adjango-0.0.1/adjango/__init__.py +0 -0
- adjango-0.0.1/adjango/apps.py +8 -0
- adjango-0.0.1/adjango/management/__init__.py +0 -0
- adjango-0.0.1/adjango/management/commands/__init__.py +0 -0
- adjango-0.0.1/adjango/management/commands/add_paths.py +137 -0
- adjango-0.0.1/adjango/management/commands/copy_proj.py +146 -0
- adjango-0.0.1/adjango/management/commands/dumpdata_to_dir.py +33 -0
- adjango-0.0.1/adjango/management/commands/loaddata_from_dir.py +53 -0
- adjango-0.0.1/adjango/management/commands/remakemigrations.py +42 -0
- adjango-0.0.1/adjango/middleware.py +18 -0
- adjango-0.0.1/adjango/utils/__init__.py +0 -0
- adjango-0.0.1/adjango/utils/base.py +200 -0
- adjango-0.0.1/adjango/utils/common.py +5 -0
- adjango-0.0.1/adjango/utils/funcs.py +158 -0
- adjango-0.0.1/adjango/utils/mail.py +30 -0
- adjango-0.0.1/adjango/utils/rediser.py +105 -0
- adjango-0.0.1/adjango.egg-info/PKG-INFO +65 -0
- adjango-0.0.1/adjango.egg-info/SOURCES.txt +26 -0
- adjango-0.0.1/adjango.egg-info/dependency_links.txt +1 -0
- adjango-0.0.1/adjango.egg-info/requires.txt +3 -0
- adjango-0.0.1/adjango.egg-info/top_level.txt +1 -0
- adjango-0.0.1/setup.cfg +7 -0
- adjango-0.0.1/setup.py +41 -0
adjango-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Artasov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
adjango-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: adjango
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A library with many features for interacting with Django
|
|
5
|
+
Home-page: https://github.com/Artasov/adjango
|
|
6
|
+
Author: xlartas
|
|
7
|
+
Author-email: ivanhvalevskey@gmail.com
|
|
8
|
+
Project-URL: Source, https://github.com/Artasov/adjango
|
|
9
|
+
Project-URL: Tracker, https://github.com/Artasov/adjango/issues
|
|
10
|
+
Keywords: adjango django utils funcs features async
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Framework :: Django :: 4.0
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Intended Audience :: Developers
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: Django<5.3,>=4.0
|
|
26
|
+
Requires-Dist: pyperclip>=1.8.0
|
|
27
|
+
Requires-Dist: aiohttp>=3.8.0
|
|
28
|
+
|
|
29
|
+
# ADjango
|
|
30
|
+
|
|
31
|
+
**ADjango**
|
|
32
|
+
> Sometimes I use this in different projects, so I decided to put it on pypi
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
```bash
|
|
36
|
+
pip install adjango
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Settings
|
|
40
|
+
|
|
41
|
+
* ### Add the application to the project.
|
|
42
|
+
```python
|
|
43
|
+
INSTALLED_APPS = [
|
|
44
|
+
#...
|
|
45
|
+
'adjango',
|
|
46
|
+
]
|
|
47
|
+
```
|
|
48
|
+
* ### In `settings.py` set the params
|
|
49
|
+
```python
|
|
50
|
+
ADJANGO_BACKENDS_APPS = BASE_DIR / 'apps'
|
|
51
|
+
ADJANGO_FRONTEND_APPS = BASE_DIR.parent / 'frontend' / 'src' / 'apps'
|
|
52
|
+
ADJANGO_APPS_PREPATH = 'apps.'
|
|
53
|
+
ADJANGO_EXCEPTION_REPORT_EMAIL = ('ivanhvalevskey@gmail.com',)
|
|
54
|
+
ADJANGO_EXCEPTION_REPORT_TEMPLATE = 'core/error_report.html'
|
|
55
|
+
ADJANGO_LOGGER_NAME = 'global'
|
|
56
|
+
ADJANGO_EMAIL_LOGGER_NAME = 'email'
|
|
57
|
+
```
|
|
58
|
+
```python
|
|
59
|
+
MIDDLEWARE = [
|
|
60
|
+
...
|
|
61
|
+
# add request.ip in views
|
|
62
|
+
'adjango.middleware.IPAddressMiddleware',
|
|
63
|
+
...
|
|
64
|
+
]
|
|
65
|
+
```
|
adjango-0.0.1/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# ADjango
|
|
2
|
+
|
|
3
|
+
**ADjango**
|
|
4
|
+
> Sometimes I use this in different projects, so I decided to put it on pypi
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
```bash
|
|
8
|
+
pip install adjango
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Settings
|
|
12
|
+
|
|
13
|
+
* ### Add the application to the project.
|
|
14
|
+
```python
|
|
15
|
+
INSTALLED_APPS = [
|
|
16
|
+
#...
|
|
17
|
+
'adjango',
|
|
18
|
+
]
|
|
19
|
+
```
|
|
20
|
+
* ### In `settings.py` set the params
|
|
21
|
+
```python
|
|
22
|
+
ADJANGO_BACKENDS_APPS = BASE_DIR / 'apps'
|
|
23
|
+
ADJANGO_FRONTEND_APPS = BASE_DIR.parent / 'frontend' / 'src' / 'apps'
|
|
24
|
+
ADJANGO_APPS_PREPATH = 'apps.'
|
|
25
|
+
ADJANGO_EXCEPTION_REPORT_EMAIL = ('ivanhvalevskey@gmail.com',)
|
|
26
|
+
ADJANGO_EXCEPTION_REPORT_TEMPLATE = 'core/error_report.html'
|
|
27
|
+
ADJANGO_LOGGER_NAME = 'global'
|
|
28
|
+
ADJANGO_EMAIL_LOGGER_NAME = 'email'
|
|
29
|
+
```
|
|
30
|
+
```python
|
|
31
|
+
MIDDLEWARE = [
|
|
32
|
+
...
|
|
33
|
+
# add request.ip in views
|
|
34
|
+
'adjango.middleware.IPAddressMiddleware',
|
|
35
|
+
...
|
|
36
|
+
]
|
|
37
|
+
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.core.management.base import BaseCommand
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Command(BaseCommand):
|
|
8
|
+
"""
|
|
9
|
+
Adds a file path comment at the top of each .py and frontend file in the specified app(s).
|
|
10
|
+
|
|
11
|
+
@usage:
|
|
12
|
+
python manage.py add_paths [app_name] [--exclude name1 name2 ...]
|
|
13
|
+
|
|
14
|
+
@arg app_name: (optional) Name of the app to process. If not specified, all apps will be processed.
|
|
15
|
+
@arg --exclude: (optional) List of folder or file names to exclude from processing.
|
|
16
|
+
Defaults to excluding 'migrations' folders and '__init__.py' files.
|
|
17
|
+
|
|
18
|
+
@behavior:
|
|
19
|
+
- Process all backend and frontend apps (with default exclusions):
|
|
20
|
+
python manage.py add_paths
|
|
21
|
+
|
|
22
|
+
- Process a specific app (e.g., 'social_oauth'):
|
|
23
|
+
python manage.py add_paths social_oauth
|
|
24
|
+
|
|
25
|
+
- Process all apps, excluding additional folders or files:
|
|
26
|
+
python manage.py add_paths --exclude migrations __init__.py tests
|
|
27
|
+
|
|
28
|
+
- Process a specific app with specified exclusions:
|
|
29
|
+
python manage.py add_paths social_oauth --exclude migrations __init__.py tests
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
help = 'Adds a file path comment at the top of .py and frontend files in the specified app(s).'
|
|
33
|
+
|
|
34
|
+
def add_arguments(self, parser):
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
'app_name',
|
|
37
|
+
nargs='?',
|
|
38
|
+
default=None,
|
|
39
|
+
help='(Optional) Name of the app to process. If not specified, all apps will be processed.'
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
'--exclude',
|
|
43
|
+
nargs='*',
|
|
44
|
+
default=['migrations', '__init__.py'],
|
|
45
|
+
help="List of folder or file names to exclude. Defaults to 'migrations' and '__init__.py'."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def handle(self, *args, **options):
|
|
49
|
+
app_name = options['app_name']
|
|
50
|
+
exclude_names = options['exclude']
|
|
51
|
+
backend_apps_dir = os.path.join(settings.BASE_DIR, 'apps')
|
|
52
|
+
frontend_apps_dir = os.path.join(settings.FRONTEND_DIR, 'src', 'apps')
|
|
53
|
+
|
|
54
|
+
# Process backend apps
|
|
55
|
+
self.process_apps(app_name, exclude_names, backend_apps_dir, '.py')
|
|
56
|
+
|
|
57
|
+
# Process frontend apps
|
|
58
|
+
self.process_apps(app_name, exclude_names, frontend_apps_dir, ('.ts', '.tsx', '.js', '.jsx'))
|
|
59
|
+
|
|
60
|
+
def process_apps(self, app_name, exclude_names, apps_dir, file_extensions):
|
|
61
|
+
if app_name:
|
|
62
|
+
app_paths = [os.path.join(apps_dir, app_name)]
|
|
63
|
+
if not os.path.exists(app_paths[0]):
|
|
64
|
+
self.stderr.write(f"App '{app_name}' does not exist in the specified directory.")
|
|
65
|
+
return
|
|
66
|
+
else:
|
|
67
|
+
# Process all apps in the directory
|
|
68
|
+
app_paths = [
|
|
69
|
+
os.path.join(apps_dir, name) for name in os.listdir(apps_dir)
|
|
70
|
+
if os.path.isdir(os.path.join(apps_dir, name)) and not name.startswith('__')
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
for app_path in app_paths:
|
|
74
|
+
current_app = os.path.basename(app_path)
|
|
75
|
+
self.stdout.write(f"Processing app '{current_app}'...")
|
|
76
|
+
for root, dirs, files in os.walk(app_path):
|
|
77
|
+
# Exclude specified directories
|
|
78
|
+
dirs[:] = [d for d in dirs if d not in exclude_names]
|
|
79
|
+
for file_name in files:
|
|
80
|
+
if file_name.endswith(file_extensions) and file_name not in exclude_names:
|
|
81
|
+
file_path = os.path.join(root, file_name)
|
|
82
|
+
relative_path = os.path.relpath(file_path, apps_dir)
|
|
83
|
+
self.check_and_fix_file(file_path, relative_path, file_extensions)
|
|
84
|
+
|
|
85
|
+
def check_and_fix_file(self, file_path, relative_path, file_extensions):
|
|
86
|
+
"""
|
|
87
|
+
Checks and updates the file by adding or correcting the file path comment at the top.
|
|
88
|
+
|
|
89
|
+
@param file_path: Full path to the file.
|
|
90
|
+
@param relative_path: Relative path of the file from the apps directory.
|
|
91
|
+
@param file_extensions: List of valid file extensions to determine comment type.
|
|
92
|
+
"""
|
|
93
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
94
|
+
lines = f.readlines()
|
|
95
|
+
|
|
96
|
+
# Determine comment style based on file extension
|
|
97
|
+
if file_path.endswith('.py'):
|
|
98
|
+
expected_comment = f'# {relative_path.replace(os.sep, "/")}\n'
|
|
99
|
+
comment_prefix = '#'
|
|
100
|
+
else:
|
|
101
|
+
expected_comment = f'// {relative_path.replace(os.sep, "/")}\n'
|
|
102
|
+
comment_prefix = '//'
|
|
103
|
+
|
|
104
|
+
if lines:
|
|
105
|
+
first_line = lines[0].strip()
|
|
106
|
+
if first_line == expected_comment.strip():
|
|
107
|
+
# The file already has the correct comment
|
|
108
|
+
# self.stdout.write(f"{file_path}: OK")
|
|
109
|
+
return
|
|
110
|
+
elif first_line.startswith(('#', '//')):
|
|
111
|
+
# The file has a comment, but it's incorrect
|
|
112
|
+
if first_line != expected_comment.strip():
|
|
113
|
+
# Update the comment
|
|
114
|
+
lines[0] = expected_comment
|
|
115
|
+
self.write_file(file_path, lines)
|
|
116
|
+
self.stdout.write(f"{file_path}: Updated comment")
|
|
117
|
+
else:
|
|
118
|
+
# The first line is not a comment, insert the correct comment at the top
|
|
119
|
+
lines.insert(0, expected_comment)
|
|
120
|
+
self.write_file(file_path, lines)
|
|
121
|
+
self.stdout.write(f"{file_path}: Added missing comment")
|
|
122
|
+
else:
|
|
123
|
+
# File is empty, write the comment
|
|
124
|
+
lines = [expected_comment]
|
|
125
|
+
self.write_file(file_path, lines)
|
|
126
|
+
self.stdout.write(f"{file_path}: Added missing comment to empty file")
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def write_file(file_path, lines):
|
|
130
|
+
"""
|
|
131
|
+
Writes the updated lines back to the file.
|
|
132
|
+
|
|
133
|
+
@param file_path: Full path to the file.
|
|
134
|
+
@param lines: List of lines to write to the file.
|
|
135
|
+
"""
|
|
136
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
137
|
+
f.writelines(lines)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# core/management/commands/copy_proj.py
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import pyperclip
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.core.management.base import BaseCommand
|
|
7
|
+
|
|
8
|
+
TARGET_ALL = [
|
|
9
|
+
'controllers',
|
|
10
|
+
'serializers',
|
|
11
|
+
# 'exceptions',
|
|
12
|
+
'tests',
|
|
13
|
+
'models',
|
|
14
|
+
'routes',
|
|
15
|
+
'classes',
|
|
16
|
+
'urls',
|
|
17
|
+
'service',
|
|
18
|
+
'services',
|
|
19
|
+
# 'decorators',
|
|
20
|
+
# 'permissions',
|
|
21
|
+
# 'tasks',
|
|
22
|
+
# 'middleware',
|
|
23
|
+
# 'forms',
|
|
24
|
+
# 'components',
|
|
25
|
+
# 'pages',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Command(BaseCommand):
|
|
30
|
+
"""
|
|
31
|
+
@behavior:
|
|
32
|
+
Handles copying the project structure and
|
|
33
|
+
contents of specified files and directories.
|
|
34
|
+
@usage:
|
|
35
|
+
manage.py copy_proj [target_names] --apps [apps] [-b/--backend] [-f/--frontend]
|
|
36
|
+
@flags:
|
|
37
|
+
-b, --backend: Include backend files.
|
|
38
|
+
-f, --frontend: Include frontend files.
|
|
39
|
+
@args:
|
|
40
|
+
target_names: list of target directory/file names to collect. Defaults to all if not provided.
|
|
41
|
+
backend: flag to include backend files.
|
|
42
|
+
frontend: flag to include frontend files.
|
|
43
|
+
apps: list of app names to collect from. Defaults to all if not provided.
|
|
44
|
+
@raise CommandError:
|
|
45
|
+
If a specified app doesn't exist in backend/frontend.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
help = 'Copy project structure and contents of specific files and directories.'
|
|
49
|
+
|
|
50
|
+
IGNORE_FILES = ['__init__.py']
|
|
51
|
+
|
|
52
|
+
def add_arguments(self, parser):
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
'target_names', nargs='*', type=str,
|
|
55
|
+
help='List of target names to collect. Defaults to all if not provided.'
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
'--apps', nargs='+', type=str,
|
|
59
|
+
help='List of apps to collect from. Defaults to all if not provided.'
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
'-b', '--backend', action='store_true',
|
|
63
|
+
help='Include backend files.'
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
'-f', '--frontend', action='store_true',
|
|
67
|
+
help='Include frontend files.'
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def handle(self, *args, **options):
|
|
71
|
+
target_names = options['target_names']
|
|
72
|
+
include_backend = options['backend']
|
|
73
|
+
include_frontend = options['frontend']
|
|
74
|
+
|
|
75
|
+
# Если флаги не указаны, по умолчанию копируем только backend
|
|
76
|
+
if not include_backend and not include_frontend:
|
|
77
|
+
include_backend = True
|
|
78
|
+
|
|
79
|
+
if not target_names or target_names[0] == 'all':
|
|
80
|
+
target_names = TARGET_ALL
|
|
81
|
+
|
|
82
|
+
apps_to_include = options['apps'] or self.get_all_apps()
|
|
83
|
+
|
|
84
|
+
result = []
|
|
85
|
+
collected_files = {name: [] for name in target_names}
|
|
86
|
+
|
|
87
|
+
if include_backend:
|
|
88
|
+
for app in apps_to_include:
|
|
89
|
+
app_path = os.path.join(settings.ADJANGO_APPS_PATH, app)
|
|
90
|
+
if not os.path.exists(app_path):
|
|
91
|
+
self.stdout.write(self.style.ERROR(f"App {app} does not exist in backend. Skipping."))
|
|
92
|
+
continue
|
|
93
|
+
for root, dirs, files in os.walk(app_path):
|
|
94
|
+
for name in target_names:
|
|
95
|
+
if name in dirs:
|
|
96
|
+
dir_path = os.path.join(root, name)
|
|
97
|
+
self.collect_directory_contents(dir_path, collected_files[name], settings.BASE_DIR)
|
|
98
|
+
if name + '.py' in files:
|
|
99
|
+
file_path = os.path.join(root, name + '.py')
|
|
100
|
+
self.collect_file_contents(file_path, collected_files[name], settings.BASE_DIR)
|
|
101
|
+
|
|
102
|
+
if include_frontend:
|
|
103
|
+
for app in apps_to_include:
|
|
104
|
+
app_path = os.path.join(settings.ADJANGO_FRONTEND_DIR, app)
|
|
105
|
+
if not os.path.exists(app_path):
|
|
106
|
+
self.stdout.write(self.style.ERROR(f"App {app} does not exist in frontend. Skipping."))
|
|
107
|
+
continue
|
|
108
|
+
for root, dirs, files in os.walk(app_path):
|
|
109
|
+
for name in target_names:
|
|
110
|
+
if name in dirs:
|
|
111
|
+
dir_path = os.path.join(root, name)
|
|
112
|
+
self.collect_directory_contents(dir_path, collected_files[name], settings.FRONTEND_DIR)
|
|
113
|
+
# Проверяем файлы с расширениями фронтенда
|
|
114
|
+
for ext in ['.ts', '.tsx', '.js', '.jsx']:
|
|
115
|
+
file_name = name + ext
|
|
116
|
+
if file_name in files:
|
|
117
|
+
file_path = os.path.join(root, file_name)
|
|
118
|
+
self.collect_file_contents(file_path, collected_files[name], settings.FRONTEND_DIR)
|
|
119
|
+
|
|
120
|
+
for name in target_names:
|
|
121
|
+
if collected_files[name]:
|
|
122
|
+
result.append(f'\n# {name.capitalize()}\n')
|
|
123
|
+
result.extend(collected_files[name])
|
|
124
|
+
|
|
125
|
+
final_text = '\n'.join(result)
|
|
126
|
+
pyperclip.copy(final_text)
|
|
127
|
+
self.stdout.write(self.style.SUCCESS('Project structure and contents copied to clipboard.'))
|
|
128
|
+
print(final_text)
|
|
129
|
+
|
|
130
|
+
def collect_directory_contents(self, dir_path, result, base_dir):
|
|
131
|
+
for sub_root, sub_dirs, sub_files in os.walk(dir_path):
|
|
132
|
+
for file in sub_files:
|
|
133
|
+
if (file.endswith('.py') or file.endswith('.ts') or file.endswith('.tsx') or file.endswith(
|
|
134
|
+
'.js') or file.endswith('.jsx')) and file not in self.IGNORE_FILES:
|
|
135
|
+
file_path = os.path.join(sub_root, file)
|
|
136
|
+
self.collect_file_contents(file_path, result, base_dir)
|
|
137
|
+
|
|
138
|
+
def collect_file_contents(self, file_path, result, base_dir):
|
|
139
|
+
relative_path = os.path.relpath(file_path, base_dir)
|
|
140
|
+
result.append(f'\n# {relative_path}\n')
|
|
141
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
142
|
+
result.append(f.read())
|
|
143
|
+
|
|
144
|
+
def get_all_apps(self):
|
|
145
|
+
apps_path = os.path.join(settings.ADJANGO_BACKENDS_APPS)
|
|
146
|
+
return [name for name in os.listdir(apps_path) if os.path.isdir(os.path.join(apps_path, name))]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from django.apps import apps
|
|
4
|
+
from django.core.management import BaseCommand, call_command
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Command(BaseCommand):
|
|
8
|
+
"""
|
|
9
|
+
Dumps data from all models into separate JSON files within a specified directory.
|
|
10
|
+
|
|
11
|
+
@usage: python manage.py <command> <directory>
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
help = 'Dumps data from all models into separate files within a specified directory'
|
|
15
|
+
|
|
16
|
+
def add_arguments(self, parser) -> None:
|
|
17
|
+
parser.add_argument('directory', type=str, help='Directory where data files will be saved')
|
|
18
|
+
|
|
19
|
+
def handle(self, *args: tuple, **options: dict) -> None:
|
|
20
|
+
directory: str = options['directory']
|
|
21
|
+
if not os.path.exists(directory):
|
|
22
|
+
os.makedirs(directory)
|
|
23
|
+
|
|
24
|
+
for app in apps.get_app_configs():
|
|
25
|
+
for model in app.get_models():
|
|
26
|
+
model_label = f"{app.label}_{model._meta.model_name}"
|
|
27
|
+
output_file_path = os.path.join(directory, f"{model_label}.json")
|
|
28
|
+
self.stdout.write(f"Dumping data for {model_label} into {output_file_path}")
|
|
29
|
+
try:
|
|
30
|
+
with open(output_file_path, 'w', encoding='utf-8') as output_file:
|
|
31
|
+
call_command('dumpdata', f'{app.label}.{model._meta.model_name}', stdout=output_file)
|
|
32
|
+
except Exception:
|
|
33
|
+
print(f'{model._meta.model_name} Not Loaded')
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from django.core.management import call_command
|
|
4
|
+
from django.core.management.base import BaseCommand
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Command(BaseCommand):
|
|
8
|
+
"""
|
|
9
|
+
Loads data from all JSON files within a specified directory into the corresponding models.
|
|
10
|
+
|
|
11
|
+
@usage: python manage.py loaddata_from_dir <directory>
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
help = 'Loads data from all files within a specified directory into the corresponding models'
|
|
15
|
+
|
|
16
|
+
def add_arguments(self, parser) -> None:
|
|
17
|
+
parser.add_argument('directory', type=str, help='Directory from which data files will be loaded')
|
|
18
|
+
|
|
19
|
+
def handle(self, *args: tuple, **options: dict) -> None:
|
|
20
|
+
directory: str = options['directory']
|
|
21
|
+
if not os.path.exists(directory):
|
|
22
|
+
self.stdout.write(self.style.ERROR(f'Directory {directory} does not exist'))
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
total_files: int = 0
|
|
26
|
+
loaded_files: int = 0
|
|
27
|
+
failed_files: list = []
|
|
28
|
+
successful_files: list = []
|
|
29
|
+
|
|
30
|
+
for filename in os.listdir(directory):
|
|
31
|
+
if filename.endswith('.json'):
|
|
32
|
+
total_files += 1
|
|
33
|
+
self.stdout.write(f'Loading data from {filename}')
|
|
34
|
+
try:
|
|
35
|
+
call_command('loaddata', os.path.join(directory, filename))
|
|
36
|
+
loaded_files += 1
|
|
37
|
+
successful_files.append(filename)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
self.stdout.write(self.style.ERROR(f'{filename} Not Loaded'))
|
|
40
|
+
self.stdout.write(self.style.ERROR(str(e)))
|
|
41
|
+
failed_files.append(filename)
|
|
42
|
+
|
|
43
|
+
self.stdout.write(self.style.SUCCESS(f'Successfully loaded {loaded_files}/{total_files} files.'))
|
|
44
|
+
|
|
45
|
+
if successful_files:
|
|
46
|
+
self.stdout.write(self.style.SUCCESS('Successfully loaded the following files:'))
|
|
47
|
+
for successful_file in successful_files:
|
|
48
|
+
self.stdout.write(self.style.SUCCESS(f'- {successful_file}'))
|
|
49
|
+
|
|
50
|
+
if failed_files:
|
|
51
|
+
self.stdout.write(self.style.ERROR('Failed to load the following files:'))
|
|
52
|
+
for failed_file in failed_files:
|
|
53
|
+
self.stdout.write(self.style.ERROR(f'- {failed_file}'))
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# core/management/commands/remakemigrations.py
|
|
2
|
+
import glob
|
|
3
|
+
import os
|
|
4
|
+
from time import sleep
|
|
5
|
+
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django.core.management import BaseCommand, call_command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Command(BaseCommand):
|
|
11
|
+
help = ('Delete all migration files from apps in the "apps." namespace, '
|
|
12
|
+
'delete SQLite database file if used, and run makemigrations and migrate')
|
|
13
|
+
|
|
14
|
+
def handle(self, *args, **kwargs):
|
|
15
|
+
apps_prepath = settings.ADJANGO_APPS_PREPATH
|
|
16
|
+
base_dir = settings.BASE_DIR
|
|
17
|
+
|
|
18
|
+
# Delete migration files
|
|
19
|
+
for app in settings.INSTALLED_APPS:
|
|
20
|
+
if apps_prepath is None or app.startswith(apps_prepath):
|
|
21
|
+
app_path = os.path.join(base_dir, app.replace('.', '/'))
|
|
22
|
+
migrations_path = os.path.join(app_path, 'migrations')
|
|
23
|
+
if os.path.exists(migrations_path):
|
|
24
|
+
files = glob.glob(os.path.join(migrations_path, '*.py'))
|
|
25
|
+
for file in files:
|
|
26
|
+
if os.path.basename(file) != '__init__.py':
|
|
27
|
+
os.remove(file)
|
|
28
|
+
self.stdout.write(f'Deleted {file}')
|
|
29
|
+
|
|
30
|
+
pyc_files = glob.glob(os.path.join(migrations_path, '*.pyc'))
|
|
31
|
+
for file in pyc_files:
|
|
32
|
+
os.remove(file)
|
|
33
|
+
self.stdout.write(f'Deleted {file}')
|
|
34
|
+
|
|
35
|
+
self.stdout.write('All migration files in apps.* deleted')
|
|
36
|
+
sleep(1)
|
|
37
|
+
|
|
38
|
+
# Run makemigrations
|
|
39
|
+
self.stdout.write('Running makemigrations...')
|
|
40
|
+
call_command('makemigrations')
|
|
41
|
+
self.stdout.write('Makemigrations completed')
|
|
42
|
+
sleep(1)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class IPAddressMiddleware:
|
|
2
|
+
"""
|
|
3
|
+
Позволяет легко получать IP-адрес через `request.ip`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def __init__(self, get_response):
|
|
7
|
+
self.get_response = get_response
|
|
8
|
+
|
|
9
|
+
def __call__(self, request):
|
|
10
|
+
if request.META.get('HTTP_X_FORWARDED_FOR'):
|
|
11
|
+
request.ip = request.META.get('HTTP_X_FORWARDED_FOR')
|
|
12
|
+
elif request.META.get("HTTP_X_REAL_IP"):
|
|
13
|
+
request.ip = request.META.get("HTTP_X_REAL_IP")
|
|
14
|
+
elif request.META.get("REMOTE_ADDR"):
|
|
15
|
+
request.ip = request.META.get("REMOTE_ADDR")
|
|
16
|
+
else:
|
|
17
|
+
request.ip = None
|
|
18
|
+
return self.get_response(request)
|
|
File without changes
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from pprint import pprint
|
|
7
|
+
from time import time
|
|
8
|
+
|
|
9
|
+
import aiohttp
|
|
10
|
+
from asgiref.sync import sync_to_async
|
|
11
|
+
from django.conf import settings
|
|
12
|
+
from django.contrib.auth.models import Group
|
|
13
|
+
from django.core.files.base import ContentFile
|
|
14
|
+
from django.core.handlers.asgi import ASGIRequest
|
|
15
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
16
|
+
from django.db import transaction
|
|
17
|
+
from django.http import HttpResponseNotAllowed, HttpResponse, QueryDict, RawPostDataException
|
|
18
|
+
from django.shortcuts import redirect
|
|
19
|
+
|
|
20
|
+
from adjango.utils.common import traceback_str
|
|
21
|
+
from adjango.utils.mail import send_emails
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def download_file_to_temp(url: str) -> ContentFile:
|
|
25
|
+
"""
|
|
26
|
+
Скачивает файл с URL и сохраняет его в объект ContentFile, не сохраняя на диск.
|
|
27
|
+
|
|
28
|
+
@param url: URL файла, который нужно скачать.
|
|
29
|
+
@return: Временный файл в памяти.
|
|
30
|
+
"""
|
|
31
|
+
async with aiohttp.ClientSession() as session:
|
|
32
|
+
async with session.get(url) as response:
|
|
33
|
+
if response.status == 200:
|
|
34
|
+
file_content = await response.read()
|
|
35
|
+
file_name = url.split('/')[-1]
|
|
36
|
+
return ContentFile(file_content, name=file_name)
|
|
37
|
+
raise ValueError(f"Failed to download image from {url}, status code: {response.status}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def add_user_to_group(user, group_name):
|
|
41
|
+
group, created = Group.objects.get_or_create(name=group_name)
|
|
42
|
+
if user not in group.user_set.all():
|
|
43
|
+
group.user_set.add(user)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def allowed_only(allowed_methods):
|
|
47
|
+
def decorator(view_func):
|
|
48
|
+
def wrapped_view(request, *args, **kwargs):
|
|
49
|
+
if request.method in allowed_methods:
|
|
50
|
+
return view_func(request, *args, **kwargs)
|
|
51
|
+
else:
|
|
52
|
+
return HttpResponseNotAllowed(allowed_methods)
|
|
53
|
+
|
|
54
|
+
return wrapped_view
|
|
55
|
+
|
|
56
|
+
return decorator
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def aallowed_only(allowed_methods) -> callable:
|
|
60
|
+
def decorator(view_func) -> callable:
|
|
61
|
+
async def wrapped_view(request, *args, **kwargs) -> HttpResponse:
|
|
62
|
+
if request.method in allowed_methods:
|
|
63
|
+
if asyncio.iscoroutinefunction(view_func):
|
|
64
|
+
return await view_func(request, *args, **kwargs)
|
|
65
|
+
else:
|
|
66
|
+
return view_func(request, *args, **kwargs)
|
|
67
|
+
else:
|
|
68
|
+
return HttpResponseNotAllowed(allowed_methods)
|
|
69
|
+
|
|
70
|
+
return wrapped_view
|
|
71
|
+
|
|
72
|
+
return decorator
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def apprint(*args, **kwargs):
|
|
76
|
+
await sync_to_async(pprint)(*args, **kwargs)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def force_data(fn):
|
|
80
|
+
@functools.wraps(fn)
|
|
81
|
+
def _wrapped_view(request, *args, **kwargs):
|
|
82
|
+
data = {}
|
|
83
|
+
|
|
84
|
+
if isinstance(request.POST, QueryDict): # Объединяем данные из request.POST
|
|
85
|
+
data.update(request.POST.dict())
|
|
86
|
+
else:
|
|
87
|
+
data.update(request.POST)
|
|
88
|
+
|
|
89
|
+
if isinstance(request.GET, QueryDict): # Объединяем данные из request.GET
|
|
90
|
+
data.update(request.GET.dict())
|
|
91
|
+
else:
|
|
92
|
+
data.update(request.GET)
|
|
93
|
+
|
|
94
|
+
try: # Обрабатываем JSON-тело запроса, если оно есть
|
|
95
|
+
json_data = json.loads(request.body)
|
|
96
|
+
if isinstance(json_data, dict):
|
|
97
|
+
data.update(json_data)
|
|
98
|
+
except (ValueError, TypeError, RawPostDataException):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
setattr(request, 'get_data', lambda: data)
|
|
102
|
+
return fn(request, *args, **kwargs)
|
|
103
|
+
|
|
104
|
+
return _wrapped_view
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def aforce_data(fn):
|
|
108
|
+
@functools.wraps(fn)
|
|
109
|
+
async def _wrapped_view(request, *args, **kwargs):
|
|
110
|
+
if isinstance(request.POST, QueryDict): # Объединяем данные из request.POST
|
|
111
|
+
request.data.update(request.POST.dict())
|
|
112
|
+
else:
|
|
113
|
+
request.data.update(request.POST)
|
|
114
|
+
|
|
115
|
+
if isinstance(request.GET, QueryDict): # Объединяем данные из request.GET
|
|
116
|
+
request.data.update(request.GET.dict())
|
|
117
|
+
else:
|
|
118
|
+
request.data.update(request.GET)
|
|
119
|
+
|
|
120
|
+
try: # Обрабатываем JSON-тело запроса, если оно есть
|
|
121
|
+
json_data = json.loads(request.body.decode('utf-8'))
|
|
122
|
+
if isinstance(json_data, dict):
|
|
123
|
+
request.data.update(json_data)
|
|
124
|
+
except (ValueError, TypeError, UnicodeDecodeError, RawPostDataException):
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
return await fn(request, *args, **kwargs)
|
|
128
|
+
|
|
129
|
+
return _wrapped_view
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def acontroller(name=None, logger=settings.ADJANGO_LOGGER_NAME, log_name=True, log_time=False) -> callable:
|
|
133
|
+
def decorator(fn) -> callable:
|
|
134
|
+
@functools.wraps(fn)
|
|
135
|
+
async def inner(request: ASGIRequest, *args, **kwargs):
|
|
136
|
+
log = logging.getLogger(logger)
|
|
137
|
+
fn_name = name or fn.__name__
|
|
138
|
+
if log_name: log.info(f'ACtrl: {request.method} | {fn_name}')
|
|
139
|
+
if log_time:
|
|
140
|
+
start_time = time()
|
|
141
|
+
|
|
142
|
+
if settings.DEBUG:
|
|
143
|
+
return await fn(request, *args, **kwargs)
|
|
144
|
+
else:
|
|
145
|
+
try:
|
|
146
|
+
if log_time:
|
|
147
|
+
end_time = time()
|
|
148
|
+
elapsed_time = end_time - start_time
|
|
149
|
+
log.info(f"Execution time of {fn_name}: {elapsed_time:.2f} seconds")
|
|
150
|
+
return await fn(request, *args, **kwargs)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
log.critical(f"ERROR in {fn_name}: {traceback_str(e)}", exc_info=True)
|
|
153
|
+
send_emails(
|
|
154
|
+
subject='SERVER ERROR',
|
|
155
|
+
emails=settings.ADJANGO_EXCEPTION_REPORT_EMAILS,
|
|
156
|
+
template=settings.ADJANGO_EXCEPTION_REPORT_TEMPLATE,
|
|
157
|
+
context={'traceback': traceback_str(e), }
|
|
158
|
+
)
|
|
159
|
+
raise e
|
|
160
|
+
|
|
161
|
+
return inner
|
|
162
|
+
|
|
163
|
+
return decorator
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def controller(name=None, logger=settings.ADJANGO_LOGGER_NAME, log_name=True, log_time=False,
|
|
167
|
+
auth_required=False, not_auth_redirect=settings.LOGIN_URL) -> callable:
|
|
168
|
+
def decorator(fn) -> callable:
|
|
169
|
+
@functools.wraps(fn)
|
|
170
|
+
def inner(request: WSGIRequest, *args, **kwargs):
|
|
171
|
+
log = logging.getLogger(logger)
|
|
172
|
+
fn_name = name or fn.__name__
|
|
173
|
+
if log_name: log.info(f'Ctrl: {request.method} | {fn_name}')
|
|
174
|
+
if log_time: start_time = time()
|
|
175
|
+
if auth_required:
|
|
176
|
+
if not request.user.is_authenticated: return redirect(not_auth_redirect)
|
|
177
|
+
if settings.DEBUG:
|
|
178
|
+
with transaction.atomic():
|
|
179
|
+
return fn(request, *args, **kwargs)
|
|
180
|
+
else:
|
|
181
|
+
try:
|
|
182
|
+
if log_time:
|
|
183
|
+
end_time = time()
|
|
184
|
+
elapsed_time = end_time - start_time
|
|
185
|
+
log.info(f"Execution time of {fn_name}: {elapsed_time:.2f} seconds")
|
|
186
|
+
with transaction.atomic():
|
|
187
|
+
return fn(request, *args, **kwargs)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
log.critical(f"ERROR in {fn_name}: {traceback_str(e)}", exc_info=True)
|
|
190
|
+
send_emails(
|
|
191
|
+
subject='SERVER ERROR',
|
|
192
|
+
emails=settings.ADJANGO_EXCEPTION_REPORT_EMAILS,
|
|
193
|
+
template=settings.ADJANGO_EXCEPTION_REPORT_TEMPLATE,
|
|
194
|
+
context={'traceback': traceback_str(e), }
|
|
195
|
+
)
|
|
196
|
+
raise e
|
|
197
|
+
|
|
198
|
+
return inner
|
|
199
|
+
|
|
200
|
+
return decorator
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import Any, Type
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
from asgiref.sync import sync_to_async
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
from django.contrib.auth import REDIRECT_FIELD_NAME
|
|
10
|
+
from django.core.files.base import ContentFile
|
|
11
|
+
from django.db.models import QuerySet, Model, Manager
|
|
12
|
+
from django.db.transaction import Atomic
|
|
13
|
+
from django.shortcuts import resolve_url, redirect
|
|
14
|
+
|
|
15
|
+
from adjango.utils import download_file_to_temp
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AsyncAtomicContextManager(Atomic):
|
|
19
|
+
def __init__(self, using=None, savepoint=True, durable=False):
|
|
20
|
+
super().__init__(using, savepoint, durable)
|
|
21
|
+
|
|
22
|
+
async def __aenter__(self):
|
|
23
|
+
await sync_to_async(super().__enter__)()
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
async def __aexit__(self, exc_type, exc_value, traceback):
|
|
27
|
+
await sync_to_async(super().__exit__)(exc_type, exc_value, traceback)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def aatomic(view_func):
|
|
31
|
+
@wraps(view_func)
|
|
32
|
+
async def _wrapped_view(request, *args, **kwargs):
|
|
33
|
+
async with AsyncAtomicContextManager():
|
|
34
|
+
return await view_func(request, *args, **kwargs)
|
|
35
|
+
|
|
36
|
+
return _wrapped_view
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def aget(
|
|
40
|
+
queryset: QuerySet,
|
|
41
|
+
exception: Type[Exception] | None = None,
|
|
42
|
+
*args: Any,
|
|
43
|
+
**kwargs: Any,
|
|
44
|
+
|
|
45
|
+
) -> Model | None:
|
|
46
|
+
"""
|
|
47
|
+
Асинхронно получает единственный объект из заданного QuerySet, соответствующий переданным параметрам.
|
|
48
|
+
|
|
49
|
+
@param queryset: QuerySet, из которого нужно получить объект.
|
|
50
|
+
@param exception: Класс исключения, которое будет выброшено, если объект не найден. Если None, возвращается None.
|
|
51
|
+
|
|
52
|
+
@return: Объект модели или None, если объект не найден и exception не задан.
|
|
53
|
+
|
|
54
|
+
@raises exception: Если объект не найден и передан класс исключения.
|
|
55
|
+
|
|
56
|
+
@behavior:
|
|
57
|
+
- Пытается асинхронно получить объект с помощью queryset.aget().
|
|
58
|
+
- Если объект не найден, обрабатывает исключение DoesNotExist.
|
|
59
|
+
- Если exception is not None, выбрасывает указанное исключение.
|
|
60
|
+
- Если exception is None, возвращает None.
|
|
61
|
+
|
|
62
|
+
@usage:
|
|
63
|
+
result = await aget(MyModel.objects, MyCustomException, id=1)
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
return await queryset.aget(*args, **kwargs)
|
|
67
|
+
except queryset.model.DoesNotExist:
|
|
68
|
+
if exception is not None:
|
|
69
|
+
raise exception()
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def arelated(model_object, related_field_name: str) -> object or None:
|
|
74
|
+
return await sync_to_async(getattr)(model_object, related_field_name, None)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def aadd(queryset, data, *args, **kwargs):
|
|
78
|
+
return await sync_to_async(queryset.add)(data, *args, **kwargs)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def aall(objects: Manager) -> list:
|
|
82
|
+
return await sync_to_async(list)(objects.all())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def afilter(queryset, *args, **kwargs) -> list:
|
|
86
|
+
"""
|
|
87
|
+
This function is used to filter objects...
|
|
88
|
+
:param queryset:
|
|
89
|
+
:param args:
|
|
90
|
+
:param kwargs:
|
|
91
|
+
:return: List of ...
|
|
92
|
+
"""
|
|
93
|
+
return await sync_to_async(list)(queryset.filter(*args, **kwargs))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def auser_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
|
|
97
|
+
"""
|
|
98
|
+
Decorator for views that checks that the user passes the given test,
|
|
99
|
+
redirecting to the log-in page if necessary. The test should be a callable
|
|
100
|
+
that takes the user object and returns True if the user passes.
|
|
101
|
+
"""
|
|
102
|
+
if not login_url:
|
|
103
|
+
login_url = settings.LOGIN_URL
|
|
104
|
+
|
|
105
|
+
def decorator(view_func):
|
|
106
|
+
@wraps(view_func)
|
|
107
|
+
async def _wrapped_view(request, *args, **kwargs):
|
|
108
|
+
if await test_func(request.user):
|
|
109
|
+
return await view_func(request, *args, **kwargs)
|
|
110
|
+
path = request.build_absolute_uri()
|
|
111
|
+
resolved_login_url = resolve_url(login_url)
|
|
112
|
+
# If the login_url is the same scheme and net location then just
|
|
113
|
+
# use the path as the "next" url.
|
|
114
|
+
login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
|
|
115
|
+
current_scheme, current_netloc = urlparse(path)[:2]
|
|
116
|
+
if ((not login_scheme or login_scheme == current_scheme) and
|
|
117
|
+
(not login_netloc or login_netloc == current_netloc)):
|
|
118
|
+
path = request.get_full_path()
|
|
119
|
+
return redirect(login_url)
|
|
120
|
+
|
|
121
|
+
return _wrapped_view
|
|
122
|
+
|
|
123
|
+
return decorator
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def alogin_required(
|
|
127
|
+
function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None
|
|
128
|
+
):
|
|
129
|
+
"""
|
|
130
|
+
Decorator for views that checks that the user is logged in, redirecting
|
|
131
|
+
to the log-in page if necessary.
|
|
132
|
+
"""
|
|
133
|
+
actual_decorator = auser_passes_test(
|
|
134
|
+
sync_to_async(lambda u: u.is_authenticated),
|
|
135
|
+
login_url=login_url,
|
|
136
|
+
redirect_field_name=redirect_field_name,
|
|
137
|
+
)
|
|
138
|
+
if function:
|
|
139
|
+
return actual_decorator(function)
|
|
140
|
+
return actual_decorator
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def set_image_by_url(model_obj: Model, field_name: str, image_url: str) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Загружает изображение с заданного URL и устанавливает его в указанное поле модели без
|
|
146
|
+
предварительного сохранения файла на диск.
|
|
147
|
+
|
|
148
|
+
@param model_obj: Экземпляр модели, в который нужно установить изображение.
|
|
149
|
+
@param field_name: Название поля, в которое нужно сохранить изображение.
|
|
150
|
+
@param image_url: URL изображения, которое нужно загрузить.
|
|
151
|
+
@return: None
|
|
152
|
+
"""
|
|
153
|
+
# Скачиваем изображение как объект ContentFile
|
|
154
|
+
image_file: ContentFile = await download_file_to_temp(image_url)
|
|
155
|
+
|
|
156
|
+
# Используем setattr, чтобы установить файл в поле модели
|
|
157
|
+
await sync_to_async(getattr(model_obj, field_name).save)(image_file.name, image_file)
|
|
158
|
+
await model_obj.asave()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.core.mail import send_mail
|
|
6
|
+
from django.template.loader import render_to_string
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger(settings.ADJANGO_EMAIL_LOGGER_NAME)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def send_emails(subject: str, emails: tuple, template: str, context=None):
|
|
12
|
+
"""
|
|
13
|
+
Отправляет email с использованием указанного шаблона.
|
|
14
|
+
|
|
15
|
+
@param subject: Тема письма.
|
|
16
|
+
@param emails: Список email-адресов получателей.
|
|
17
|
+
@param template: Путь к шаблону письма.
|
|
18
|
+
@param context: Контекст для рендеринга шаблона.
|
|
19
|
+
"""
|
|
20
|
+
if send_mail(
|
|
21
|
+
subject=subject, message=str(json.dumps(context)),
|
|
22
|
+
from_email=settings.EMAIL_HOST_USER,
|
|
23
|
+
recipient_list=list(emails),
|
|
24
|
+
html_message=render_to_string(template, context=context if context is not None else {})
|
|
25
|
+
):
|
|
26
|
+
log.info(f'Successfully sent template={template} emails {", ".join(emails)}')
|
|
27
|
+
else:
|
|
28
|
+
log.critical(
|
|
29
|
+
f'Failed to send template={template} emails {", ".join(emails)} context={str(json.dumps(context))}'
|
|
30
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from django.core.cache import cache as djc
|
|
2
|
+
|
|
3
|
+
MIN_1 = 1 * 60
|
|
4
|
+
MIN_5 = 5 * 60
|
|
5
|
+
MIN_10 = 10 * 60
|
|
6
|
+
MIN_15 = 15 * 60
|
|
7
|
+
MIN_20 = 20 * 60
|
|
8
|
+
MIN_25 = 25 * 60
|
|
9
|
+
MIN_30 = 30 * 60
|
|
10
|
+
MIN_40 = 40 * 60
|
|
11
|
+
MIN_50 = 50 * 60
|
|
12
|
+
HOUR_1 = 1 * 60 * 60
|
|
13
|
+
HOUR_2 = 2 * 60 * 60
|
|
14
|
+
HOUR_3 = 3 * 60 * 60
|
|
15
|
+
HOUR_4 = 4 * 60 * 60
|
|
16
|
+
HOUR_5 = 5 * 60 * 60
|
|
17
|
+
HOUR_6 = 6 * 60 * 60
|
|
18
|
+
HOUR_7 = 7 * 60 * 60
|
|
19
|
+
HOUR_8 = 8 * 60 * 60
|
|
20
|
+
HOUR_9 = 9 * 60 * 60
|
|
21
|
+
HOUR_10 = 10 * 60 * 60
|
|
22
|
+
HOUR_11 = 11 * 60 * 60
|
|
23
|
+
HOUR_12 = 12 * 60 * 60
|
|
24
|
+
HOUR_13 = 13 * 60 * 60
|
|
25
|
+
HOUR_14 = 14 * 60 * 60
|
|
26
|
+
HOUR_15 = 15 * 60 * 60
|
|
27
|
+
HOUR_16 = 16 * 60 * 60
|
|
28
|
+
HOUR_17 = 17 * 60 * 60
|
|
29
|
+
HOUR_18 = 18 * 60 * 60
|
|
30
|
+
HOUR_19 = 19 * 60 * 60
|
|
31
|
+
HOUR_20 = 20 * 60 * 60
|
|
32
|
+
HOUR_21 = 21 * 60 * 60
|
|
33
|
+
HOUR_22 = 22 * 60 * 60
|
|
34
|
+
HOUR_23 = 23 * 60 * 60
|
|
35
|
+
DAY_1 = 1 * 60 * 60 * 24
|
|
36
|
+
DAYS_2 = 2 * 60 * 60 * 24
|
|
37
|
+
DAYS_3 = 3 * 60 * 60 * 24
|
|
38
|
+
DAYS_4 = 4 * 60 * 60 * 24
|
|
39
|
+
DAYS_5 = 5 * 60 * 60 * 24
|
|
40
|
+
DAYS_6 = 6 * 60 * 60 * 24
|
|
41
|
+
DAYS_7 = 7 * 60 * 60 * 24
|
|
42
|
+
DAYS_8 = 8 * 60 * 60 * 24
|
|
43
|
+
DAYS_9 = 9 * 60 * 60 * 24
|
|
44
|
+
DAYS_10 = 10 * 60 * 60 * 24
|
|
45
|
+
DAYS_11 = 11 * 60 * 60 * 24
|
|
46
|
+
DAYS_12 = 12 * 60 * 60 * 24
|
|
47
|
+
DAYS_13 = 13 * 60 * 60 * 24
|
|
48
|
+
DAYS_14 = 14 * 60 * 60 * 24
|
|
49
|
+
DAYS_15 = 15 * 60 * 60 * 24
|
|
50
|
+
DAYS_16 = 16 * 60 * 60 * 24
|
|
51
|
+
DAYS_17 = 17 * 60 * 60 * 24
|
|
52
|
+
DAYS_18 = 18 * 60 * 60 * 24
|
|
53
|
+
DAYS_19 = 19 * 60 * 60 * 24
|
|
54
|
+
DAYS_20 = 20 * 60 * 60 * 24
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CacheNotFound(Exception): pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Rediser:
|
|
61
|
+
"""Рофлан класыч для удобного использования кэширования в Django."""
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def cache(name, obj=None, timeout=HOUR_12, *args, **kwargs):
|
|
65
|
+
"""
|
|
66
|
+
Кэширует результат функции или возвращает кэшированный результат, если он доступен.
|
|
67
|
+
|
|
68
|
+
@param name: Базовое имя кэш-записи.
|
|
69
|
+
@param obj: Объект или функция для кэширования.
|
|
70
|
+
@param timeout: Время в секундах до истечения срока действия.
|
|
71
|
+
@return: Кэшированный результат или результат вызова функции.
|
|
72
|
+
"""
|
|
73
|
+
if djc.get(name) is not None: return False, djc.get(name)
|
|
74
|
+
if obj is None:
|
|
75
|
+
raise CacheNotFound(f'No cache found for {name}')
|
|
76
|
+
else:
|
|
77
|
+
result = obj(*args, **kwargs) if callable(obj) else obj
|
|
78
|
+
djc.set(name, result, timeout=timeout)
|
|
79
|
+
return True, result
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
async def acache(name, obj=None, timeout=HOUR_12, *args, **kwargs):
|
|
83
|
+
"""
|
|
84
|
+
Асинхронно кэширует результат функции или возвращает кэшированный результат, если он доступен.
|
|
85
|
+
|
|
86
|
+
@param name: Базовое имя кэш-записи.
|
|
87
|
+
@param obj: Объект или функция для кэширования.
|
|
88
|
+
@param timeout: Время в секундах до истечения срока действия.
|
|
89
|
+
@return: Кэшированный результат или результат вызова функции.
|
|
90
|
+
"""
|
|
91
|
+
if djc.get(name) is not None: return False, djc.get(name)
|
|
92
|
+
if obj is None:
|
|
93
|
+
raise CacheNotFound(f'No cache found for {name}')
|
|
94
|
+
else:
|
|
95
|
+
result = await obj(*args, **kwargs) if callable(obj) else obj
|
|
96
|
+
djc.set(name, result, timeout=timeout)
|
|
97
|
+
return True, result
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def delete(name):
|
|
101
|
+
djc.delete(name)
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def delete_all():
|
|
105
|
+
djc.clear()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: adjango
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A library with many features for interacting with Django
|
|
5
|
+
Home-page: https://github.com/Artasov/adjango
|
|
6
|
+
Author: xlartas
|
|
7
|
+
Author-email: ivanhvalevskey@gmail.com
|
|
8
|
+
Project-URL: Source, https://github.com/Artasov/adjango
|
|
9
|
+
Project-URL: Tracker, https://github.com/Artasov/adjango/issues
|
|
10
|
+
Keywords: adjango django utils funcs features async
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Framework :: Django :: 4.0
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Intended Audience :: Developers
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: Django<5.3,>=4.0
|
|
26
|
+
Requires-Dist: pyperclip>=1.8.0
|
|
27
|
+
Requires-Dist: aiohttp>=3.8.0
|
|
28
|
+
|
|
29
|
+
# ADjango
|
|
30
|
+
|
|
31
|
+
**ADjango**
|
|
32
|
+
> Sometimes I use this in different projects, so I decided to put it on pypi
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
```bash
|
|
36
|
+
pip install adjango
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Settings
|
|
40
|
+
|
|
41
|
+
* ### Add the application to the project.
|
|
42
|
+
```python
|
|
43
|
+
INSTALLED_APPS = [
|
|
44
|
+
#...
|
|
45
|
+
'adjango',
|
|
46
|
+
]
|
|
47
|
+
```
|
|
48
|
+
* ### In `settings.py` set the params
|
|
49
|
+
```python
|
|
50
|
+
ADJANGO_BACKENDS_APPS = BASE_DIR / 'apps'
|
|
51
|
+
ADJANGO_FRONTEND_APPS = BASE_DIR.parent / 'frontend' / 'src' / 'apps'
|
|
52
|
+
ADJANGO_APPS_PREPATH = 'apps.'
|
|
53
|
+
ADJANGO_EXCEPTION_REPORT_EMAIL = ('ivanhvalevskey@gmail.com',)
|
|
54
|
+
ADJANGO_EXCEPTION_REPORT_TEMPLATE = 'core/error_report.html'
|
|
55
|
+
ADJANGO_LOGGER_NAME = 'global'
|
|
56
|
+
ADJANGO_EMAIL_LOGGER_NAME = 'email'
|
|
57
|
+
```
|
|
58
|
+
```python
|
|
59
|
+
MIDDLEWARE = [
|
|
60
|
+
...
|
|
61
|
+
# add request.ip in views
|
|
62
|
+
'adjango.middleware.IPAddressMiddleware',
|
|
63
|
+
...
|
|
64
|
+
]
|
|
65
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
setup.cfg
|
|
5
|
+
setup.py
|
|
6
|
+
adjango/__init__.py
|
|
7
|
+
adjango/apps.py
|
|
8
|
+
adjango/middleware.py
|
|
9
|
+
adjango.egg-info/PKG-INFO
|
|
10
|
+
adjango.egg-info/SOURCES.txt
|
|
11
|
+
adjango.egg-info/dependency_links.txt
|
|
12
|
+
adjango.egg-info/requires.txt
|
|
13
|
+
adjango.egg-info/top_level.txt
|
|
14
|
+
adjango/management/__init__.py
|
|
15
|
+
adjango/management/commands/__init__.py
|
|
16
|
+
adjango/management/commands/add_paths.py
|
|
17
|
+
adjango/management/commands/copy_proj.py
|
|
18
|
+
adjango/management/commands/dumpdata_to_dir.py
|
|
19
|
+
adjango/management/commands/loaddata_from_dir.py
|
|
20
|
+
adjango/management/commands/remakemigrations.py
|
|
21
|
+
adjango/utils/__init__.py
|
|
22
|
+
adjango/utils/base.py
|
|
23
|
+
adjango/utils/common.py
|
|
24
|
+
adjango/utils/funcs.py
|
|
25
|
+
adjango/utils/mail.py
|
|
26
|
+
adjango/utils/rediser.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
adjango
|
adjango-0.0.1/setup.cfg
ADDED
adjango-0.0.1/setup.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import setuptools
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
|
4
|
+
long_description = fh.read()
|
|
5
|
+
|
|
6
|
+
setuptools.setup(
|
|
7
|
+
name="adjango",
|
|
8
|
+
version="0.0.1",
|
|
9
|
+
author="xlartas",
|
|
10
|
+
author_email="ivanhvalevskey@gmail.com",
|
|
11
|
+
description="A library with many features for interacting with Django",
|
|
12
|
+
long_description=long_description,
|
|
13
|
+
long_description_content_type="text/markdown",
|
|
14
|
+
url="https://github.com/Artasov/adjango",
|
|
15
|
+
packages=setuptools.find_packages(),
|
|
16
|
+
include_package_data=True,
|
|
17
|
+
install_requires=[
|
|
18
|
+
"Django>=4.0,<5.3",
|
|
19
|
+
"pyperclip>=1.8.0",
|
|
20
|
+
"aiohttp>=3.8.0",
|
|
21
|
+
],
|
|
22
|
+
classifiers=[
|
|
23
|
+
"Framework :: Django",
|
|
24
|
+
"Framework :: Django :: 4.0",
|
|
25
|
+
"Programming Language :: Python :: 3.8",
|
|
26
|
+
"Programming Language :: Python :: 3.9",
|
|
27
|
+
"Programming Language :: Python :: 3.10",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Operating System :: OS Independent",
|
|
31
|
+
"License :: OSI Approved :: MIT License",
|
|
32
|
+
"Intended Audience :: Developers",
|
|
33
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
34
|
+
],
|
|
35
|
+
python_requires='>=3.8',
|
|
36
|
+
keywords='adjango django utils funcs features async',
|
|
37
|
+
project_urls={
|
|
38
|
+
'Source': 'https://github.com/Artasov/adjango',
|
|
39
|
+
'Tracker': 'https://github.com/Artasov/adjango/issues',
|
|
40
|
+
},
|
|
41
|
+
)
|