starco-dj-utils 1.0.2__py3-none-any.whl

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.
dj_utils/__init__.py ADDED
File without changes
dj_utils/admin.py ADDED
@@ -0,0 +1,3 @@
1
+ from django.contrib import admin
2
+
3
+ # Register your models here.
dj_utils/apps.py ADDED
@@ -0,0 +1,9 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DjUtilsConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'dj_utils'
7
+ def ready(self):
8
+ from . import handlers
9
+ from . import tasks_config
dj_utils/handlers.py ADDED
@@ -0,0 +1,83 @@
1
+ from asgiref.sync import async_to_sync, sync_to_async
2
+ import os
3
+ from django.dispatch import receiver
4
+ from .signals import notifire, send_sms, send_mail
5
+ import re
6
+ from telegram import Bot
7
+ import logging
8
+ from dotenv import load_dotenv
9
+ load_dotenv()
10
+
11
+ def get_bot():
12
+ TOKEN = os.getenv('UTILS_BOT_TOKEN')
13
+ if not TOKEN:
14
+ logging.error("Telegram bot token not found in environment variables.")
15
+ raise ValueError("Telegram bot token is required.\nUTILS_BOT_TOKEN must be set in environment variables.")
16
+ return Bot(token=TOKEN)
17
+
18
+
19
+ def clean_html_for_telegram(text: str) -> str:
20
+ """
21
+ حذف تگ‌های HTML نامجاز برای استفاده در Telegram با parse_mode='HTML'
22
+ و جایگزینی برخی از آن‌ها مثل <p> با newline
23
+ """
24
+ # تگ‌های مجاز HTML برای تلگرام
25
+ allowed_tags = ['b', 'strong', 'i', 'em', 'u', 's', 'strike', 'del',
26
+ 'span', 'a', 'code', 'pre']
27
+
28
+ # جایگزینی <p> و </p> با newline
29
+ text = re.sub(r'</?p\s*>', '\n', text)
30
+
31
+ # حذف تگ‌های غیرمجاز
32
+ def remove_invalid_tags(match):
33
+ tag = match.group(1)
34
+ if tag.lower() not in allowed_tags:
35
+ return ''
36
+ return match.group(0)
37
+
38
+ # حذف تگ‌های باز
39
+ text = re.sub(r'<(/?\s*?)(\w+)([^>]*)>',
40
+ lambda m: remove_invalid_tags((m.group(2),)) if m.group(2).lower() not in allowed_tags else m.group(
41
+ 0), text)
42
+
43
+ return text.strip()
44
+
45
+
46
+ @receiver(notifire)
47
+ def handle_notifire(sender, text, chat_id=None, label=True, **kwargs):
48
+ chat_id = chat_id if chat_id else os.getenv('UTILS_TELEGRAM_CHAT_ID')
49
+ if not chat_id:
50
+ logging.error("Telegram chat ID not found in environment variables.")
51
+ raise ValueError("Telegram chat ID is required.\nUTILS_TELEGRAM_CHAT_ID must be set in environment variables.")
52
+ file = kwargs.get('file')
53
+ parse_mode = kwargs.get('parse_mode', 'HTML')
54
+ disable_notification = kwargs.get('disable_notification', False)
55
+ protect_content = kwargs.get('protect_content')
56
+ reply_markup = kwargs.get('reply_markup')
57
+ reply_to_message_id = kwargs.get('reply_to_message_id')
58
+ disable_web_page_preview = kwargs.get('disable_web_page_preview')
59
+ data = {
60
+ 'chat_id': chat_id,
61
+ 'parse_mode': parse_mode,
62
+ 'disable_notification': disable_notification,
63
+ 'protect_content': protect_content,
64
+ 'reply_markup': reply_markup,
65
+ 'reply_to_message_id': reply_to_message_id,
66
+
67
+ }
68
+ if label:
69
+ text = f"#{os.getenv('PROJECT_NAME_EV')}:{sender}\n{text}"
70
+ if kwargs.get('parse_mode') == 'HTML':
71
+ text = clean_html_for_telegram(text)
72
+ try:
73
+ bot = get_bot()
74
+ if file:
75
+ res = async_to_sync(bot.send_document)(document=file, caption=text, **data)
76
+ print(res)
77
+
78
+ else:
79
+ data['disable_web_page_preview'] = disable_web_page_preview
80
+ res = async_to_sync(bot.send_message)(text=text, **data)
81
+ print(res)
82
+ except Exception as e:
83
+ print(e)
File without changes
File without changes
@@ -0,0 +1,156 @@
1
+ import os
2
+ import subprocess
3
+ import datetime
4
+ from django.core.management.base import BaseCommand, CommandError
5
+ from django.conf import settings
6
+
7
+
8
+ class Command(BaseCommand):
9
+ help = 'Backup database to a file'
10
+
11
+ def add_arguments(self, parser):
12
+ parser.add_argument('--path', type=str, help='Path to save the backup file (optional)')
13
+ parser.add_argument('--format', type=str, choices=['sql', 'dump', 'custom'], default='sql',
14
+ help='Backup format (sql, dump, or custom for PostgreSQL)')
15
+
16
+ def handle(self, *args, **options):
17
+ db_settings = settings.DATABASES['default']
18
+ db_type = db_settings['ENGINE'].split('.')[-1]
19
+
20
+ # Generate default backup path if not provided
21
+ backup_path = options.get('path')
22
+ if not backup_path:
23
+ timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
24
+ backup_dir = os.path.join(settings.BASE_DIR, 'backups')
25
+ os.makedirs(backup_dir, exist_ok=True)
26
+
27
+ if db_type == 'postgresql':
28
+ ext = '.sql' if options['format'] == 'sql' else '.dump'
29
+ backup_path = os.path.join(backup_dir, f'backup_pg_{timestamp}{ext}')
30
+ elif db_type == 'sqlite3':
31
+ backup_path = os.path.join(backup_dir, f'backup_sqlite_{timestamp}.sqlite3')
32
+ elif db_type == 'mysql':
33
+ backup_path = os.path.join(backup_dir, f'backup_mysql_{timestamp}.sql')
34
+ else:
35
+ raise CommandError(f'Unsupported database type: {db_type}')
36
+
37
+ try:
38
+ if db_type == 'postgresql':
39
+ self._backup_postgresql(db_settings, backup_path, options['format'])
40
+ elif db_type == 'sqlite3':
41
+ self._backup_sqlite3(db_settings, backup_path)
42
+ elif db_type == 'mysql':
43
+ self._backup_mysql(db_settings, backup_path)
44
+ else:
45
+ raise CommandError(f'Unsupported database type: {db_type}')
46
+
47
+ self.stdout.write(self.style.SUCCESS(f'Database backup created successfully at: {backup_path}'))
48
+ except Exception as e:
49
+ raise CommandError(f'Failed to backup database: {str(e)}')
50
+
51
+ def _backup_postgresql(self, db_settings, backup_path, format_type):
52
+ """Backup PostgreSQL database to a file"""
53
+ db_name = db_settings['NAME']
54
+ db_user = db_settings['USER']
55
+ db_password = db_settings['PASSWORD']
56
+ db_host = db_settings['HOST']
57
+ db_port = db_settings['PORT']
58
+
59
+ env = os.environ.copy()
60
+ env['PGPASSWORD'] = db_password
61
+
62
+ if format_type == 'sql':
63
+ # Plain SQL dump
64
+ command = [
65
+ 'pg_dump',
66
+ f'-h{db_host}',
67
+ f'-p{db_port}',
68
+ f'-U{db_user}',
69
+ '-d', db_name,
70
+ '-f', backup_path
71
+ ]
72
+ else:
73
+ # Custom format dump
74
+ format_flag = '-Fc' if format_type == 'dump' else '-Fc' # Default to custom format
75
+ command = [
76
+ 'pg_dump',
77
+ f'-h{db_host}',
78
+ f'-p{db_port}',
79
+ f'-U{db_user}',
80
+ format_flag,
81
+ '-d', db_name,
82
+ '-f', backup_path
83
+ ]
84
+
85
+ process = subprocess.Popen(
86
+ command,
87
+ stdout=subprocess.PIPE,
88
+ stderr=subprocess.PIPE,
89
+ env=env
90
+ )
91
+ stdout, stderr = process.communicate()
92
+
93
+ if process.returncode != 0:
94
+ raise CommandError(f'pg_dump command failed: {stderr.decode()}')
95
+
96
+ def _backup_sqlite3(self, db_settings, backup_path):
97
+ """Backup SQLite database to a file"""
98
+ db_path = db_settings['NAME']
99
+
100
+ if not os.path.exists(db_path):
101
+ raise CommandError(f'SQLite database file does not exist: {db_path}')
102
+
103
+ # For SQLite, we can simply copy the database file
104
+ import shutil
105
+ shutil.copy2(db_path, backup_path)
106
+
107
+ # Alternatively, we can use the .dump command for a SQL dump
108
+ if backup_path.endswith('.sql'):
109
+ command = [
110
+ 'sqlite3',
111
+ db_path,
112
+ '.dump'
113
+ ]
114
+
115
+ with open(backup_path, 'w') as f:
116
+ process = subprocess.Popen(
117
+ command,
118
+ stdout=f,
119
+ stderr=subprocess.PIPE
120
+ )
121
+ _, stderr = process.communicate()
122
+
123
+ if process.returncode != 0:
124
+ raise CommandError(f'sqlite3 dump command failed: {stderr.decode()}')
125
+
126
+ def _backup_mysql(self, db_settings, backup_path):
127
+ """Backup MySQL database to a file"""
128
+ db_name = db_settings['NAME']
129
+ db_user = db_settings['USER']
130
+ db_password = db_settings['PASSWORD']
131
+ db_host = db_settings['HOST']
132
+ db_port = db_settings['PORT']
133
+
134
+ command = [
135
+ 'mysqldump',
136
+ f'-h{db_host}',
137
+ f'-P{db_port}',
138
+ f'-u{db_user}',
139
+ f'-p{db_password}',
140
+ '--single-transaction',
141
+ '--routines',
142
+ '--triggers',
143
+ '--events',
144
+ db_name
145
+ ]
146
+
147
+ with open(backup_path, 'w') as f:
148
+ process = subprocess.Popen(
149
+ command,
150
+ stdout=f,
151
+ stderr=subprocess.PIPE
152
+ )
153
+ _, stderr = process.communicate()
154
+
155
+ if process.returncode != 0:
156
+ raise CommandError(f'mysqldump command failed: {stderr.decode()}')
@@ -0,0 +1,167 @@
1
+ import os
2
+ import subprocess
3
+ from django.core.management.base import BaseCommand, CommandError
4
+ from django.conf import settings
5
+
6
+
7
+ class Command(BaseCommand):
8
+ help = 'Restore database from a backup file'
9
+
10
+ def add_arguments(self, parser):
11
+ parser.add_argument('--path', type=str, required=True, help='Path to the backup file')
12
+
13
+ def handle(self, *args, **options):
14
+ backup_path = options['path']
15
+
16
+ if not os.path.exists(backup_path):
17
+ raise CommandError(f'Backup file does not exist: {backup_path}')
18
+
19
+ db_settings = settings.DATABASES['default']
20
+ db_type = db_settings['ENGINE'].split('.')[-1]
21
+
22
+ self.stdout.write(self.style.WARNING(f'Restoring database from {backup_path}'))
23
+ self.stdout.write(self.style.WARNING('This will overwrite the current database. Are you sure? (y/n)'))
24
+
25
+ # confirm = input()
26
+ # if confirm.lower() != 'y':
27
+ # self.stdout.write(self.style.ERROR('Restore cancelled.'))
28
+ # return
29
+
30
+ try:
31
+ if db_type == 'postgresql':
32
+ self._restore_postgresql(db_settings, backup_path)
33
+ elif db_type == 'sqlite3':
34
+ self._restore_sqlite3(db_settings, backup_path)
35
+ elif db_type == 'mysql':
36
+ self._restore_mysql(db_settings, backup_path)
37
+ else:
38
+ raise CommandError(f'Unsupported database type: {db_type}')
39
+
40
+ self.stdout.write(self.style.SUCCESS('Database restored successfully!'))
41
+ except Exception as e:
42
+ raise CommandError(f'Failed to restore database: {str(e)}')
43
+
44
+ def _restore_postgresql(self, db_settings, backup_path):
45
+ """Restore PostgreSQL database from backup file"""
46
+ db_name = db_settings['NAME']
47
+ db_user = db_settings['USER']
48
+ db_password = db_settings['PASSWORD']
49
+ db_host = db_settings['HOST']
50
+ db_port = db_settings['PORT']
51
+
52
+ # Check file extension to determine restore method
53
+ if backup_path.endswith('.sql'):
54
+ # SQL dump file
55
+ env = os.environ.copy()
56
+ env['PGPASSWORD'] = db_password
57
+
58
+ command = [
59
+ 'psql',
60
+ f'-h{db_host}',
61
+ f'-p{db_port}',
62
+ f'-U{db_user}',
63
+ f'-d{db_name}',
64
+ '-f', backup_path
65
+ ]
66
+
67
+ process = subprocess.Popen(
68
+ command,
69
+ stdout=subprocess.PIPE,
70
+ stderr=subprocess.PIPE,
71
+ env=env
72
+ )
73
+ stdout, stderr = process.communicate()
74
+
75
+ if process.returncode != 0:
76
+ raise CommandError(f'psql command failed: {stderr.decode()}')
77
+
78
+ elif backup_path.endswith('.dump'):
79
+ # pg_dump custom format
80
+ env = os.environ.copy()
81
+ env['PGPASSWORD'] = db_password
82
+
83
+ command = [
84
+ 'pg_restore',
85
+ '--clean',
86
+ '--if-exists',
87
+ f'-h{db_host}',
88
+ f'-p{db_port}',
89
+ f'-U{db_user}',
90
+ f'-d{db_name}',
91
+ backup_path
92
+ ]
93
+
94
+ process = subprocess.Popen(
95
+ command,
96
+ stdout=subprocess.PIPE,
97
+ stderr=subprocess.PIPE,
98
+ env=env
99
+ )
100
+ stdout, stderr = process.communicate()
101
+
102
+ if process.returncode != 0:
103
+ raise CommandError(f'pg_restore command failed: {stderr.decode()}')
104
+ else:
105
+ raise CommandError(f'Unsupported file format for PostgreSQL: {backup_path}')
106
+
107
+ def _restore_sqlite3(self, db_settings, backup_path):
108
+ """Restore SQLite database from backup file"""
109
+ db_path = db_settings['NAME']
110
+
111
+ if os.path.exists(db_path):
112
+ # Create a backup of the current database
113
+ backup_name = f"{db_path}.bak"
114
+ os.rename(db_path, backup_name)
115
+ self.stdout.write(self.style.WARNING(f'Current database backed up to {backup_name}'))
116
+
117
+ # For SQLite, we can simply copy the backup file to the database location
118
+ if backup_path.endswith('.sql'):
119
+ # SQL dump file - need to execute the SQL commands
120
+ try:
121
+ import sqlite3
122
+ conn = sqlite3.connect(db_path)
123
+ cursor = conn.cursor()
124
+
125
+ with open(backup_path, 'r') as f:
126
+ sql_script = f.read()
127
+ cursor.executescript(sql_script)
128
+
129
+ conn.commit()
130
+ conn.close()
131
+ except Exception as e:
132
+ raise CommandError(f'Failed to restore SQLite database: {str(e)}')
133
+ else:
134
+ # Binary backup - just copy the file
135
+ import shutil
136
+ shutil.copy2(backup_path, db_path)
137
+
138
+ def _restore_mysql(self, db_settings, backup_path):
139
+ """Restore MySQL database from backup file"""
140
+ db_name = db_settings['NAME']
141
+ db_user = db_settings['USER']
142
+ db_password = db_settings['PASSWORD']
143
+ db_host = db_settings['HOST']
144
+ db_port = db_settings['PORT']
145
+
146
+ command = [
147
+ 'mysql',
148
+ f'-h{db_host}',
149
+ f'-P{db_port}',
150
+ f'-u{db_user}',
151
+ f'-p{db_password}',
152
+ db_name,
153
+ '<', backup_path
154
+ ]
155
+
156
+ # For MySQL, we need to use shell=True to handle the redirect operator
157
+ command_str = ' '.join(command)
158
+ process = subprocess.Popen(
159
+ command_str,
160
+ stdout=subprocess.PIPE,
161
+ stderr=subprocess.PIPE,
162
+ shell=True
163
+ )
164
+ stdout, stderr = process.communicate()
165
+
166
+ if process.returncode != 0:
167
+ raise CommandError(f'mysql command failed: {stderr.decode()}')
File without changes
dj_utils/models.py ADDED
@@ -0,0 +1,3 @@
1
+ from django.db import models
2
+
3
+ # Create your models here.
dj_utils/signals.py ADDED
@@ -0,0 +1,7 @@
1
+ from django.dispatch import Signal
2
+
3
+
4
+
5
+ notifire = Signal()
6
+ send_sms = Signal()
7
+ send_mail = Signal()
dj_utils/tasks.py ADDED
@@ -0,0 +1,29 @@
1
+ from celery import shared_task
2
+ import zipfile
3
+ import os
4
+ from datetime import datetime
5
+ from django.core.management import call_command
6
+ from .handlers import notifire
7
+ @shared_task
8
+ def backup_database():
9
+ path= '/app/backup.sql'
10
+ call_command('backup',path= path)
11
+ # Create ZIP archive
12
+ zip_file = f"{path}.zip"
13
+ with zipfile.ZipFile(zip_file, 'w', zipfile.ZIP_DEFLATED) as zipf:
14
+ zipf.write(path, os.path.basename(path))
15
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
16
+ try:
17
+ with open(zip_file, 'rb') as backup:
18
+ notifire.send(
19
+ 'backup_database',
20
+ chat_id=os.getenv('BACKUP_TELEGRAM_CHAT_ID_EV'), # Send to admin group
21
+ file=backup,
22
+ text=f"📂 Database Backup \n📅 {timestamp}\n💾 {os.path.getsize(zip_file) / (1024 * 1024):.2f} MB"
23
+ )
24
+ except Exception as e:
25
+ print(e)
26
+ notifire.send(sender='backup_database', text=f"❌ Error sending backup: {str(e)}")
27
+ os.remove(zip_file)
28
+ os.remove(path)
29
+
@@ -0,0 +1,11 @@
1
+ try:
2
+ from core.celery_config import app
3
+ import os
4
+ app.conf.beat_schedule = {
5
+ 'run-backup-task': {
6
+ 'task': 'utils.tasks.backup_database',
7
+ 'schedule': int(os.getenv('BACKUP_EVERY_SECOND_EV')), # seconds
8
+ },
9
+
10
+ }
11
+ except:pass
@@ -0,0 +1,261 @@
1
+ {% load static %}
2
+ <!DOCTYPE html>
3
+ <html lang="fa" dir="rtl">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>{{ title }}</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <style>
10
+ @keyframes slideIn {
11
+ from {
12
+ opacity: 0;
13
+ transform: translateY(-20px);
14
+ }
15
+ to {
16
+ opacity: 1;
17
+ transform: translateY(0);
18
+ }
19
+ }
20
+
21
+ @keyframes spin {
22
+ 0% { transform: rotate(0deg); }
23
+ 100% { transform: rotate(360deg); }
24
+ }
25
+
26
+ .animate-slide-in {
27
+ animation: slideIn 0.3s ease;
28
+ }
29
+
30
+ .spinner {
31
+ border: 4px solid #f3f3f3;
32
+ border-top: 4px solid #667eea;
33
+ border-radius: 50%;
34
+ width: 40px;
35
+ height: 40px;
36
+ animation: spin 1s linear infinite;
37
+ margin: 0 auto;
38
+ }
39
+ </style>
40
+ </head>
41
+ <body class="bg-gradient-to-br from-purple-600 to-purple-800 min-h-screen p-4 md:p-8">
42
+ <div class="max-w-4xl mx-auto">
43
+ <!-- Header -->
44
+ <div class="bg-white rounded-t-2xl shadow-2xl overflow-hidden">
45
+ <div class="bg-gradient-to-r from-purple-600 to-purple-800 text-white p-8">
46
+ <h1 class="text-3xl md:text-4xl font-bold text-center mb-4">
47
+ 🗄️ {{ title }}
48
+ </h1>
49
+ <div class="bg-white/10 backdrop-blur-sm rounded-xl p-4 space-y-2">
50
+ <p class="text-sm md:text-base">
51
+ <span class="font-semibold">موتور دیتابیس:</span>
52
+ <span class="font-mono">{{ db_engine }}</span>
53
+ </p>
54
+ <p class="text-sm md:text-base">
55
+ <span class="font-semibold">نام دیتابیس:</span>
56
+ <span class="font-mono">{{ db_name }}</span>
57
+ </p>
58
+ </div>
59
+ </div>
60
+ </div>
61
+
62
+ <!-- Content -->
63
+ <div class="bg-white rounded-b-2xl shadow-2xl p-6 md:p-10">
64
+ <!-- Messages -->
65
+ {% if messages %}
66
+ <div class="mb-6 space-y-3">
67
+ {% for message in messages %}
68
+ <div class="animate-slide-in rounded-lg p-4 {% if message.tags == 'success' %}bg-green-50 text-green-800 border border-green-200{% elif message.tags == 'error' %}bg-red-50 text-red-800 border border-red-200{% else %}bg-blue-50 text-blue-800 border border-blue-200{% endif %}">
69
+ <p class="text-sm md:text-base">{{ message }}</p>
70
+ </div>
71
+ {% endfor %}
72
+ </div>
73
+ {% endif %}
74
+
75
+ <!-- Backup Section -->
76
+ <div class="bg-gradient-to-br from-green-50 to-emerald-50 rounded-xl p-6 md:p-8 mb-6 border border-green-100">
77
+ <h2 class="text-2xl font-bold text-green-700 mb-4 flex items-center gap-2">
78
+ <span>📦</span>
79
+ <span>ایجاد بک‌آپ</span>
80
+ </h2>
81
+ <p class="text-gray-700 mb-6 leading-relaxed">
82
+ با استفاده از این بخش می‌توانید از دیتابیس خود بک‌آپ تهیه کنید. فایل بک‌آپ به صورت فشرده (ZIP) دانلود خواهد شد.
83
+ </p>
84
+
85
+ <form method="post" action="{% url 'database_backup' %}" id="backupForm">
86
+ {% csrf_token %}
87
+ <button type="submit" class="bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white font-bold py-3 px-8 rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 flex items-center gap-2">
88
+ <span>💾</span>
89
+ <span>دانلود بک‌آپ</span>
90
+ </button>
91
+ </form>
92
+
93
+ <div class="hidden mt-6 text-center" id="backupLoading">
94
+ <div class="spinner mb-3"></div>
95
+ <p class="text-gray-600 font-medium">در حال ایجاد بک‌آپ...</p>
96
+ </div>
97
+ </div>
98
+
99
+ <!-- Restore Section -->
100
+ <div class="bg-gradient-to-br from-purple-50 to-indigo-50 rounded-xl p-6 md:p-8 border border-purple-100">
101
+ <h2 class="text-2xl font-bold text-purple-700 mb-4 flex items-center gap-2">
102
+ <span>♻️</span>
103
+ <span>بازیابی دیتابیس</span>
104
+ </h2>
105
+ <p class="text-gray-700 mb-4 leading-relaxed">
106
+ برای بازیابی دیتابیس، فایل بک‌آپ (.sql، .dump یا .zip) خود را انتخاب کنید.
107
+ </p>
108
+
109
+ <!-- Warning -->
110
+ <div class="bg-yellow-50 border-r-4 border-yellow-400 p-4 mb-6 rounded-lg">
111
+ <div class="flex items-start gap-3">
112
+ <span class="text-2xl">⚠️</span>
113
+ <div>
114
+ <p class="font-bold text-yellow-800 mb-2">هشدار مهم:</p>
115
+ <p class="text-yellow-700 text-sm leading-relaxed">
116
+ بازیابی دیتابیس تمام اطلاعات فعلی را جایگزین می‌کند. قبل از ادامه، مطمئن شوید که از دیتابیس فعلی بک‌آپ تهیه کرده‌اید.
117
+ </p>
118
+ </div>
119
+ </div>
120
+ </div>
121
+
122
+ <form method="post" action="{% url 'database_restore' %}" enctype="multipart/form-data" id="restoreForm">
123
+ {% csrf_token %}
124
+
125
+ <div class="mb-6">
126
+ <label class="block text-gray-700 font-semibold mb-3">
127
+ انتخاب فایل بک‌آپ:
128
+ </label>
129
+
130
+ <div class="flex flex-col md:flex-row items-start md:items-center gap-4">
131
+ <label class="cursor-pointer bg-white hover:bg-gray-50 text-purple-600 font-semibold py-3 px-6 rounded-lg border-2 border-purple-300 hover:border-purple-400 transition-all duration-200 flex items-center gap-2">
132
+ <span>📁</span>
133
+ <span>انتخاب فایل</span>
134
+ <input type="file" name="backup_file" id="backupFile" accept=".sql,.dump,.zip" class="hidden" required>
135
+ </label>
136
+
137
+ <div class="flex-1 w-full md:w-auto">
138
+ <div id="fileName" class="bg-white border-2 border-dashed border-purple-300 rounded-lg p-3 text-gray-500 text-sm min-w-[200px]">
139
+ هیچ فایلی انتخاب نشده
140
+ </div>
141
+ </div>
142
+ </div>
143
+
144
+ <p class="text-xs text-gray-500 mt-2">
145
+ فرمت‌های مجاز: .sql، .dump، .zip
146
+ </p>
147
+ </div>
148
+
149
+ <div class="flex flex-col sm:flex-row gap-3">
150
+ <button type="submit" id="restoreBtn" class="bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-bold py-3 px-8 rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none flex items-center justify-center gap-2" disabled>
151
+ <span>♻️</span>
152
+ <span>بازیابی دیتابیس</span>
153
+ </button>
154
+
155
+ <button type="button" onclick="window.location.href='{% url 'database_management' %}'" class="bg-gray-200 hover:bg-gray-300 text-gray-700 font-semibold py-3 px-8 rounded-lg transition-all duration-200 flex items-center justify-center gap-2">
156
+ <span>↩️</span>
157
+ <span>بازگشت</span>
158
+ </button>
159
+ </div>
160
+ </form>
161
+
162
+ <div class="hidden mt-6 text-center" id="restoreLoading">
163
+ <div class="spinner mb-3"></div>
164
+ <p class="text-gray-600 font-medium">در حال بازیابی دیتابیس...</p>
165
+ <p class="text-sm text-gray-500 mt-2">این عملیات ممکن است چند دقیقه طول بکشد...</p>
166
+ </div>
167
+ </div>
168
+
169
+ <!-- Info Section -->
170
+ <div class="mt-6 bg-blue-50 border border-blue-200 rounded-xl p-6">
171
+ <h3 class="text-lg font-bold text-blue-800 mb-3 flex items-center gap-2">
172
+ <span>ℹ️</span>
173
+ <span>راهنما</span>
174
+ </h3>
175
+ <ul class="space-y-2 text-sm text-blue-700">
176
+ <li class="flex items-start gap-2">
177
+ <span class="mt-1">•</span>
178
+ <span>برای ایجاد بک‌آپ، روی دکمه "دانلود بک‌آپ" کلیک کنید</span>
179
+ </li>
180
+ <li class="flex items-start gap-2">
181
+ <span class="mt-1">•</span>
182
+ <span>فایل بک‌آپ به صورت خودکار فشرده و دانلود می‌شود</span>
183
+ </li>
184
+ <li class="flex items-start gap-2">
185
+ <span class="mt-1">•</span>
186
+ <span>برای بازیابی، فایل بک‌آپ را انتخاب کرده و روی "بازیابی دیتابیس" کلیک کنید</span>
187
+ </li>
188
+ <li class="flex items-start gap-2">
189
+ <span class="mt-1">•</span>
190
+ <span>حتماً قبل از بازیابی، از دیتابیس فعلی بک‌آپ تهیه کنید</span>
191
+ </li>
192
+ </ul>
193
+ </div>
194
+ </div>
195
+ </div>
196
+
197
+ <script>
198
+ // File input handler
199
+ const fileInput = document.getElementById('backupFile');
200
+ const fileName = document.getElementById('fileName');
201
+ const restoreBtn = document.getElementById('restoreBtn');
202
+
203
+ fileInput.addEventListener('change', function(e) {
204
+ if (this.files && this.files[0]) {
205
+ const file = this.files[0];
206
+ const fileSize = (file.size / (1024 * 1024)).toFixed(2);
207
+ fileName.innerHTML = `
208
+ <div class="flex items-center justify-between">
209
+ <span class="text-gray-700 font-medium truncate">${file.name}</span>
210
+ <span class="text-gray-500 text-xs mr-2">${fileSize} MB</span>
211
+ </div>
212
+ `;
213
+ fileName.classList.remove('border-dashed', 'text-gray-500');
214
+ fileName.classList.add('border-solid', 'border-green-400', 'bg-green-50');
215
+ restoreBtn.disabled = false;
216
+ } else {
217
+ fileName.textContent = 'هیچ فایلی انتخاب نشده';
218
+ fileName.classList.add('border-dashed', 'text-gray-500');
219
+ fileName.classList.remove('border-solid', 'border-green-400', 'bg-green-50');
220
+ restoreBtn.disabled = true;
221
+ }
222
+ });
223
+
224
+ // Backup form handler
225
+ const backupForm = document.getElementById('backupForm');
226
+ const backupLoading = document.getElementById('backupLoading');
227
+
228
+ backupForm.addEventListener('submit', function() {
229
+ backupLoading.classList.remove('hidden');
230
+ this.querySelector('button').disabled = true;
231
+ });
232
+
233
+ // Restore form handler
234
+ const restoreForm = document.getElementById('restoreForm');
235
+ const restoreLoading = document.getElementById('restoreLoading');
236
+
237
+ restoreForm.addEventListener('submit', function(e) {
238
+ if (!confirm('⚠️ آیا مطمئن هستید که می‌خواهید دیتابیس را بازیابی کنید؟\n\nتمام اطلاعات فعلی جایگزین خواهند شد!')) {
239
+ e.preventDefault();
240
+ return false;
241
+ }
242
+
243
+ restoreLoading.classList.remove('hidden');
244
+ restoreBtn.disabled = true;
245
+ restoreBtn.innerHTML = '<span class="spinner-small"></span> در حال بازیابی...';
246
+ });
247
+
248
+ // Auto-hide messages after 5 seconds
249
+ setTimeout(function() {
250
+ const messages = document.querySelectorAll('.animate-slide-in');
251
+ messages.forEach(function(msg) {
252
+ msg.style.transition = 'opacity 0.5s ease';
253
+ msg.style.opacity = '0';
254
+ setTimeout(function() {
255
+ msg.remove();
256
+ }, 500);
257
+ });
258
+ }, 5000);
259
+ </script>
260
+ </body>
261
+ </html>
dj_utils/tests.py ADDED
@@ -0,0 +1,3 @@
1
+ from django.test import TestCase
2
+
3
+ # Create your tests here.
dj_utils/urls.py ADDED
@@ -0,0 +1,11 @@
1
+
2
+ from django.urls import path
3
+ from . import views
4
+
5
+ urlpatterns = [
6
+ # Database Management URLs
7
+ path('database/', views.database_management_view, name='database_management'),
8
+ path('database/backup/', views.database_backup_view, name='database_backup'),
9
+ path('database/restore/', views.database_restore_view, name='database_restore'),
10
+ path('database/backup/ajax/', views.database_backup_ajax, name='database_backup_ajax'),
11
+ ]
dj_utils/views.py ADDED
@@ -0,0 +1,174 @@
1
+
2
+ from django.shortcuts import render, redirect
3
+ from django.contrib.admin.views.decorators import staff_member_required
4
+ from django.contrib.auth.decorators import user_passes_test
5
+ from django.http import JsonResponse, HttpResponse, FileResponse
6
+ from django.core.management import call_command
7
+ from django.views.decorators.http import require_http_methods
8
+ from django.contrib import messages
9
+ from django.conf import settings
10
+ import os
11
+ import zipfile
12
+ from datetime import datetime
13
+ import tempfile
14
+ from io import BytesIO
15
+
16
+ def is_superuser(user):
17
+ """Check if user is superuser"""
18
+ return user.is_superuser
19
+
20
+ @user_passes_test(is_superuser)
21
+ @require_http_methods(["GET", "POST"])
22
+ def database_backup_view(request):
23
+ """
24
+ View for creating database backup
25
+ Only accessible by superusers
26
+ """
27
+ if request.method == "POST":
28
+ try:
29
+ # Create temporary backup file
30
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
31
+ backup_filename = f'backup_{timestamp}.sql'
32
+ backup_path = os.path.join(tempfile.gettempdir(), backup_filename)
33
+
34
+ # Call backup command
35
+ call_command('backup', path=backup_path)
36
+
37
+ # Create ZIP archive
38
+ zip_buffer = BytesIO()
39
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zipf:
40
+ zipf.write(backup_path, os.path.basename(backup_path))
41
+
42
+ # Clean up temporary file
43
+ if os.path.exists(backup_path):
44
+ os.remove(backup_path)
45
+
46
+ # Prepare response
47
+ zip_buffer.seek(0)
48
+ response = HttpResponse(zip_buffer.getvalue(), content_type='application/zip')
49
+ response['Content-Disposition'] = f'attachment; filename="backup_{timestamp}.zip"'
50
+
51
+ messages.success(request, f'✅ بک‌آپ با موفقیت ایجاد شد: {backup_filename}')
52
+ return response
53
+
54
+ except Exception as e:
55
+ messages.error(request, f'❌ خطا در ایجاد بک‌آپ: {str(e)}')
56
+ return redirect('database_management')
57
+
58
+ return redirect('database_management')
59
+
60
+ @user_passes_test(is_superuser)
61
+ @require_http_methods(["GET", "POST"])
62
+ def database_restore_view(request):
63
+ """
64
+ View for restoring database from backup
65
+ Only accessible by superusers
66
+ """
67
+ if request.method == "POST":
68
+ backup_file = request.FILES.get('backup_file')
69
+
70
+ if not backup_file:
71
+ messages.error(request, '❌ لطفاً فایل بک‌آپ را انتخاب کنید')
72
+ return redirect('database_management')
73
+
74
+ try:
75
+ # Save uploaded file temporarily
76
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
77
+ temp_dir = tempfile.gettempdir()
78
+
79
+ # Check if file is ZIP
80
+ if backup_file.name.endswith('.zip'):
81
+ zip_path = os.path.join(temp_dir, f'restore_{timestamp}.zip')
82
+ with open(zip_path, 'wb+') as destination:
83
+ for chunk in backup_file.chunks():
84
+ destination.write(chunk)
85
+
86
+ # Extract ZIP file
87
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
88
+ zip_ref.extractall(temp_dir)
89
+ extracted_files = zip_ref.namelist()
90
+
91
+ if not extracted_files:
92
+ raise Exception('فایل ZIP خالی است')
93
+
94
+ backup_path = os.path.join(temp_dir, extracted_files[0])
95
+
96
+ # Clean up ZIP file
97
+ os.remove(zip_path)
98
+ else:
99
+ # Direct SQL or dump file
100
+ backup_path = os.path.join(temp_dir, f'restore_{timestamp}{os.path.splitext(backup_file.name)[1]}')
101
+ with open(backup_path, 'wb+') as destination:
102
+ for chunk in backup_file.chunks():
103
+ destination.write(chunk)
104
+
105
+ # Validate file extension
106
+ if not (backup_path.endswith('.sql') or backup_path.endswith('.dump')):
107
+ os.remove(backup_path)
108
+ raise Exception('فرمت فایل پشتیبانی نشده است. فقط .sql یا .dump مجاز است')
109
+
110
+ # Call restore command
111
+ call_command('restore', path=backup_path)
112
+
113
+ # Clean up temporary file
114
+ if os.path.exists(backup_path):
115
+ os.remove(backup_path)
116
+
117
+ messages.success(request, '✅ دیتابیس با موفقیت بازیابی شد')
118
+
119
+ except Exception as e:
120
+ messages.error(request, f'❌ خطا در بازیابی دیتابیس: {str(e)}')
121
+
122
+ # Clean up on error
123
+ if 'backup_path' in locals() and os.path.exists(backup_path):
124
+ os.remove(backup_path)
125
+
126
+ return redirect('database_management')
127
+
128
+ @user_passes_test(is_superuser)
129
+ def database_management_view(request):
130
+ """
131
+ Main view for database management
132
+ Shows backup and restore options
133
+ """
134
+ context = {
135
+ 'title': 'مدیریت دیتابیس',
136
+ 'db_engine': settings.DATABASES['default']['ENGINE'],
137
+ 'db_name': settings.DATABASES['default']['NAME'],
138
+ }
139
+
140
+ return render(request, 'dj_utils/database_management.html', context)
141
+
142
+ @user_passes_test(is_superuser)
143
+ @require_http_methods(["POST"])
144
+ def database_backup_ajax(request):
145
+ """
146
+ AJAX endpoint for database backup
147
+ Returns JSON response
148
+ """
149
+ try:
150
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
151
+ backup_filename = f'backup_{timestamp}.sql'
152
+ backup_path = os.path.join(tempfile.gettempdir(), backup_filename)
153
+
154
+ call_command('backup', path=backup_path)
155
+
156
+ file_size = os.path.getsize(backup_path) / (1024 * 1024) # Size in MB
157
+
158
+ # Clean up
159
+ if os.path.exists(backup_path):
160
+ os.remove(backup_path)
161
+
162
+ return JsonResponse({
163
+ 'success': True,
164
+ 'message': f'بک‌آپ با موفقیت ایجاد شد',
165
+ 'filename': backup_filename,
166
+ 'size': f'{file_size:.2f} MB',
167
+ 'timestamp': timestamp
168
+ })
169
+
170
+ except Exception as e:
171
+ return JsonResponse({
172
+ 'success': False,
173
+ 'message': f'خطا در ایجاد بک‌آپ: {str(e)}'
174
+ }, status=500)
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: starco-dj-utils
3
+ Version: 1.0.2
4
+ Summary: A Django pluggable app for utilities and database management
5
+ Author: Mojtaba
6
+ Author-email: m.tahmasbi0111@yahoo.com
7
+ License: MIT
8
+ Classifier: Framework :: Django
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Dist: Django>=4.0
11
+ Dynamic: author
12
+ Dynamic: author-email
13
+ Dynamic: classifier
14
+ Dynamic: license
15
+ Dynamic: requires-dist
16
+ Dynamic: summary
@@ -0,0 +1,21 @@
1
+ dj_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ dj_utils/admin.py,sha256=suMo4x8I3JBxAFBVIdE-5qnqZ6JAZV0FESABHOSc-vg,63
3
+ dj_utils/apps.py,sha256=4Hj6VLtqAhiItX4Ifztaj5DnP7csfHKk_kCtTmPAC8w,234
4
+ dj_utils/handlers.py,sha256=4BAmPAV1gByCrZHpUyXc9qxpqIJeT2cLCV8p_F-cJCc,3102
5
+ dj_utils/models.py,sha256=Vjc0p2XbAPgE6HyTF6vll98A4eDhA5AvaQqsc4kQ9AQ,57
6
+ dj_utils/signals.py,sha256=1UsFrBjhrJPL7mExvD6SExgYAl1oqU8SDdZVmr65NF8,99
7
+ dj_utils/tasks.py,sha256=vBCfY_cE_tbS6I-0NyUm4fwf5_QfvUvyAjm0GYoJSiY,1047
8
+ dj_utils/tasks_config.py,sha256=aRjQ4xcbFWRY3XwCyAeBAqxS1ZfIPYBZBXNs4LnONGo,276
9
+ dj_utils/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2nk6VY-g,60
10
+ dj_utils/urls.py,sha256=sKZ0c7IxR6Z-reGRhDilLq4ZOldD4ZC_t5uN_SPIP3Y,442
11
+ dj_utils/views.py,sha256=6HQBbaDiYIpYoRlYrTs3pKNTfzj9U4xXaFxkg9PBXWc,6599
12
+ dj_utils/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ dj_utils/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ dj_utils/management/commands/backup.py,sha256=2LVTUyeKpyvrjEzliGbyehYNk-umXFasSDr1qHZkhj0,5573
15
+ dj_utils/management/commands/restore.py,sha256=hg_S-Rvd83pyyG9-nML99Fh6htZHkaQgmTkMUc4we3k,5809
16
+ dj_utils/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ dj_utils/templates/dj_utils/database_management.html,sha256=KMOseSxu_RGujuEwSpeXrp2kYm61FISRhKyk9L5Khbo,13248
18
+ starco_dj_utils-1.0.2.dist-info/METADATA,sha256=Qza2AyGxRVAbmlmUV7I7QPFMw5xxlO29kNR9otso_T8,419
19
+ starco_dj_utils-1.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ starco_dj_utils-1.0.2.dist-info/top_level.txt,sha256=rsoZCEk78JSKSSx0ac_lRbFiZBJpicuobSbw2l0cxvk,9
21
+ starco_dj_utils-1.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ dj_utils