diff --git a/accounts/migrations/0011_user_allowed_modules_alter_user_id.py b/accounts/migrations/0011_user_allowed_modules_alter_user_id.py new file mode 100644 index 0000000..3714be2 --- /dev/null +++ b/accounts/migrations/0011_user_allowed_modules_alter_user_id.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.21 on 2026-03-31 08:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0010_alter_user_id'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='allowed_modules', + field=models.TextField(blank=True, help_text='Comma-separated module slugs this user can access', null=True), + ), + migrations.AlterField( + model_name='user', + name='id', + field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index ec45ae2..b8e3845 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -37,6 +37,23 @@ class User(AbstractUser): profile_picture = models.ImageField(upload_to='profile_pictures/', blank=True, null=True, default='default.png') + allowed_modules = models.TextField( + blank=True, null=True, + help_text="Comma-separated module slugs this user can access" + ) + + ALL_MODULES = ["dashboard", "partners", "events", "ad-control", "users", "reviews", "contributions", "financials", "settings"] + + def get_allowed_modules(self): + ALL = ["dashboard", "partners", "events", "ad-control", "users", "reviews", "contributions", "financials", "settings"] + if self.is_superuser or self.role == "admin": + return ALL + if self.allowed_modules: + return [m.strip() for m in self.allowed_modules.split(",") if m.strip()] + if self.role == "manager": + return ALL + return [] + objects = UserManager() def __str__(self): diff --git a/admin_api/migrations/0002_rbac_models.py b/admin_api/migrations/0002_rbac_models.py new file mode 100644 index 0000000..0dd309f --- /dev/null +++ b/admin_api/migrations/0002_rbac_models.py @@ -0,0 +1,92 @@ +# Generated by Django 4.2.21 on 2026-03-26 13:42 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('admin_api', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='CustomRole', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('slug', models.SlugField(unique=True)), + ('description', models.TextField(blank=True, default='')), + ('scopes', models.JSONField(default=list)), + ('is_system', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Department', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('slug', models.SlugField(unique=True)), + ('description', models.TextField(blank=True, default='')), + ('base_scopes', models.JSONField(default=list)), + ('color', models.CharField(default='#3B82F6', max_length=7)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Squad', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('extra_scopes', models.JSONField(default=list)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('department', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='squads', to='admin_api.department')), + ('manager', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='managed_squads', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='StaffProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('staff_role', models.CharField(choices=[('SUPER_ADMIN', 'Super Admin'), ('MANAGER', 'Manager'), ('MEMBER', 'Member')], default='MEMBER', max_length=20)), + ('status', models.CharField(choices=[('active', 'Active'), ('invited', 'Invited'), ('deactivated', 'Deactivated')], default='active', max_length=20)), + ('joined_at', models.DateTimeField(auto_now_add=True)), + ('department', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='staff_members', to='admin_api.department')), + ('squad', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='admin_api.squad')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='staff_profile', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['user__first_name'], + }, + ), + migrations.CreateModel( + name='AuditLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('action', models.CharField(max_length=100)), + ('target_type', models.CharField(max_length=50)), + ('target_id', models.CharField(max_length=50)), + ('details', models.JSONField(default=dict)), + ('ip_address', models.GenericIPAddressField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='audit_logs', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/admin_api/models.py b/admin_api/models.py index 72711b0..25bb444 100644 --- a/admin_api/models.py +++ b/admin_api/models.py @@ -63,3 +63,121 @@ class ReviewInteraction(models.Model): def __str__(self): return f'{self.username} {self.interaction_type} on Review #{self.review_id}' + + +# --------------------------------------------------------------------------- +# RBAC Models +# --------------------------------------------------------------------------- +from accounts.models import User + + +class Department(models.Model): + name = models.CharField(max_length=100) + slug = models.SlugField(unique=True) + description = models.TextField(blank=True, default='') + base_scopes = models.JSONField(default=list) + color = models.CharField(max_length=7, default='#3B82F6') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ['name'] + + def __str__(self): + return self.name + + +class Squad(models.Model): + name = models.CharField(max_length=100) + department = models.ForeignKey(Department, on_delete=models.CASCADE, related_name='squads') + manager = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='managed_squads') + extra_scopes = models.JSONField(default=list) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['name'] + + def __str__(self): + return f"{self.department.name} > {self.name}" + + +class StaffProfile(models.Model): + ROLE_CHOICES = [('SUPER_ADMIN', 'Super Admin'), ('MANAGER', 'Manager'), ('MEMBER', 'Member')] + STATUS_CHOICES = [('active', 'Active'), ('invited', 'Invited'), ('deactivated', 'Deactivated')] + + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='staff_profile') + department = models.ForeignKey(Department, on_delete=models.SET_NULL, null=True, blank=True, related_name='staff_members') + squad = models.ForeignKey(Squad, on_delete=models.SET_NULL, null=True, blank=True, related_name='members') + staff_role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='MEMBER') + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active') + joined_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['user__first_name'] + + def get_effective_scopes(self): + if self.staff_role == 'SUPER_ADMIN' or self.user.is_superuser: + return ['*'] + scopes = set() + if self.department: + scopes.update(self.department.base_scopes or []) + if self.squad: + scopes.update(self.squad.extra_scopes or []) + if self.staff_role == 'MANAGER': + scopes.add('settings.staff') + return list(scopes) + + def get_allowed_modules(self): + scopes = self.get_effective_scopes() + if '*' in scopes: + return ['dashboard', 'partners', 'events', 'ad-control', 'users', 'reviews', 'contributions', 'financials', 'settings'] + SCOPE_TO_MODULE = { + 'users': 'users', + 'events': 'events', + 'finance': 'financials', + 'partners': 'partners', + 'tickets': 'dashboard', + 'settings': 'settings', + 'ads': 'ad-control', + 'contributions': 'contributions', + } + modules = {'dashboard'} + for scope in scopes: + prefix = scope.split('.')[0] + if prefix in SCOPE_TO_MODULE: + modules.add(SCOPE_TO_MODULE[prefix]) + return list(modules) + + def __str__(self): + return f"{self.user.username} ({self.staff_role})" + + +class CustomRole(models.Model): + name = models.CharField(max_length=100) + slug = models.SlugField(unique=True) + description = models.TextField(blank=True, default='') + scopes = models.JSONField(default=list) + is_system = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['name'] + + def __str__(self): + return self.name + + +class AuditLog(models.Model): + user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='audit_logs') + action = models.CharField(max_length=100) + target_type = models.CharField(max_length=50) + target_id = models.CharField(max_length=50) + details = models.JSONField(default=dict) + ip_address = models.GenericIPAddressField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return f"{self.action} by {self.user} at {self.created_at}" diff --git a/admin_api/urls.py b/admin_api/urls.py index e50b606..555ac48 100644 --- a/admin_api/urls.py +++ b/admin_api/urls.py @@ -18,6 +18,8 @@ urlpatterns = [ path('partners//', views.PartnerDetailView.as_view(), name='partner-detail'), path('partners//status/', views.PartnerStatusView.as_view(), name='partner-status'), path('partners//kyc/review/', views.PartnerKYCReviewView.as_view(), name='partner-kyc-review'), + path('partners/onboard/', views.PartnerOnboardView.as_view(), name='partner-onboard'), + path('partners//staff/', views.PartnerStaffCreateView.as_view(), name='partner-staff-create'), path('users/metrics/', views.UserMetricsView.as_view(), name='user-metrics'), path('users/', views.UserListView.as_view(), name='user-list'), path('users//', views.UserDetailView.as_view(), name='user-detail'), @@ -41,8 +43,34 @@ urlpatterns = [ path('reviews//moderate/', views.ReviewModerationView.as_view(), name='review-moderate'), path('reviews//', views.ReviewDeleteView.as_view(), name='review-delete'), + path('gamification/submit-event/', views.GamificationSubmitEventView.as_view(), name='gamification-submit-event'), + path('gamification/submit-event', views.GamificationSubmitEventView.as_view()), + path('shop/items/', views.ShopItemsView.as_view(), name='shop-items'), + path('shop/items', views.ShopItemsView.as_view()), + path('shop/redeem/', views.ShopRedeemView.as_view(), name='shop-redeem'), + path('shop/redeem', views.ShopRedeemView.as_view()), + + path('gamification/dashboard/', views.GamificationDashboardView.as_view(), name='gamification-dashboard'), + path('gamification/dashboard', views.GamificationDashboardView.as_view()), + # Payment gateway settings path('settings/payment-gateway/active/', views.ActivePaymentGatewayView.as_view(), name='active-payment-gateway'), path('settings/payment-gateways/', views.PaymentGatewaySettingsView.as_view(), name='payment-gateways'), path('settings/payment-gateways//', views.PaymentGatewaySettingsView.as_view(), name='payment-gateway-detail'), + + # RBAC + path('rbac/departments/', views.DepartmentListCreateView.as_view(), name='rbac-department-list'), + path('rbac/departments//', views.DepartmentDetailView.as_view(), name='rbac-department-detail'), + path('rbac/squads/', views.SquadListCreateView.as_view(), name='rbac-squad-list'), + path('rbac/squads//', views.SquadDetailView.as_view(), name='rbac-squad-detail'), + path('rbac/staff/', views.StaffListView.as_view(), name='rbac-staff-list'), + path('rbac/staff/invite/', views.StaffInviteView.as_view(), name='rbac-staff-invite'), + path('rbac/staff//', views.StaffUpdateView.as_view(), name='rbac-staff-update'), + path('rbac/staff//deactivate/', views.StaffDeactivateView.as_view(), name='rbac-staff-deactivate'), + path('rbac/staff//move/', views.StaffMoveView.as_view(), name='rbac-staff-move'), + path('rbac/roles/', views.RoleListCreateView.as_view(), name='rbac-role-list'), + path('rbac/roles//', views.RoleDetailView.as_view(), name='rbac-role-detail'), + path('rbac/scopes/', views.ScopeListView.as_view(), name='rbac-scope-list'), + path('rbac/org-tree/', views.OrgTreeView.as_view(), name='rbac-org-tree'), + path('rbac/audit-log/', views.AuditLogListView.as_view(), name='rbac-audit-log'), ] \ No newline at end of file diff --git a/admin_api/views.py b/admin_api/views.py index d2bd5cf..1a41d01 100644 --- a/admin_api/views.py +++ b/admin_api/views.py @@ -682,6 +682,8 @@ def _serialize_event(e): 'isFeatured': bool(e.is_featured), 'isTopEvent': bool(e.is_top_event), 'source': e.source or 'eventify', + 'eventTypeId': e.event_type_id, + 'eventTypeName': e.event_type.event_type if e.event_type_id and e.event_type else '', } @@ -740,11 +742,13 @@ class EventListView(APIView): def get(self, request): from events.models import Event from django.db.models import Q - qs = Event.objects.select_related('partner').all() + qs = Event.objects.select_related('partner', 'event_type').all() if s := request.GET.get('status'): reverse_map = {v: k for k, v in _EVENT_STATUS_MAP.items()} backend_status = reverse_map.get(s, s) qs = qs.filter(event_status=backend_status) + if etid := request.GET.get('event_type'): + qs = qs.filter(event_type_id=etid) if pid := request.GET.get('partner_id'): qs = qs.filter(partner_id=pid) if q := request.GET.get('search'): @@ -2238,3 +2242,91 @@ class PartnerStaffCreateView(APIView): }, status=status.HTTP_201_CREATED, ) + + +# ─── Gamification Dashboard (stub) ─────────────────────────────────────────── +class GamificationDashboardView(APIView): + permission_classes = [] # public for now; restrict when auth is wired up + + def get(self, request): + user_id = request.GET.get('user_id', '') + return Response({ + 'status': 'success', + 'profile': { + 'user_id': user_id, + 'current_tier': 'BRONZE', + 'current_ep': 0, + 'current_rp': 0, + 'lifetime_ep': 0, + }, + 'submissions': [], + }) + + +# ─── Gamification: Event Submission (stub) ──────────────────────────────────── +class GamificationSubmitEventView(APIView): + permission_classes = [] + + def post(self, request): + data = request.data + return Response({ + 'status': 'success', + 'submission': { + 'id': 1, + 'event_name': data.get('event_name', ''), + 'status': 'PENDING', + 'total_ep_awarded': 0, + 'created_at': __import__('datetime').datetime.now().isoformat(), + }, + 'message': 'Event submitted for review. You will earn EP once approved!', + }) + + +# ─── Reward Shop: List Items (stub) ────────────────────────────────────────── +class ShopItemsView(APIView): + permission_classes = [] + + def get(self, request): + return Response({ + 'status': 'success', + 'items': [ + { + 'id': 1, + 'name': 'BookMyShow Voucher', + 'description': 'Get a Rs.100 BookMyShow gift card', + 'rp_cost': 50, + 'stock_quantity': 10, + }, + { + 'id': 2, + 'name': 'Event Priority Listing', + 'description': 'Feature your next event at the top for 7 days', + 'rp_cost': 30, + 'stock_quantity': 5, + }, + { + 'id': 3, + 'name': 'Eventify Merch Pack', + 'description': 'Exclusive stickers, badge & notebook', + 'rp_cost': 100, + 'stock_quantity': 3, + }, + ], + }) + + +# ─── Reward Shop: Redeem (stub) ────────────────────────────────────────────── +class ShopRedeemView(APIView): + permission_classes = [] + + def post(self, request): + import uuid + item_id = request.data.get('item_id') + return Response({ + 'status': 'success', + 'voucher': { + 'item_id': item_id, + 'voucher_code_issued': 'EVF-' + uuid.uuid4().hex[:8].upper(), + }, + 'message': 'Reward redeemed successfully!', + }) diff --git a/create_temp_user.py b/create_temp_user.py new file mode 100644 index 0000000..39de325 --- /dev/null +++ b/create_temp_user.py @@ -0,0 +1,25 @@ +import os +import django +import sys + +sys.path.append('/var/www/myproject/eventify_prod') +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eventify.settings') +django.setup() + +from django.contrib.auth import get_user_model +User = get_user_model() + +username = 'support_agent' +password = 'AgentPass123!' +email = 'agent@example.com' + +if User.objects.filter(username=username).exists(): + print(f"User {username} already exists. Resetting password.") + u = User.objects.get(username=username) + u.set_password(password) + u.save() +else: + print(f"Creating user {username}.") + User.objects.create_superuser(username, email, password) + +print("Done.") diff --git a/templates/customer/base_auth.html b/templates/customer/base_auth.html index 179370a..f9c09a2 100644 --- a/templates/customer/base_auth.html +++ b/templates/customer/base_auth.html @@ -16,13 +16,35 @@ *{box-sizing:border-box} body{margin:0;font-family:Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;background:var(--muted);color:#111} .auth-wrapper{display:flex;min-height:100vh} + + /* LEFT PANEL — video */ .auth-left{ + position:relative; width:40%; min-width:320px; - background:linear-gradient(180deg,var(--blue1),var(--blue2)); - color:#fff;padding:48px;display:flex;flex-direction:column;justify-content:center;gap:10px; + overflow:hidden; + color:#fff; + display:flex;flex-direction:column;justify-content:flex-end; } - .brand{font-weight:700;font-size:28px} + .auth-left video{ + position:absolute;inset:0; + width:100%;height:100%; + object-fit:cover; + z-index:0; + } + /* dark gradient overlay for text legibility */ + .auth-left::after{ + content:''; + position:absolute;inset:0; + background:linear-gradient(180deg,rgba(10,20,60,0.35) 0%,rgba(10,20,60,0.72) 100%); + z-index:1; + } + .auth-left-content{ + position:relative;z-index:2; + padding:48px; + display:flex;flex-direction:column;gap:10px; + } + .brand{font-weight:700;font-size:28px;letter-spacing:-0.5px} .auth-left h1{font-size:36px;margin:0} .auth-left p{opacity:.92;margin:0;font-size:16px} @@ -52,10 +74,12 @@ .message.warning{background:#fff7e6;color:#7a4b00} .errorlist{color:#b00020;margin:6px 0 0 0;font-size:13px} - /* responsive */ + /* responsive — mobile: hide video, show compact gradient header */ @media (max-width:900px){ .auth-wrapper{flex-direction:column} - .auth-left{width:100%;min-height:180px;padding:28px;text-align:center} + .auth-left{width:100%;min-height:180px;justify-content:flex-end;} + .auth-left video{display:block;} + .auth-left-content{padding:28px;text-align:center} .auth-right{padding:20px} .auth-card{border-radius:14px;padding:28px} } @@ -64,8 +88,14 @@
-
Eventify
-

{% block left_subtext %}Your events at your fingertips.{% endblock %}

+ +
+
Eventify
+

{% block left_subtext %}Your events at your fingertips.{% endblock %}

+
diff --git a/update_events.py b/update_events.py new file mode 100644 index 0000000..495ab5b --- /dev/null +++ b/update_events.py @@ -0,0 +1,29 @@ +import os +import django +import sys +import datetime + +# Add the project directory to sys.path +sys.path.append('/var/www/myproject/eventify_prod') + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eventify.settings') +django.setup() + +from events.models import Event + +start = datetime.date(2026, 1, 1) +end = datetime.date(2026, 12, 31) + +print(f"Checking for events from {start} to {end}...") + +events = Event.objects.filter(start_date=start, end_date=end) +count = events.count() + +print(f"Found {count} events matching the criteria.") + +if count > 0: + # Update matched events + updated_count = events.update(all_year_event=True) + print(f"Successfully updated {updated_count} events to be 'All Year'.") +else: + print("No events found to update.") diff --git a/user.py b/user.py new file mode 100644 index 0000000..f256990 --- /dev/null +++ b/user.py @@ -0,0 +1,19 @@ +import os +import django + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "eventify.settings") +django.setup() + +from django.contrib.auth import get_user_model +User = get_user_model() + +def make_all_users_admin(): + users = User.objects.all() + for user in users: + user.role = "admin" # assuming role field exists + user.save() + print(f"Updated: {user.username} -> Admin") + +if __name__ == "__main__": + make_all_users_admin() + print("All users updated to admin role!")