Backend Draft
This commit is contained in:
0
backend/tenants/__init__.py
Normal file
0
backend/tenants/__init__.py
Normal file
BIN
backend/tenants/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
backend/tenants/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/tenants/__pycache__/admin.cpython-314.pyc
Normal file
BIN
backend/tenants/__pycache__/admin.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/tenants/__pycache__/apps.cpython-314.pyc
Normal file
BIN
backend/tenants/__pycache__/apps.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/tenants/__pycache__/managers.cpython-314.pyc
Normal file
BIN
backend/tenants/__pycache__/managers.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/tenants/__pycache__/middleware.cpython-314.pyc
Normal file
BIN
backend/tenants/__pycache__/middleware.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/tenants/__pycache__/models.cpython-314.pyc
Normal file
BIN
backend/tenants/__pycache__/models.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/tenants/__pycache__/serializers.cpython-314.pyc
Normal file
BIN
backend/tenants/__pycache__/serializers.cpython-314.pyc
Normal file
Binary file not shown.
8
backend/tenants/admin.py
Normal file
8
backend/tenants/admin.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.contrib import admin
|
||||
from .models import Tenant
|
||||
|
||||
@admin.register(Tenant)
|
||||
class TenantAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'subdomain', 'is_active', 'created_at')
|
||||
search_fields = ('name', 'subdomain')
|
||||
list_filter = ('is_active',)
|
||||
6
backend/tenants/apps.py
Normal file
6
backend/tenants/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TenantsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "tenants"
|
||||
Binary file not shown.
294
backend/tenants/management/commands/populate_test_data.py
Normal file
294
backend/tenants/management/commands/populate_test_data.py
Normal file
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Django Management Command: populate_test_data
|
||||
|
||||
Creates realistic test data for MTCBD.
|
||||
Run with: python manage.py populate_test_data
|
||||
"""
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from faker import Faker
|
||||
from django.utils import timezone
|
||||
import random
|
||||
import datetime
|
||||
|
||||
from tenants.models import Tenant
|
||||
from accounts.models import User
|
||||
from projects.models import Project, Task
|
||||
from dashboard.models import Notification
|
||||
from analytics.models import ActivityLog
|
||||
|
||||
fake = Faker()
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Populate database with realistic test data (100+ users, projects, tasks, etc.)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--clear',
|
||||
action='store_true',
|
||||
help='Clear existing data before populating',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
clear = options['clear']
|
||||
|
||||
if clear and Tenant.objects.exists():
|
||||
self.stdout.write(self.style.WARNING('Clearing existing data...'))
|
||||
Task.objects.all().delete()
|
||||
Project.objects.all().delete()
|
||||
Notification.objects.all().delete()
|
||||
ActivityLog.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
Tenant.objects.all().delete()
|
||||
self.stdout.write(self.style.SUCCESS('✓ Data cleared'))
|
||||
|
||||
self.main()
|
||||
self.stdout.write(self.style.SUCCESS('\n✅ Population complete!'))
|
||||
|
||||
def main(self):
|
||||
self.stdout.write("MTCBD Database Population".center(60))
|
||||
self.stdout.write("="*60)
|
||||
|
||||
# Configuration
|
||||
NUM_TENANTS = 5
|
||||
USERS_PER_TENANT = 20
|
||||
PROJECTS_PER_TENANT = (3, 8)
|
||||
TASKS_PER_PROJECT = (5, 15)
|
||||
NOTIFICATIONS_PER_USER = (0, 5)
|
||||
ACTIVITY_LOGS_PER_TENANT = 50
|
||||
|
||||
TENANT_DATA = [
|
||||
{'name': 'Tech Academy', 'subdomain': 'techacademy'},
|
||||
{'name': 'Medical College', 'subdomain': 'medcollege'},
|
||||
{'name': 'Business School', 'subdomain': 'businessschool'},
|
||||
{'name': 'Engineering Institute', 'subdomain': 'enginstitute'},
|
||||
{'name': 'Arts University', 'subdomain': 'artsuniv'},
|
||||
]
|
||||
|
||||
tenants = self.create_tenants(TENANT_DATA)
|
||||
total_users = 0
|
||||
total_projects = 0
|
||||
total_tasks = 0
|
||||
total_notifications = 0
|
||||
total_logs = 0
|
||||
|
||||
for tenant in tenants:
|
||||
users = self.create_users_for_tenant(tenant, USERS_PER_TENANT)
|
||||
total_users += len(users)
|
||||
|
||||
projects = self.create_projects_for_tenant(tenant, users, PROJECTS_PER_TENANT)
|
||||
total_projects += len(projects)
|
||||
|
||||
if projects:
|
||||
tasks_created = self.create_tasks_for_projects(projects, users, TASKS_PER_PROJECT)
|
||||
total_tasks += tasks_created
|
||||
|
||||
notifications = self.create_notifications(users, NOTIFICATIONS_PER_USER)
|
||||
total_notifications += notifications
|
||||
|
||||
logs = self.create_activity_logs(tenant, users, ACTIVITY_LOGS_PER_TENANT)
|
||||
total_logs += logs
|
||||
|
||||
self.print_summary(tenants, total_users, total_projects, total_tasks, total_notifications, total_logs)
|
||||
|
||||
def create_tenants(self, tenant_data):
|
||||
self.stdout.write('\nCreating tenants...')
|
||||
tenants = []
|
||||
for data in tenant_data:
|
||||
tenant, created = Tenant.objects.get_or_create(
|
||||
subdomain=data['subdomain'],
|
||||
defaults={'name': data['name'], 'is_active': True}
|
||||
)
|
||||
tenants.append(tenant)
|
||||
status = "✓ Created" if created else "→ Exists"
|
||||
self.stdout.write(f' {status}: {tenant.name} ({tenant.subdomain})')
|
||||
return tenants
|
||||
|
||||
def create_users_for_tenant(self, tenant, count):
|
||||
self.stdout.write(f'\n Creating users for {tenant.subdomain}...')
|
||||
users = []
|
||||
|
||||
# One super_admin and one institution_admin
|
||||
admin_roles = ['super_admin', 'institution_admin']
|
||||
for role in admin_roles:
|
||||
username = f"{role}_{tenant.subdomain}"
|
||||
email = f"{role}@{tenant.subdomain}.com"
|
||||
user, created = User.objects.get_or_create(
|
||||
username=username,
|
||||
defaults={
|
||||
'email': email,
|
||||
'first_name': fake.first_name(),
|
||||
'last_name': fake.last_name(),
|
||||
'role': role,
|
||||
'tenant': tenant,
|
||||
'is_staff': role in ['super_admin', 'institution_admin'],
|
||||
'is_active': True,
|
||||
}
|
||||
)
|
||||
if created:
|
||||
user.set_password('password123')
|
||||
user.save()
|
||||
users.append(user)
|
||||
self.stdout.write(f' ✓ {role}: {username}')
|
||||
|
||||
# Other users
|
||||
remaining = count - 2
|
||||
for i in range(remaining):
|
||||
role = random.choice(['teacher', 'project_manager', 'student', 'student', 'student'])
|
||||
username = f"{tenant.subdomain}_{i+1}"
|
||||
email = f"user{i+1}@{tenant.subdomain}.com"
|
||||
user, created = User.objects.get_or_create(
|
||||
username=username,
|
||||
defaults={
|
||||
'email': email,
|
||||
'first_name': fake.first_name(),
|
||||
'last_name': fake.last_name(),
|
||||
'role': role,
|
||||
'tenant': tenant,
|
||||
'is_active': True,
|
||||
}
|
||||
)
|
||||
if created:
|
||||
user.set_password('password123')
|
||||
user.save()
|
||||
users.append(user)
|
||||
|
||||
self.stdout.write(f' → Total users for {tenant.subdomain}: {len(users)}')
|
||||
return users
|
||||
|
||||
def create_projects_for_tenant(self, tenant, users, range_tuple):
|
||||
self.stdout.write(f'\n Creating projects for {tenant.subdomain}...')
|
||||
projects = []
|
||||
num_projects = random.randint(*range_tuple)
|
||||
statuses = ['active', 'planned', 'completed', 'archived']
|
||||
|
||||
admin_users = [u for u in users if u.role in ['teacher', 'institution_admin', 'super_admin', 'project_manager']]
|
||||
if not admin_users:
|
||||
admin_users = users
|
||||
|
||||
for i in range(num_projects):
|
||||
creator = random.choice(admin_users)
|
||||
project = Project.objects.create(
|
||||
tenant=tenant,
|
||||
name=fake.catch_phrase().title(),
|
||||
description=fake.text(max_nb_chars=200) if random.random() > 0.3 else '',
|
||||
created_by=creator,
|
||||
status=random.choice(statuses),
|
||||
created_at=random_date(timezone.now() - datetime.timedelta(days=365), timezone.now()),
|
||||
)
|
||||
projects.append(project)
|
||||
self.stdout.write(f' ✓ Project: {project.name}')
|
||||
return projects
|
||||
|
||||
def create_tasks_for_projects(self, projects, users, range_tuple):
|
||||
self.stdout.write(f'\n Creating tasks...')
|
||||
total_tasks = 0
|
||||
statuses = ['todo', 'in_progress', 'review', 'done']
|
||||
priorities = ['low', 'medium', 'high', 'urgent']
|
||||
|
||||
for project in projects:
|
||||
num_tasks = random.randint(*range_tuple)
|
||||
project_users = [u for u in users if u.tenant == project.tenant]
|
||||
|
||||
for i in range(num_tasks):
|
||||
assigned_to = random.choice(project_users) if random.random() > 0.3 else None
|
||||
due_date = random_date(timezone.now() + datetime.timedelta(days=1), timezone.now() + datetime.timedelta(days=90)) if random.random() > 0.4 else None
|
||||
|
||||
task = Task.objects.create(
|
||||
tenant=project.tenant,
|
||||
project=project,
|
||||
title=fake.sentence(nb_words=6).rstrip('.'),
|
||||
description=fake.text(max_nb_chars=150) if random.random() > 0.5 else '',
|
||||
assigned_to=assigned_to,
|
||||
status=random.choice(statuses),
|
||||
priority=random.choice(priorities),
|
||||
due_date=due_date,
|
||||
)
|
||||
total_tasks += 1
|
||||
self.stdout.write(f' → {project.name}: {num_tasks} tasks')
|
||||
|
||||
self.stdout.write(f' ✓ Total tasks created: {total_tasks}')
|
||||
return total_tasks
|
||||
|
||||
def create_notifications(self, users, range_tuple):
|
||||
self.stdout.write(f'\n Creating notifications...')
|
||||
total_notifications = 0
|
||||
titles = [
|
||||
'New task assigned', 'Project update', 'Deadline approaching',
|
||||
'Comment on your task', 'Project completed', 'Meeting reminder',
|
||||
]
|
||||
|
||||
for user in users:
|
||||
num_notifications = random.randint(*range_tuple)
|
||||
for i in range(num_notifications):
|
||||
Notification.objects.create(
|
||||
tenant=user.tenant,
|
||||
user=user,
|
||||
title=random.choice(titles),
|
||||
message=fake.sentence(),
|
||||
is_read=random.choice([True, False, False, False]),
|
||||
link=fake.url() if random.random() > 0.7 else None,
|
||||
created_at=random_date(timezone.now() - datetime.timedelta(days=30), timezone.now()),
|
||||
)
|
||||
total_notifications += 1
|
||||
self.stdout.write(f' ✓ Total notifications: {total_notifications}')
|
||||
return total_notifications
|
||||
|
||||
def create_activity_logs(self, tenant, users, count):
|
||||
self.stdout.write(f'\n Creating activity logs for {tenant.subdomain}...')
|
||||
actions = ['created', 'updated', 'deleted', 'logged_in', 'assigned', 'completed']
|
||||
target_types = ['project', 'task', 'user', 'notification']
|
||||
|
||||
all_users = list(User.objects.all())
|
||||
total_logs = 0
|
||||
|
||||
for i in range(count):
|
||||
user = random.choice(all_users) if all_users else None
|
||||
ActivityLog.objects.create(
|
||||
tenant=tenant,
|
||||
user=user,
|
||||
action=random.choice(actions),
|
||||
target_type=random.choice(target_types),
|
||||
target_id=str(random.randint(1, 1000)),
|
||||
metadata={
|
||||
'ip_address': fake.ipv4(),
|
||||
'user_agent': fake.user_agent(),
|
||||
} if random.random() > 0.5 else {},
|
||||
created_at=random_date(timezone.now() - datetime.timedelta(days=180), timezone.now()),
|
||||
)
|
||||
total_logs += 1
|
||||
|
||||
self.stdout.write(f' ✓ Activity logs: {total_logs}')
|
||||
return total_logs
|
||||
|
||||
def print_summary(self, tenants, total_users, total_projects, total_tasks, total_notifications, total_logs):
|
||||
self.stdout.write("\n" + "="*60)
|
||||
self.stdout.write(" POPULATION COMPLETE".center(60))
|
||||
self.stdout.write("="*60)
|
||||
self.stdout.write(f' Tenants created: {len(tenants)}')
|
||||
self.stdout.write(f' Total users: {total_users}')
|
||||
self.stdout.write(f' Total projects: {total_projects}')
|
||||
self.stdout.write(f' Total tasks: {total_tasks}')
|
||||
self.stdout.write(f' Total notifications: {total_notifications}')
|
||||
self.stdout.write(f' Total activity logs: {total_logs}')
|
||||
self.stdout.write("="*60)
|
||||
self.stdout.write('\nTest accounts per tenant:')
|
||||
self.stdout.write(' Username: super_admin_<subdomain> / Password: password123')
|
||||
self.stdout.write(' Username: institution_admin_<subdomain> / Password: password123')
|
||||
self.stdout.write(' Other users: <subdomain>_1, <subdomain>_2, ...')
|
||||
self.stdout.write('\nLogin example (use first tenant ID as X-Tenant-ID):')
|
||||
self.stdout.write(' POST /api/auth/login/')
|
||||
self.stdout.write(' Headers: X-Tenant-ID: 1')
|
||||
self.stdout.write(' Body: {"username": "super_admin_techacademy", "password": "password123"}')
|
||||
self.stdout.write("="*60)
|
||||
|
||||
# Also print tenant IDs for reference
|
||||
self.stdout.write('\nTenant IDs:')
|
||||
for t in tenants:
|
||||
self.stdout.write(f' {t.id}: {t.name} ({t.subdomain})')
|
||||
|
||||
def random_date(start_date, end_date):
|
||||
delta = end_date - start_date
|
||||
random_days = random.randint(0, delta.days)
|
||||
return start_date + datetime.timedelta(days=random_days)
|
||||
15
backend/tenants/managers.py
Normal file
15
backend/tenants/managers.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django.db import models
|
||||
from django.core.exceptions import FieldError
|
||||
|
||||
class TenantScopedQuerySet(models.QuerySet):
|
||||
def tenant(self, tenant=None):
|
||||
if tenant:
|
||||
return self.filter(tenant=tenant)
|
||||
return self
|
||||
|
||||
class TenantScopedManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return TenantScopedQuerySet(self.model, using=self._db)
|
||||
|
||||
def tenant(self, tenant=None):
|
||||
return self.get_queryset().tenant(tenant)
|
||||
39
backend/tenants/middleware.py
Normal file
39
backend/tenants/middleware.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from django.http import JsonResponse
|
||||
from tenants.models import Tenant
|
||||
|
||||
class TenantMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
self.exempt_paths = [
|
||||
'/admin/',
|
||||
'/api/schema/',
|
||||
'/api/docs/',
|
||||
'/api/auth/login/',
|
||||
'/api/auth/register/',
|
||||
'/api/auth/token/refresh/',
|
||||
'/api/auth/profile/',
|
||||
]
|
||||
|
||||
def __call__(self, request):
|
||||
if any(request.path.startswith(path) for path in self.exempt_paths):
|
||||
request.tenant = None
|
||||
return self.get_response(request)
|
||||
|
||||
# 1. Check Header
|
||||
tenant_id = request.headers.get('X-Tenant-ID')
|
||||
if not tenant_id:
|
||||
# 2. Check Subdomain (Optional, skipping for now, can implement later)
|
||||
# host = request.get_host().split(':')[0]
|
||||
# subdomain = host.split('.')[0]
|
||||
pass
|
||||
|
||||
if tenant_id:
|
||||
try:
|
||||
request.tenant = Tenant.objects.get(id=tenant_id, is_active=True)
|
||||
except Tenant.DoesNotExist:
|
||||
return JsonResponse({"detail": "Invalid or inactive tenant ID supplied."}, status=403)
|
||||
else:
|
||||
# Normally we might enforce tenant_id, but we'll let permission classes handle it.
|
||||
request.tenant = None
|
||||
|
||||
return self.get_response(request)
|
||||
34
backend/tenants/migrations/0001_initial.py
Normal file
34
backend/tenants/migrations/0001_initial.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 6.0.2 on 2026-02-20 18:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Tenant",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
(
|
||||
"subdomain",
|
||||
models.CharField(db_index=True, max_length=100, unique=True),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
backend/tenants/migrations/__init__.py
Normal file
0
backend/tenants/migrations/__init__.py
Normal file
Binary file not shown.
BIN
backend/tenants/migrations/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
backend/tenants/migrations/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
10
backend/tenants/models.py
Normal file
10
backend/tenants/models.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.db import models
|
||||
|
||||
class Tenant(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
subdomain = models.CharField(max_length=100, unique=True, db_index=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
8
backend/tenants/serializers.py
Normal file
8
backend/tenants/serializers.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from rest_framework import serializers
|
||||
from .models import Tenant
|
||||
|
||||
class TenantSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Tenant
|
||||
fields = ['id', 'name', 'subdomain', 'created_at', 'is_active']
|
||||
read_only_fields = ['id', 'created_at']
|
||||
3
backend/tenants/tests.py
Normal file
3
backend/tenants/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
backend/tenants/views.py
Normal file
3
backend/tenants/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
Reference in New Issue
Block a user