19 Commits

Author SHA1 Message Date
a8f12a9d34 docs: add CHANGELOG.md and update README version to 1.6.0
- CHANGELOG.md: full history from 1.0.0 → 1.6.0 (Keep a Changelog format)
- README.md: bump version badge 1.5.0 → 1.6.0, add changelog summary table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 11:03:18 +05:30
35a99c08fd feat: add Eventify ID (EVT-XXXXXXXX) to User model and all APIs
- Add eventify_id CharField (unique, indexed, editable=False) to User
- Auto-generate on save() with charset excluding I/O/0/1 for clarity
- Migration 0012: add field nullable, backfill all existing users, make non-null
- Sync migration 0011 (allowed_modules) pulled from server
- Expose eventify_id in accounts/api.py, partner/api.py serializers
- Expose eventify_id in mobile_api login response (populates localStorage)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 10:26:08 +05:30
291fa40283 feat: add RBAC migrations, user modules, admin API updates, and utility scripts 2026-04-02 04:06:02 +00:00
068d700059 security: fix SMTP credential exposure and auth bypass
- C-1: Move EMAIL_HOST_PASSWORD to os.environ (was hardcoded plaintext)
- C-2: Enable token-user cross-validation in validate_token_and_get_user()
  (compares token.user_id with user.id to prevent impersonation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 19:29:42 +00:00
6ea51e1463 feat: add source field with 3 options, fix EventListAPI fallback, add is_eventify_event to API response
- Event.source field updated: eventify, community, partner (radio select in form)
- EventListAPI: fallback to all events when pincode returns < 6
- EventListAPI: include is_eventify_event and source in serializer
- Admin API: add source to list serializer
- Django admin: source in list_display, list_filter, list_editable
- Event form template: proper radio button rendering for source field

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 11:23:03 +00:00
4cf70b6330 feat: add user search/filter, banned metric, mobile review API, event detail improvements
- admin_api/views.py: Add banned count to UserMetrics, fix server-side search/filter in UserListView
- admin_api/models.py: Add ReviewInteraction model, display_name/is_verified/helpful_count/flag_count to Review
- mobile_api/views/reviews.py: Customer-facing review submit/list/helpful/flag endpoints
- mobile_api/urls.py: Wire review API routes
- mobile_api/views/events.py: Event detail and listing improvements
- Security hardening across API modules
2026-03-26 09:50:03 +00:00
f8c17e2c8d fix: security audit remediation — Django settings + payment gateway API
- ALLOWED_HOSTS: wildcard replaced with explicit domain list (#15)
- CORS_ALLOWED_ORIGINS: added app.eventifyplus.com (#16)
- CSRF_TRUSTED_ORIGINS: added app.eventifyplus.com (#18)
- JWT ACCESS_TOKEN_LIFETIME: 1 day reduced to 30 minutes (#19)
- ROTATE_REFRESH_TOKENS enabled
- SECRET_KEY: removed unsafe fallback, crash on missing env var
- Added ActivePaymentGatewayView for dynamic gateway config (#1, #5, #20)
- Added PaymentGatewaySettingsView CRUD for admin panel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 12:25:11 +00:00
f2134c5529 fix: update admin_api migration dependency to existing events migration
0001_initial was referencing events.0011_dashboard_indexes which no
longer exists as a file on disk (the DB has it applied but the file
was removed). Updated dependency to 0010_merge_20260324_1443 which
is the latest events migration file present, resolving the
NodeNotFoundError on management commands.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:49:15 +05:30
239a84cd76 refactor: migrate users to PostgreSQL, remove SQLite secondary DB
Users have been migrated from eventify-django SQLite to eventify-backend
PostgreSQL. The temporary users_db workaround is no longer needed:

- settings.py: removed users_db SQLite secondary database config
- views.py: removed _user_db()/_user_qs() helpers; user views now query
  the default PostgreSQL directly with plain User.objects.filter()
- docker-compose.yml: SQLite read-only volume mount removed

All 27 users (25 non-superuser customers) now live in PostgreSQL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:43:12 +05:30
1a668c078a fix: read real users from eventify-django SQLite via secondary database
The admin_api was querying eventify-backend's empty PostgreSQL. Real users
live in eventify-django's SQLite (db.sqlite3 on host). Fix:

- settings.py: auto-adds 'users_db' database config when users_db.sqlite3
  is mounted into the container (read-only volume in docker-compose)
- views.py: _user_db() helper selects the correct database alias;
  _user_qs() defers 'partner' field (absent from older SQLite schema)
- UserMetricsView, UserListView, UserDetailView, UserStatusView all use
  _user_qs() so they query the 25 real registered customers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:38:03 +05:30
4274672d25 fix: scope users API to end-users and tag new registrations as customers
- UserListView and UserMetricsView now filter is_superuser=False so only
  end-user accounts appear in the admin Users page (not admin/staff)
- _serialize_user now returns avatarUrl from profile_picture field so the
  grid view renders profile images instead of broken img tags
- RegisterForm and WebRegisterForm now set is_customer=True and
  role='customer' on save so future registrants are correctly classified

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:10:29 +05:30
19b4482057 Phase 7: Reviews Moderation — Review model + migration + 4 admin endpoints (metrics, list, moderate, delete) 2026-03-25 02:46:50 +00:00
81a261f726 Phase 6: Financials & Payouts — 4 new financial endpoints (metrics, transactions, settlements, release) 2026-03-24 19:05:33 +00:00
e26f463bbb docs: beautify README with ASCII banner, badges, API reference, and architecture diagram 2026-03-24 18:47:15 +00:00
8e081c8c24 Phase 5: Events Admin — 4 new event endpoints (stats, list, detail, moderate) 2026-03-24 18:42:15 +00:00
c2175fa58e Phase 4: Users & RBAC — 4 new user endpoints (list, metrics, detail, status) 2026-03-24 18:26:55 +00:00
Ubuntu
0ae42f5757 feat: Phase 3 - Partners API (5 endpoints + 2 helpers)
- GET /api/v1/partners/stats/ - total, active, pendingKyc, highRisk counts
- GET /api/v1/partners/ - paginated list with status/kyc/type/search filters
- GET /api/v1/partners/:id/ - full detail with events, kycDocuments, dealTerms, ledger
- PATCH /api/v1/partners/:id/status/ - suspend/activate partner
- POST /api/v1/partners/:id/kyc/review/ - approve/reject KYC with reason

Helpers: _serialize_partner(), _partner_kyc_docs()
Status/KYC/type mapping: backend snake_case to frontend capitalised values
Risk score derived from kyc_compliance_status (high_risk=80, approved=5, etc.)
All views IsAuthenticated, models imported inside methods
2026-03-24 18:11:33 +00:00
Ubuntu
422002e9ef feat: Phase 1+2 - JWT auth, dashboard metrics API, DB indexes
Phase 1 - JWT Auth Foundation:
- Replace token auth with djangorestframework-simplejwt
- POST /api/v1/admin/auth/login/ - returns access + refresh JWT
- POST /api/v1/auth/refresh/ - JWT refresh
- GET /api/v1/auth/me/ - current admin profile
- GET /api/v1/health/ - DB health check
- Add ledger app to INSTALLED_APPS

Phase 2 - Dashboard Metrics API:
- GET /api/v1/dashboard/metrics/ - revenue, partners, events, tickets
- GET /api/v1/dashboard/revenue/ - 7-day revenue vs payouts chart data
- GET /api/v1/dashboard/activity/ - last 10 platform events feed
- GET /api/v1/dashboard/actions/ - KYC queue, flagged events, pending payouts

DB Indexes (dashboard query optimisation):
- RazorpayTransaction: status, captured_at
- Partner: status, kyc_compliance_status
- Event: event_status, start_date, created_date
- Booking: created_date
- PaymentTransaction: payment_type, payment_transaction_status, payment_transaction_date

Infra:
- Add Dockerfile for eventify-backend container
- Add simplejwt to requirements.txt
- All 4 dashboard views use IsAuthenticated permission class
2026-03-24 17:46:41 +00:00
Ubuntu
91751ac127 feat: add JWT auth foundation - /api/v1/ with admin login, refresh, me, health endpoints
- Add djangorestframework-simplejwt==5.3.1 to requirements-docker.txt
- Configure REST_FRAMEWORK with JWTAuthentication and SIMPLE_JWT settings
- Create admin_api Django app with AdminLoginView, MeView, HealthView
- Wire /api/v1/ routes without touching existing /api/ mobile endpoints
- Resolve pre-existing events migration conflict (0010_merge)
- Superuser admin created for initial authentication

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 14:46:03 +00:00
59 changed files with 4346 additions and 223 deletions

77
CHANGELOG.md Normal file
View File

@@ -0,0 +1,77 @@
# Changelog
All notable changes to the Eventify Backend are documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), versioning follows [Semantic Versioning](https://semver.org/).
---
## [1.6.0] — 2026-04-02
### Added
- **Unique Eventify ID system** (`EVT-XXXXXXXX` format)
- New `eventify_id` field on `User` model — `CharField(max_length=12, unique=True, editable=False, db_index=True)`
- Charset `ABCDEFGHJKLMNPQRSTUVWXYZ23456789` (no ambiguous characters I/O/0/1) giving ~1.78T combinations
- Auto-generated on first `save()` via a 10-attempt retry loop using `secrets.choice()`
- Migration `0012_user_eventify_id`: add nullable → backfill all existing users → make non-null
- `eventify_id` exposed in `accounts/api.py``_partner_user_to_dict()` fields list
- `eventify_id` exposed in `partner/api.py``_user_to_dict()` fields list
- `eventify_id` exposed in `mobile_api/views/user.py``LoginView` response (populates `localStorage.event_user.eventify_id`)
- `eventifyId` exposed in `admin_api/views.py``_serialize_user()` (camelCase for direct TypeScript compatibility)
- Server-side search in `UserListView` now also filters on `eventify_id__icontains`
- Synced migration `0011_user_allowed_modules_alter_user_id` (pulled from server, was missing from local repo)
### Changed
- `accounts/models.py`: merged `allowed_modules` field + `get_allowed_modules()` + `ALL_MODULES` constant from server (previously only existed on server)
---
## [1.5.0] — 2026-03-31
### Added
- `allowed_modules` TextField on `User` model — comma-separated module slug access control
- `get_allowed_modules()` method on `User` — returns list of accessible modules based on role or explicit list
- `ALL_MODULES` class constant listing all platform module slugs
- Migration `0011_user_allowed_modules_alter_user_id`
---
## [1.4.0] — 2026-03-24
### Added
- Partner portal login/logout APIs (`accounts/api.py`) — `PartnerLoginAPI`, `PartnerLogoutAPI`, `PartnerMeAPI`
- `_partner_user_to_dict()` serializer for partner-scoped user data
- Partner CRUD, KYC review, and user management endpoints in `partner/api.py`
---
## [1.3.0] — 2026-03-14
### Changed
- User `id` field changed from `AutoField` to `BigAutoField` (migration `0010_alter_user_id`)
---
## [1.2.0] — 2026-03-10
### Added
- `partner` ForeignKey on `User` model linking users to partners (migration `0009_user_partner`)
- Profile picture upload support (`ImageField`) with `default.png` fallback (migration `00060007`)
---
## [1.1.0] — 2026-02-28
### Added
- Location fields on `User`: `pincode`, `district`, `state`, `country`, `place`, `latitude`, `longitude`
- Custom `UserManager` for programmatic user creation
---
## [1.0.0] — 2026-03-01
### Added
- Initial Django project with custom `User` model extending `AbstractUser`
- Role choices: `admin`, `manager`, `staff`, `customer`, `partner`, `partner_manager`, `partner_staff`, `partner_customer`
- JWT authentication via `djangorestframework-simplejwt`
- Admin API foundation: auth, dashboard metrics, partners, users, events
- Docker + Gunicorn + PostgreSQL 16 production setup

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM python:3.10-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
RUN apt-get update && apt-get install -y gcc libpq-dev && rm -rf /var/lib/apt/lists/*
COPY requirements-docker.txt .
RUN pip install --no-cache-dir -r requirements-docker.txt
COPY . .
RUN python manage.py collectstatic --noinput || true
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "--timeout", "120", "eventify.wsgi:application"]

275
README.md
View File

@@ -1,30 +1,269 @@
# Eventify - Django <div align=center>
This repository contains a production-oriented Django project skeleton for the Eventify application. ```
███████╗██╗ ██╗███████╗███╗ ██╗████████╗██╗███████╗██╗ ██╗
██╔════╝██║ ██║██╔════╝████╗ ██║╚══██╔══╝██║██╔════╝╚██╗ ██╔╝
█████╗ ██║ ██║█████╗ ██╔██╗ ██║ ██║ ██║█████╗ ╚████╔╝
██╔══╝ ╚██╗ ██╔╝██╔══╝ ██║╚██╗██║ ██║ ██║██╔══╝ ╚██╔╝
███████╗ ╚████╔╝ ███████╗██║ ╚████║ ██║ ██║██║ ██║
╚══════╝ ╚═══╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═╝ ╚═╝
A D M I N B A C K E N D
```
## Features ![Version](https://img.shields.io/badge/version-1.6.0-blue?style=for-the-badge)
- Custom `User` model ![Django](https://img.shields.io/badge/Django-4.2-092E20?style=for-the-badge&logo=django&logoColor=white)
- EventType (categories), Event, EventImages models ![DRF](https://img.shields.io/badge/DRF-3.15-red?style=for-the-badge)
- CRUD for EventType, Event, and Users ![Python](https://img.shields.io/badge/Python-3.11-3776AB?style=for-the-badge&logo=python&logoColor=white)
- Bootstrap-based templates and navigation ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-16-4169E1?style=for-the-badge&logo=postgresql&logoColor=white)
- Settings prepared to use environment variables for production ![JWT](https://img.shields.io/badge/JWT-Auth-000000?style=for-the-badge&logo=jsonwebtokens&logoColor=white)
![Docker](https://img.shields.io/badge/Docker-Container-2496ED?style=for-the-badge&logo=docker&logoColor=white)
![License](https://img.shields.io/badge/license-Proprietary-lightgrey?style=for-the-badge)
**Production REST API powering the Eventify Admin Command Center**
[Live Admin →](https://admin.eventifyplus.com) · [API Base →](https://admin.eventifyplus.com/api/v1/) · [Gitea →](https://code.bshtech.net/Sicherhaven/eventify_backend)
</div>
---
## ✦ Overview
Eventify Backend is the **Django 4.2 + Django REST Framework** API layer for the Eventify platform. It powers the admin command center at `admin.eventifyplus.com`, providing JWT-authenticated endpoints for partner management, user CRM, event moderation, financial reporting, and platform analytics.
> Built phase-by-phase as a production rebuild — all endpoints are real, all data is live.
---
## ✦ Tech Stack
| Layer | Technology |
|-------|-----------|
| **Framework** | Django 4.2 + Django REST Framework 3.15 |
| **Auth** | `djangorestframework-simplejwt` — JWT (access 1 day / refresh 7 days) |
| **Database** | PostgreSQL 16 (`event_dashboard` DB) |
| **Runtime** | Python 3.11 · Gunicorn · Docker |
| **Reverse Proxy** | Nginx (host-level, ports 80/443) |
| **SSL** | Let's Encrypt (auto-renew via certbot) |
| **Deployment** | AWS EC2 · Docker Compose · `docker cp` deploy |
| **Source Control** | Gitea self-hosted at `code.bshtech.net` |
---
## ✦ Architecture
```
┌─────────────────────────────────────────────────┐
│ admin.eventifyplus.com │
│ (HTTPS / 443) │
└────────────────────┬────────────────────────────┘
│ Nginx Reverse Proxy
┌─────────┴──────────┐
│ │
/api/* → :3001 /* → :8084
│ │
┌───────┴──────┐ ┌────────┴────────┐
│ eventify- │ │ admin-frontend │
│ backend │ │ (React + Vite) │
│ (Django) │ │ [nginx SPA] │
└───────┬──────┘ └─────────────────┘
┌───────┴──────┐
│ eventify- │
│ postgres │
│ (PG 16) │
└──────────────┘
```
---
## ✦ API Reference
All endpoints are under `/api/v1/` and require `Authorization: Bearer <access_token>` except Auth.
### 🔐 Authentication
```
POST /api/v1/admin/auth/login/ → { access, refresh, user }
POST /api/v1/auth/refresh/ → { access }
GET /api/v1/auth/me/ → { user }
GET /api/v1/health/ → { status, db }
```
### 📊 Dashboard
```
GET /api/v1/dashboard/metrics/ → totalRevenue, revenueGrowth, activePartners, liveEvents, ticketSales
GET /api/v1/dashboard/revenue/ → 7-day revenue vs payouts chart data
GET /api/v1/dashboard/activity/ → recent platform activity feed (top 10)
GET /api/v1/dashboard/actions/ → action items panel (KYC queue, flagged events, payouts)
```
### 🤝 Partners
```
GET /api/v1/partners/stats/ → total, active, pendingKyc, highRisk
GET /api/v1/partners/ → paginated list [ status, kyc_status, search ]
GET /api/v1/partners/:id/ → full partner profile + events + KYC docs
PATCH /api/v1/partners/:id/status/ → { status: active|suspended|archived }
POST /api/v1/partners/:id/kyc/review/ → { decision: approved|rejected, reason? }
```
### 👤 Users
```
GET /api/v1/users/metrics/ → total, active, suspended, newThisWeek
GET /api/v1/users/ → paginated list [ status, role, search ]
GET /api/v1/users/:id/ → user profile
PATCH /api/v1/users/:id/status/ → { action: suspend|ban|reinstate }
```
### 🎪 Events
```
GET /api/v1/events/stats/ → total, live, pending, flagged, published
GET /api/v1/events/ → paginated list [ status, partner_id, search ]
GET /api/v1/events/:id/ → event detail
PATCH /api/v1/events/:id/moderate/ → { action: approve|reject|flag|feature|unfeature }
```
---
## ✦ Project Structure
```
eventify-django/
├── admin_api/ ← All admin REST endpoints (Phases 15)
│ ├── views.py ← Auth + Dashboard + Partners + Users + Events views
│ ├── urls.py ← /api/v1/ URL router
│ └── serializers.py ← UserSerializer
├── accounts/ ← Custom User model (extends AbstractUser)
├── events/ ← Event model + legacy CRUD views
├── partner/ ← Partner model + KYC fields
├── bookings/ ← Booking + Ticket models
├── ledger/ ← RazorpayTransaction model
├── banking_operations/ ← PaymentTransaction model
├── eventify/ ← Django settings + root urls.py
├── requirements-docker.txt ← Production dependencies
└── manage.py
```
---
## ✦ Changelog
> Full history in [CHANGELOG.md](./CHANGELOG.md)
| Version | Date | Summary |
|---------|------|---------|
| **1.6.0** | 2026-04-02 | Unique Eventify ID (`EVT-XXXXXXXX`) on User model, exposed across all APIs |
| **1.5.0** | 2026-03-31 | `allowed_modules` field + `get_allowed_modules()` for RBAC |
| **1.4.0** | 2026-03-24 | Partner portal login/logout/me APIs |
| **1.3.0** | 2026-03-14 | User `id``BigAutoField` |
| **1.0.0** | 2026-03-01 | Initial release — Django + JWT + Admin API |
---
## ✦ Build Phases
| Phase | Module | Endpoints | Status |
|-------|--------|-----------|--------|
| **1** | JWT Auth Foundation | login, refresh, me, health | ✅ Live |
| **2** | Dashboard Metrics | metrics, revenue, activity, actions | ✅ Live |
| **3** | Partners API | stats, list, detail, status, KYC review | ✅ Live |
| **4** | Users & RBAC | metrics, list, detail, status | ✅ Live |
| **5** | Events Admin | stats, list, detail, moderate | ✅ Live |
| **6** | Financials & Payouts | transactions, settlements, payouts | ⏳ Planned |
| **7** | Notifications & Settings | notifications, audit log, system config | ⏳ Planned |
---
## ✦ Local Development
## Quick start (development)
1. Create a virtualenv and activate it
```bash ```bash
# Clone
git clone https://code.bshtech.net/Sicherhaven/eventify_backend.git
cd eventify_backend
# Virtual environment
python -m venv venv python -m venv venv
source venv/bin/activate source venv/bin/activate
pip install -r requirements.txt pip install -r requirements-docker.txt
```
2. Run migrations and create superuser # Environment variables
```bash cp .env.example .env # set DJANGO_SECRET_KEY, DB_* vars
# Database
python manage.py migrate python manage.py migrate
python manage.py createsuperuser python manage.py createsuperuser
# Run
python manage.py runserver python manage.py runserver
``` ```
## Production notes ---
- Set `DJANGO_SECRET_KEY`, `DJANGO_DEBUG`, and `DJANGO_ALLOWED_HOSTS` environment variables
- Collect static files with `python manage.py collectstatic`
- Serve via uWSGI/gunicorn + nginx or any WSGI server
## ✦ Production Deployment
```bash
# Files are deployed via docker cp (no volume mount)
scp admin_api/views.py eventify:/tmp/
ssh eventify docker cp /tmp/views.py eventify-backend:/app/admin_api/views.py
# Reload gunicorn (graceful — no downtime)
ssh eventify docker exec eventify-backend kill -HUP 1
# Verify
ssh eventify curl -s http://localhost:3001/api/v1/health/
```
**Containers:**
| Container | Image | Port | Role |
|-----------|-------|------|------|
| `eventify-backend` | eventify-django | :3001 | Django API |
| `eventify-postgres` | postgres:16-alpine | internal | Database |
| `admin-frontend` | admin-prototype | :8084 | React SPA |
---
## ✦ Environment Variables
| Variable | Description | Example |
|----------|-------------|---------|
| `DJANGO_SECRET_KEY` | Django secret key | `django-insecure-...` |
| `DJANGO_DEBUG` | Debug mode | `False` |
| `DJANGO_ALLOWED_HOSTS` | Allowed hostnames | `admin.eventifyplus.com` |
| `DB_NAME` | PostgreSQL database | `event_dashboard` |
| `DB_USER` | PostgreSQL user | `event_user` |
| `DB_PASSWORD` | PostgreSQL password | — |
| `DB_HOST` | PostgreSQL host | `eventify-postgres` |
| `DB_PORT` | PostgreSQL port | `5432` |
---
## ✦ Status Mappings
### Partner KYC
| Backend | Frontend |
|---------|----------|
| `approved` | `Verified` |
| `rejected` | `Rejected` |
| `pending` / `high_risk` / `medium_risk` | `Pending` |
### Event Status
| Backend | Frontend |
|---------|----------|
| `live` | `live` |
| `published` | `published` |
| `pending` / `created` | `draft` |
| `flagged` | `flagged` |
| `cancelled` / `postponed` | `cancelled` |
| `completed` | `completed` |
---
<div align=center>
**Eventify Admin Backend** · Built by [BSH Technologies](https://bshtechnologies.in)
![Made with Django](https://img.shields.io/badge/Made_with-Django-092E20?style=flat-square&logo=django)
![Hosted on AWS](https://img.shields.io/badge/Hosted_on-AWS_EC2-FF9900?style=flat-square&logo=amazonaws)
![Served via Nginx](https://img.shields.io/badge/Served_via-Nginx-009639?style=flat-square&logo=nginx)
</div>

View File

@@ -17,6 +17,7 @@ def _partner_user_to_dict(user, request=None):
user, user,
fields=[ fields=[
"id", "id",
"eventify_id",
"username", "username",
"email", "email",
"phone_number", "phone_number",
@@ -38,7 +39,7 @@ def _partner_user_to_dict(user, request=None):
# Add profile picture URL if exists # Add profile picture URL if exists
if getattr(user, "profile_picture", None): if getattr(user, "profile_picture", None):
if request: if request:
data["profile_picture"] = request.build_absolute_uri(user.profile_picture.url) data["profile_picture"] = user.profile_picture.url
else: else:
data["profile_picture"] = user.profile_picture.url data["profile_picture"] = user.profile_picture.url
else: else:

View File

@@ -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'),
),
]

View File

@@ -0,0 +1,55 @@
# Generated migration for eventify_id field
import secrets
from django.db import migrations, models
EVENTIFY_ID_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
def generate_unique_eventify_id(existing_ids):
for _ in range(100):
candidate = 'EVT-' + ''.join(secrets.choice(EVENTIFY_ID_CHARS) for _ in range(8))
if candidate not in existing_ids:
return candidate
raise RuntimeError("Could not generate a unique Eventify ID after 100 attempts")
def backfill_eventify_ids(apps, schema_editor):
User = apps.get_model('accounts', 'User')
existing_ids = set(User.objects.exclude(eventify_id__isnull=True).values_list('eventify_id', flat=True))
users_to_update = []
for user in User.objects.filter(eventify_id__isnull=True):
new_id = generate_unique_eventify_id(existing_ids)
existing_ids.add(new_id)
user.eventify_id = new_id
users_to_update.append(user)
User.objects.bulk_update(users_to_update, ['eventify_id'])
def reverse_backfill(apps, schema_editor):
pass # No-op: reversing just leaves IDs set, which is fine
class Migration(migrations.Migration):
dependencies = [
('accounts', '0011_user_allowed_modules_alter_user_id'),
]
operations = [
# Step 1: Add the column as nullable
migrations.AddField(
model_name='user',
name='eventify_id',
field=models.CharField(blank=True, db_index=True, editable=False, max_length=12, null=True, unique=True),
),
# Step 2: Backfill all existing users
migrations.RunPython(backfill_eventify_ids, reverse_backfill),
# Step 3: Make the field non-nullable
migrations.AlterField(
model_name='user',
name='eventify_id',
field=models.CharField(blank=False, db_index=True, editable=False, max_length=12, null=False, unique=True),
),
]

View File

@@ -1,8 +1,18 @@
import secrets
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from accounts.manager import UserManager from accounts.manager import UserManager
from partner.models import Partner from partner.models import Partner
EVENTIFY_ID_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' # no I, O, 0, 1
def generate_eventify_id():
return 'EVT-' + ''.join(secrets.choice(EVENTIFY_ID_CHARS) for _ in range(8))
ROLE_CHOICES = ( ROLE_CHOICES = (
('admin', 'Admin'), ('admin', 'Admin'),
('manager', 'Manager'), ('manager', 'Manager'),
@@ -15,6 +25,15 @@ ROLE_CHOICES = (
) )
class User(AbstractUser): class User(AbstractUser):
eventify_id = models.CharField(
max_length=12,
unique=True,
editable=False,
db_index=True,
null=True,
blank=True,
)
phone_number = models.CharField(max_length=15, blank=True, null=True) phone_number = models.CharField(max_length=15, blank=True, null=True)
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='Staff') role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='Staff')
@@ -37,7 +56,33 @@ class User(AbstractUser):
profile_picture = models.ImageField(upload_to='profile_pictures/', blank=True, null=True, default='default.png') 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() objects = UserManager()
def save(self, *args, **kwargs):
if not self.eventify_id:
for _ in range(10):
candidate = generate_eventify_id()
if not User.objects.filter(eventify_id=candidate).exists():
self.eventify_id = candidate
break
super().save(*args, **kwargs)
def __str__(self): def __str__(self):
return self.username return self.username

0
admin_api/__init__.py Normal file
View File

4
admin_api/apps.py Normal file
View File

@@ -0,0 +1,4 @@
from django.apps import AppConfig
class AdminApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'admin_api'

View File

@@ -0,0 +1,36 @@
# Generated by Django 4.2.21 on 2026-03-25 02:17
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('events', '0010_merge_20260324_1443'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Review',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('rating', models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)])),
('review_text', models.TextField()),
('submission_date', models.DateTimeField(auto_now_add=True)),
('status', models.CharField(choices=[('pending', 'Pending'), ('live', 'Live'), ('rejected', 'Rejected')], default='pending', max_length=10)),
('reject_reason', models.CharField(blank=True, choices=[('spam', 'Spam'), ('inappropriate', 'Inappropriate'), ('fake', 'Fake')], max_length=15, null=True)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='admin_reviews', to='events.event')),
('reviewer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='admin_reviews', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-submission_date'],
'indexes': [models.Index(fields=['status'], name='admin_api_r_status_2f6c07_idx'), models.Index(fields=['submission_date'], name='admin_api_r_submiss_02d0c1_idx')],
},
),
]

View File

@@ -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'],
},
),
]

View File

183
admin_api/models.py Normal file
View File

@@ -0,0 +1,183 @@
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
class Review(models.Model):
STATUS_PENDING = 'pending'
STATUS_LIVE = 'live'
STATUS_REJECTED = 'rejected'
STATUS_CHOICES = [
(STATUS_PENDING, 'Pending'),
(STATUS_LIVE, 'Live'),
(STATUS_REJECTED, 'Rejected'),
]
REJECT_CHOICES = [
('spam', 'Spam'),
('inappropriate', 'Inappropriate'),
('fake', 'Fake'),
]
reviewer = models.ForeignKey(
'accounts.User', on_delete=models.CASCADE, related_name='admin_reviews'
)
event = models.ForeignKey(
'events.Event', on_delete=models.CASCADE, related_name='admin_reviews'
)
rating = models.IntegerField(
validators=[MinValueValidator(1), MaxValueValidator(5)]
)
review_text = models.TextField()
submission_date = models.DateTimeField(auto_now_add=True)
status = models.CharField(
max_length=10, choices=STATUS_CHOICES, default=STATUS_PENDING
)
reject_reason = models.CharField(
max_length=15, choices=REJECT_CHOICES, null=True, blank=True
)
display_name = models.CharField(max_length=100, blank=True, default='')
is_verified = models.BooleanField(default=False)
helpful_count = models.IntegerField(default=0)
flag_count = models.IntegerField(default=0)
class Meta:
ordering = ['-submission_date']
indexes = [
models.Index(fields=['status']),
models.Index(fields=['submission_date']),
]
def __str__(self):
return f'Review #{self.pk} by {self.reviewer_id}{self.status}'
class ReviewInteraction(models.Model):
INTERACTION_CHOICES = [('HELPFUL', 'Helpful'), ('FLAG', 'Flag')]
review = models.ForeignKey(Review, on_delete=models.CASCADE, related_name='interactions')
username = models.CharField(max_length=255)
interaction_type = models.CharField(max_length=20, choices=INTERACTION_CHOICES)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('review', 'username', 'interaction_type')
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}"

18
admin_api/serializers.py Normal file
View File

@@ -0,0 +1,18 @@
from rest_framework import serializers
from django.contrib.auth import get_user_model
User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField()
role = serializers.SerializerMethodField()
class Meta:
model = User
fields = ['id', 'email', 'username', 'name', 'role']
def get_name(self, obj):
return f"{obj.first_name} {obj.last_name}".strip() or obj.username
def get_role(self, obj):
if obj.is_superuser:
return 'superadmin'
if obj.is_staff:
return 'admin'
return getattr(obj, 'role', 'user')

76
admin_api/urls.py Normal file
View File

@@ -0,0 +1,76 @@
from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView
from . import views
urlpatterns = [
path('admin/auth/login/', views.AdminLoginView.as_view(), name='admin_login'),
path('auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('auth/me/', views.MeView.as_view(), name='auth_me'),
path('health/', views.HealthView.as_view(), name='health'),
# Phase 2: Dashboard endpoints
path('dashboard/metrics/', views.DashboardMetricsView.as_view(), name='dashboard-metrics'),
path('dashboard/revenue/', views.DashboardRevenueView.as_view(), name='dashboard-revenue'),
path('dashboard/activity/', views.DashboardActivityView.as_view(), name='dashboard-activity'),
path('dashboard/actions/', views.DashboardActionsView.as_view(), name='dashboard-actions'),
# Phase 3: Partner endpoints
path('partners/stats/', views.PartnerStatsView.as_view(), name='partner-stats'),
path('partners/', views.PartnerListView.as_view(), name='partner-list'),
path('partners/<int:pk>/', views.PartnerDetailView.as_view(), name='partner-detail'),
path('partners/<int:pk>/status/', views.PartnerStatusView.as_view(), name='partner-status'),
path('partners/<int:pk>/kyc/review/', views.PartnerKYCReviewView.as_view(), name='partner-kyc-review'),
path('partners/onboard/', views.PartnerOnboardView.as_view(), name='partner-onboard'),
path('partners/<int:partner_id>/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/<int:pk>/', views.UserDetailView.as_view(), name='user-detail'),
path('users/<int:pk>/status/', views.UserStatusView.as_view(), name='user-status'),
# Phase 5: Events endpoints
path('events/stats/', views.EventStatsView.as_view(), name='event-stats'),
path('events/', views.EventListView.as_view(), name='event-list'),
path('events/<int:pk>/', views.EventDetailView.as_view(), name='event-detail'),
path('events/<int:pk>/update/', views.EventUpdateView.as_view(), name='event-update'),
path('events/<int:pk>/moderate/', views.EventModerationView.as_view(), name='event-moderate'),
path('events/create/', views.EventCreateView.as_view(), name='event-create'),
path('events/types/', views.EventTypesView.as_view(), name='event-types'),
path('events/<int:pk>/primary-image/', views.EventPrimaryImageView.as_view(), name='event-primary-image'),
path('financials/metrics/', views.FinancialMetricsView.as_view(), name='financial-metrics'),
path('financials/transactions/', views.TransactionListView.as_view(), name='transaction-list'),
path('financials/settlements/', views.SettlementListView.as_view(), name='settlement-list'),
path('financials/settlements/<int:pk>/release/', views.SettlementReleaseView.as_view(), name='settlement-release'),
path('reviews/metrics/', views.ReviewMetricsView.as_view(), name='review-metrics'),
path('reviews/', views.ReviewListView.as_view(), name='review-list'),
path('reviews/<int:pk>/moderate/', views.ReviewModerationView.as_view(), name='review-moderate'),
path('reviews/<int:pk>/', 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/<int:pk>/', views.PaymentGatewaySettingsView.as_view(), name='payment-gateway-detail'),
# RBAC
path('rbac/departments/', views.DepartmentListCreateView.as_view(), name='rbac-department-list'),
path('rbac/departments/<int:pk>/', views.DepartmentDetailView.as_view(), name='rbac-department-detail'),
path('rbac/squads/', views.SquadListCreateView.as_view(), name='rbac-squad-list'),
path('rbac/squads/<int:pk>/', 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/<int:pk>/', views.StaffUpdateView.as_view(), name='rbac-staff-update'),
path('rbac/staff/<int:pk>/deactivate/', views.StaffDeactivateView.as_view(), name='rbac-staff-deactivate'),
path('rbac/staff/<int:pk>/move/', views.StaffMoveView.as_view(), name='rbac-staff-move'),
path('rbac/roles/', views.RoleListCreateView.as_view(), name='rbac-role-list'),
path('rbac/roles/<int:pk>/', 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'),
]

2332
admin_api/views.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -34,7 +34,7 @@ def _payment_gateway_to_dict(gateway, request=None):
# Add logo URL if exists # Add logo URL if exists
if gateway.payment_gateway_logo: if gateway.payment_gateway_logo:
if request: if request:
data["payment_gateway_logo"] = request.build_absolute_uri(gateway.payment_gateway_logo.url) data["payment_gateway_logo"] = gateway.payment_gateway_logo.url
else: else:
data["payment_gateway_logo"] = gateway.payment_gateway_logo.url data["payment_gateway_logo"] = gateway.payment_gateway_logo.url
else: else:

View File

@@ -44,7 +44,7 @@ class PaymentGatewayCredentials(models.Model):
class PaymentTransaction(models.Model): class PaymentTransaction(models.Model):
payment_transaction_id = models.CharField(max_length=250) payment_transaction_id = models.CharField(max_length=250)
payment_type = models.CharField(max_length=250, choices=[ payment_type = models.CharField(max_length=250, db_index=True, choices=[
('credit', 'Credit'), ('credit', 'Credit'),
('debit', 'Debit'), ('debit', 'Debit'),
('transfer', 'Transfer'), ('transfer', 'Transfer'),
@@ -58,14 +58,14 @@ class PaymentTransaction(models.Model):
payment_gateway = models.ForeignKey(PaymentGateway, on_delete=models.CASCADE) payment_gateway = models.ForeignKey(PaymentGateway, on_delete=models.CASCADE)
payment_transaction_amount = models.DecimalField(max_digits=10, decimal_places=2) payment_transaction_amount = models.DecimalField(max_digits=10, decimal_places=2)
payment_transaction_currency = models.CharField(max_length=10) payment_transaction_currency = models.CharField(max_length=10)
payment_transaction_status = models.CharField(max_length=250, choices=[ payment_transaction_status = models.CharField(max_length=250, db_index=True, choices=[
('pending', 'Pending'), ('pending', 'Pending'),
('completed', 'Completed'), ('completed', 'Completed'),
('failed', 'Failed'), ('failed', 'Failed'),
('refunded', 'Refunded'), ('refunded', 'Refunded'),
('cancelled', 'Cancelled'), ('cancelled', 'Cancelled'),
]) ])
payment_transaction_date = models.DateField(auto_now_add=True) payment_transaction_date = models.DateField(auto_now_add=True, db_index=True)
payment_transaction_time = models.TimeField(auto_now_add=True) payment_transaction_time = models.TimeField(auto_now_add=True)
payment_transaction_notes = models.TextField(blank=True, null=True) payment_transaction_notes = models.TextField(blank=True, null=True)
payment_transaction_raw_data = models.JSONField(blank=True, null=True) payment_transaction_raw_data = models.JSONField(blank=True, null=True)

View File

@@ -61,7 +61,7 @@ class Booking(models.Model):
ticket_type = models.ForeignKey(TicketType, on_delete=models.CASCADE) ticket_type = models.ForeignKey(TicketType, on_delete=models.CASCADE)
quantity = models.IntegerField() quantity = models.IntegerField()
price = models.DecimalField(max_digits=10, decimal_places=2) price = models.DecimalField(max_digits=10, decimal_places=2)
created_date = models.DateField(auto_now_add=True) created_date = models.DateField(auto_now_add=True, db_index=True)
updated_date = models.DateField(auto_now=True) updated_date = models.DateField(auto_now=True)
transaction_id = models.CharField(max_length=250, blank=True, null=True) transaction_id = models.CharField(max_length=250, blank=True, null=True)

25
create_temp_user.py Normal file
View File

@@ -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.")

View File

@@ -3,16 +3,23 @@ from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'change-me-in-production') SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
# DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True' # DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True'
# #
# ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '*').split(',') # ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '*').split(',')
DEBUG = True DEBUG = False
ALLOWED_HOSTS = [ ALLOWED_HOSTS = [
'*' 'db.eventifyplus.com',
'uat.eventifyplus.com',
'em.eventifyplus.com',
'backend.eventifyplus.com',
'admin.eventifyplus.com',
'app.eventifyplus.com',
'localhost',
'127.0.0.1',
] ]
INSTALLED_APPS = [ INSTALLED_APPS = [
@@ -33,7 +40,11 @@ INSTALLED_APPS = [
'bookings', 'bookings',
'banking_operations', 'banking_operations',
'rest_framework', 'rest_framework',
'rest_framework.authtoken' 'rest_framework.authtoken',
'rest_framework_simplejwt',
'admin_api',
'django_summernote',
'ledger',
] ]
INSTALLED_APPS += [ INSTALLED_APPS += [
@@ -54,10 +65,20 @@ MIDDLEWARE = [
] ]
CORS_ALLOWED_ORIGINS = [ CORS_ALLOWED_ORIGINS = [
"https://app.eventifyplus.com",
"https://admin.eventifyplus.com",
"https://uat.eventifyplus.com",
"http://localhost:5178",
"http://localhost:5179",
"http://localhost:5173", "http://localhost:5173",
"http://localhost:3001",
"http://localhost:3000",
"https://prototype.eventifyplus.com", "https://prototype.eventifyplus.com",
"https://eventifyplus.com", "https://eventifyplus.com",
"https://mv.eventifyplus.com" "https://mv.eventifyplus.com",
"https://db.eventifyplus.com",
"https://test.eventifyplus.com",
"https://em.eventifyplus.com"
] ]
ROOT_URLCONF = 'eventify.urls' ROOT_URLCONF = 'eventify.urls'
@@ -82,8 +103,12 @@ WSGI_APPLICATION = 'eventify.wsgi.application'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': os.environ.get('DB_ENGINE', 'django.db.backends.sqlite3'),
'NAME': BASE_DIR / 'db.sqlite3', 'NAME': os.environ.get('DB_NAME', str(BASE_DIR / 'db.sqlite3')),
'USER': os.environ.get('DB_USER', ''),
'PASSWORD': os.environ.get('DB_PASS', ''),
'HOST': os.environ.get('DB_HOST', ''),
'PORT': os.environ.get('DB_PORT', '5432'),
} }
} }
@@ -92,7 +117,6 @@ DATABASES = {
# 'ENGINE': 'django.db.backends.postgresql', # 'ENGINE': 'django.db.backends.postgresql',
# 'NAME': 'eventify_uat_db', # your DB name # 'NAME': 'eventify_uat_db', # your DB name
# 'USER': 'eventify_uat', # your DB user # 'USER': 'eventify_uat', # your DB user
# 'PASSWORD': 'eventifyplus@!@#$', # your DB password
# 'HOST': '0.0.0.0', # or IP/domain # 'HOST': '0.0.0.0', # or IP/domain
# 'PORT': '5440', # default PostgreSQL port # 'PORT': '5440', # default PostgreSQL port
# } # }
@@ -118,11 +142,54 @@ STATICFILES_DIRS = [BASE_DIR / 'static']
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media' MEDIA_ROOT = BASE_DIR / 'media'
X_FRAME_OPTIONS = 'SAMEORIGIN'
AUTH_USER_MODEL = 'accounts.User' AUTH_USER_MODEL = 'accounts.User'
LOGIN_URL = 'login' LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = 'dashboard' LOGIN_REDIRECT_URL = 'dashboard'
LOGOUT_REDIRECT_URL = 'login' LOGOUT_REDIRECT_URL = 'login'
# EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# DEFAULT_FROM_EMAIL = 'no-reply@example.com' EMAIL_HOST = 'mail.bshtech.net'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'no-reply@eventifyplus.com'
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', '')
DEFAULT_FROM_EMAIL = 'Eventify <no-reply@eventifyplus.com>'
SUMMERNOTE_THEME = 'bs5'
# Reverse proxy / CSRF fix
CSRF_TRUSTED_ORIGINS = [
'https://app.eventifyplus.com',
'https://admin.eventifyplus.com',
'https://db.eventifyplus.com',
'https://uat.eventifyplus.com',
'https://test.eventifyplus.com',
'https://eventifyplus.com',
]
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
USE_X_FORWARDED_HOST = True
# --- JWT Auth (Phase 1) ---
from datetime import timedelta
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), # Reduced from 1 day for security
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'AUTH_HEADER_TYPES': ('Bearer',),
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
}

View File

@@ -35,7 +35,10 @@ urlpatterns = [
path('partner/', include('partner.urls')), path('partner/', include('partner.urls')),
path('banking/', include('banking_operations.urls')), path('banking/', include('banking_operations.urls')),
path('api/', include('mobile_api.urls')), path('api/', include('mobile_api.urls')),
path('api/v1/', include('admin_api.urls')),
# path('web-api/', include('web_api.urls')), # path('web-api/', include('web_api.urls')),
path('summernote/', include('django_summernote.urls')),
] ]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -3,9 +3,9 @@ from .models import Event, EventImages
@admin.register(Event) @admin.register(Event)
class EventAdmin(admin.ModelAdmin): class EventAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'start_date', 'end_date', 'event_type', 'event_status', 'is_featured', 'is_top_event') list_display = ('id', 'name', 'start_date', 'end_date', 'event_type', 'event_status', 'source', 'is_featured', 'is_top_event')
list_filter = ('event_status', 'event_type', 'is_featured', 'is_top_event') list_filter = ('event_status', 'event_type', 'source', 'is_featured', 'is_top_event')
list_editable = ('is_featured', 'is_top_event') list_editable = ('is_featured', 'is_top_event', 'source')
search_fields = ('name', 'place', 'district') search_fields = ('name', 'place', 'district')
@admin.register(EventImages) @admin.register(EventImages)

View File

@@ -45,7 +45,7 @@ def _event_to_dict(event, request=None):
} }
if event.event_type.event_type_icon: if event.event_type.event_type_icon:
if request: if request:
data["event_type"]["event_type_icon"] = request.build_absolute_uri(event.event_type.event_type_icon.url) data["event_type"]["event_type_icon"] = event.event_type.event_type_icon.url
else: else:
data["event_type"]["event_type_icon"] = event.event_type.event_type_icon.url data["event_type"]["event_type_icon"] = event.event_type.event_type_icon.url
else: else:

View File

@@ -36,9 +36,13 @@ class EventForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Set source to 'official' only and hide the field # Show source as visible radio buttons with Bootstrap styling
self.fields['source'].initial = 'official' self.fields['source'].widget = forms.RadioSelect(
self.fields['source'].widget = forms.HiddenInput() choices=self.fields['source'].choices,
attrs={'class': 'form-check-input'}
)
if not self.instance.pk:
self.fields['source'].initial = 'eventify'
# Check if all_year_event is True (from instance or initial data) # Check if all_year_event is True (from instance or initial data)
all_year_event = False all_year_event = False
@@ -60,8 +64,7 @@ class EventForm(forms.ModelForm):
cleaned_data = super().clean() cleaned_data = super().clean()
all_year_event = cleaned_data.get('all_year_event', False) all_year_event = cleaned_data.get('all_year_event', False)
# Force source to be 'official' only # Source is now user-selectable (eventify/community/partner)
cleaned_data['source'] = 'official'
# If all_year_event is True, clear date/time fields # If all_year_event is True, clear date/time fields
if all_year_event: if all_year_event:

View File

@@ -0,0 +1,14 @@
# Generated by Django 4.2.21 on 2026-03-24 14:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('events', '0007_add_is_featured_is_top_event'),
('events', '0009_alter_event_id_alter_eventimages_id'),
]
operations = [
]

View File

@@ -0,0 +1,48 @@
# Generated by Django 4.2.21 on 2025-11-26 22:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('master_data', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Event',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_date', models.DateField(auto_now_add=True)),
('name', models.CharField(max_length=200)),
('description', models.TextField()),
('start_date', models.DateField()),
('end_date', models.DateField()),
('latitude', models.DecimalField(decimal_places=6, max_digits=9)),
('longitude', models.DecimalField(decimal_places=6, max_digits=9)),
('pincode', models.CharField(max_length=10)),
('district', models.CharField(max_length=100)),
('state', models.CharField(max_length=100)),
('place', models.CharField(max_length=200)),
('is_bookable', models.BooleanField(default=False)),
('is_eventify_event', models.BooleanField(default=True)),
('outside_event_url', models.URLField(default='NA')),
('event_status', models.CharField(choices=[('created', 'Created'), ('cancelled', 'Cancelled'), ('pending', 'Pending'), ('completed', 'Completed'), ('postponed', 'Postponed')], default='pending', max_length=250)),
('cancelled_reason', models.TextField(default='NA')),
('event_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='master_data.eventtype')),
],
),
migrations.CreateModel(
name='EventImages',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_primary', models.BooleanField(default=False)),
('event_image', models.ImageField(upload_to='event_images')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.event')),
],
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 4.2.21 on 2025-11-28 20:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='event',
name='important_information',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='event',
name='title',
field=models.CharField(blank=True, max_length=250),
),
migrations.AddField(
model_name='event',
name='venue_name',
field=models.CharField(blank=True, max_length=250),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.2.21 on 2025-11-28 21:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0002_event_important_information_event_title_and_more'),
]
operations = [
migrations.AddField(
model_name='event',
name='end_time',
field=models.TimeField(blank=True, null=True),
),
migrations.AddField(
model_name='event',
name='start_time',
field=models.TimeField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.0 on 2025-12-19 22:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0003_event_end_time_event_start_time'),
]
operations = [
migrations.AddField(
model_name='event',
name='all_year_event',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='event',
name='end_date',
field=models.DateField(blank=True, null=True),
),
migrations.AlterField(
model_name='event',
name='start_date',
field=models.DateField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0 on 2025-12-19 22:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0004_event_all_year_event_alter_event_end_date_and_more'),
]
operations = [
migrations.AddField(
model_name='event',
name='source',
field=models.CharField(blank=True, choices=[('eventify', 'Eventify'), ('community', 'Community')], max_length=250),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0 on 2025-12-19 22:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0005_event_source'),
]
operations = [
migrations.AlterField(
model_name='event',
name='source',
field=models.CharField(blank=True, choices=[('official', 'Official'), ('community', 'Community')], max_length=250),
),
]

View File

@@ -0,0 +1,21 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0006_alter_event_source'),
]
operations = [
migrations.AddField(
model_name='event',
name='is_featured',
field=models.BooleanField(default=False, help_text='Show this event in the featured section'),
),
migrations.AddField(
model_name='event',
name='is_top_event',
field=models.BooleanField(default=False, help_text='Show this event in the Top Events section'),
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 4.2.27 on 2026-03-13 16:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0006_alter_event_source'),
]
operations = [
migrations.AddField(
model_name='event',
name='gst_percentage_1',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='event',
name='gst_percentage_2',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='event',
name='include_gst',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='event',
name='event_status',
field=models.CharField(choices=[('created', 'Created'), ('cancelled', 'Cancelled'), ('pending', 'Pending'), ('completed', 'Completed'), ('postponed', 'Postponed'), ('published', 'Published'), ('live', 'Live'), ('flagged', 'Flagged')], default='pending', max_length=250),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 4.2.27 on 2026-03-14 15:57
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('partner', '0001_initial'),
('events', '0007_event_gst_percentage_1_event_gst_percentage_2_and_more'),
]
operations = [
migrations.AddField(
model_name='event',
name='is_partner_event',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='event',
name='partner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='partner.partner'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 6.0.3 on 2026-03-14 19:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0008_event_is_partner_event_event_partner'),
]
operations = [
migrations.AlterField(
model_name='event',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='eventimages',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -0,0 +1,14 @@
# Generated by Django 4.2.21 on 2026-03-24 14:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('events', '0007_add_is_featured_is_top_event'),
('events', '0009_alter_event_id_alter_eventimages_id'),
]
operations = [
]

View File

@@ -0,0 +1,43 @@
# Generated by Django 4.2.21 on 2026-03-30 10:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0010_merge_20260324_1443'),
]
operations = [
migrations.AlterField(
model_name='event',
name='created_date',
field=models.DateField(auto_now_add=True, db_index=True),
),
migrations.AlterField(
model_name='event',
name='event_status',
field=models.CharField(choices=[('created', 'Created'), ('cancelled', 'Cancelled'), ('pending', 'Pending'), ('completed', 'Completed'), ('postponed', 'Postponed'), ('published', 'Published'), ('live', 'Live'), ('flagged', 'Flagged')], db_index=True, default='pending', max_length=250),
),
migrations.AlterField(
model_name='event',
name='id',
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='event',
name='source',
field=models.CharField(choices=[('eventify', 'Added by Eventify'), ('community', 'Community Contribution'), ('partner', 'Partner Event')], default='eventify', max_length=50),
),
migrations.AlterField(
model_name='event',
name='start_date',
field=models.DateField(blank=True, db_index=True, null=True),
),
migrations.AlterField(
model_name='eventimages',
name='id',
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

View File

@@ -5,10 +5,10 @@ from partner.models import Partner
class Event(models.Model): class Event(models.Model):
created_date = models.DateField(auto_now_add=True) created_date = models.DateField(auto_now_add=True, db_index=True)
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
description = models.TextField() description = models.TextField()
start_date = models.DateField(blank=True, null=True) start_date = models.DateField(blank=True, null=True, db_index=True)
end_date = models.DateField(blank=True, null=True) end_date = models.DateField(blank=True, null=True)
start_time = models.TimeField(blank=True, null=True) start_time = models.TimeField(blank=True, null=True)
end_time = models.TimeField(blank=True, null=True) end_time = models.TimeField(blank=True, null=True)
@@ -42,16 +42,17 @@ class Event(models.Model):
('published', 'Published'), ('published', 'Published'),
('live', 'Live'), ('live', 'Live'),
('flagged', 'Flagged'), ('flagged', 'Flagged'),
], default='pending') ], default='pending', db_index=True)
cancelled_reason = models.TextField(default='NA') cancelled_reason = models.TextField(default='NA')
title = models.CharField(max_length=250, blank=True) title = models.CharField(max_length=250, blank=True)
important_information = models.TextField(blank=True) important_information = models.TextField(blank=True)
venue_name = models.CharField(max_length=250, blank=True) venue_name = models.CharField(max_length=250, blank=True)
source = models.CharField(max_length=250, blank=True, choices=[ source = models.CharField(max_length=50, default='eventify', choices=[
('official', 'Official'), ('eventify', 'Added by Eventify'),
('community', 'Community'), ('community', 'Community Contribution'),
('partner', 'Partner Event'),
]) ])
is_featured = models.BooleanField(default=False, help_text='Show this event in the featured section') is_featured = models.BooleanField(default=False, help_text='Show this event in the featured section')

View File

@@ -1,5 +1,6 @@
from django.views import generic from django.views import generic
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.db.models import Q
from .models import Event from .models import Event
from .models import EventImages from .models import EventImages
from .forms import EventForm from .forms import EventForm
@@ -18,6 +19,17 @@ class EventListView(LoginRequiredMixin, generic.ListView):
template_name = 'events/event_list.html' template_name = 'events/event_list.html'
paginate_by = 10 paginate_by = 10
def get_queryset(self):
queryset = super().get_queryset()
query = self.request.GET.get('q')
if query:
queryset = queryset.filter(
Q(name__icontains=query) |
Q(district__icontains=query) |
Q(state__icontains=query)
)
return queryset
class EventCreateView(LoginRequiredMixin, generic.CreateView): class EventCreateView(LoginRequiredMixin, generic.CreateView):
model = Event model = Event
@@ -91,5 +103,3 @@ def delete_event_image(request, pk, img_id):
image.delete() image.delete()
messages.success(request, "Image deleted!") messages.success(request, "Image deleted!")
return redirect("events:event_images", pk=pk) return redirect("events:event_images", pk=pk)

View File

@@ -36,6 +36,7 @@ class RazorpayTransaction(models.Model):
status = models.CharField( status = models.CharField(
max_length=50, max_length=50,
help_text="created/authorized/captured/failed/refunded", help_text="created/authorized/captured/failed/refunded",
db_index=True,
) )
method = models.CharField( method = models.CharField(
max_length=50, max_length=50,
@@ -59,7 +60,7 @@ class RazorpayTransaction(models.Model):
# Timestamps # Timestamps
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
captured_at = models.DateTimeField(blank=True, null=True) captured_at = models.DateTimeField(blank=True, null=True, db_index=True)
def __str__(self): def __str__(self):
return f"{self.razorpay_payment_id or self.razorpay_order_id} - {self.status}" return f"{self.razorpay_payment_id or self.razorpay_order_id} - {self.status}"

View File

@@ -31,6 +31,9 @@ class RegisterForm(forms.ModelForm):
# Set username equal to email to avoid separate username errors # Set username equal to email to avoid separate username errors
user.username = self.cleaned_data['email'] user.username = self.cleaned_data['email']
user.set_password(self.cleaned_data['password']) user.set_password(self.cleaned_data['password'])
# Mark as a customer / end-user
user.is_customer = True
user.role = 'customer'
if commit: if commit:
user.save() user.save()
return user return user
@@ -70,9 +73,9 @@ class WebRegisterForm(forms.ModelForm):
# Set username equal to email to avoid separate username errors # Set username equal to email to avoid separate username errors
user.username = self.cleaned_data['email'] user.username = self.cleaned_data['email']
user.set_password(self.cleaned_data['password']) user.set_password(self.cleaned_data['password'])
print('*' * 100) # Mark as a customer / end-user
print(user.username) user.is_customer = True
print('*' * 100) user.role = 'customer'
if commit: if commit:
user.save() user.save()
return user return user

View File

@@ -1,5 +1,6 @@
from django.urls import path from django.urls import path
from .views import * from .views import *
from mobile_api.views.reviews import ReviewSubmitView, MobileReviewListView, ReviewHelpfulView, ReviewFlagView
# Customer URLS # Customer URLS
@@ -24,3 +25,11 @@ urlpatterns += [
path('events/featured-events/', FeaturedEventsAPI.as_view(), name='featured_events'), path('events/featured-events/', FeaturedEventsAPI.as_view(), name='featured_events'),
path('events/top-events/', TopEventsAPI.as_view(), name='top_events'), path('events/top-events/', TopEventsAPI.as_view(), name='top_events'),
] ]
# Review URLs
urlpatterns += [
path('reviews/submit', ReviewSubmitView.as_view()),
path('reviews/list', MobileReviewListView.as_view()),
path('reviews/helpful', ReviewHelpfulView.as_view()),
path('reviews/flag', ReviewFlagView.as_view()),
]

View File

@@ -80,13 +80,13 @@ def validate_token_and_get_user(request, error_status_code=None):
status=status status=status
)) ))
# Verify username matches token user # Verify token belongs to this user
# if user.username != username: if token.user_id != user.id:
# status = 401 if error_status_code else None status = 401 if error_status_code else None
# return (None, None, None, JsonResponse( return (None, None, None, JsonResponse(
# {"status": "error", "message": "token does not match user"}, {"status": "error", "message": "token does not match user"},
# status=status status=status
# )) ))
# Success - return user, token, data, and None for error_response # Success - return user, token, data, and None for error_response
return (user, token, data, None) return (user, token, data, None)

View File

@@ -1,2 +1,3 @@
from .user import * from .user import *
from .events import * from .events import *
from .reviews import *

View File

@@ -1,7 +1,8 @@
import json
from django.http import JsonResponse from django.http import JsonResponse
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.authentication import TokenAuthentication from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated, AllowAny
from events.models import Event, EventImages from events.models import Event, EventImages
from master_data.models import EventType from master_data.models import EventType
from django.forms.models import model_to_dict from django.forms.models import model_to_dict
@@ -15,93 +16,126 @@ from mobile_api.utils import validate_token_and_get_user
@method_decorator(csrf_exempt, name='dispatch') @method_decorator(csrf_exempt, name='dispatch')
class EventTypeListAPIView(APIView): class EventTypeListAPIView(APIView):
permission_classes = [AllowAny]
def post(self, request): def post(self, request):
try: try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
# Fetch event types manually without serializer
event_types_queryset = EventType.objects.all() event_types_queryset = EventType.objects.all()
event_types = [] event_types = []
for event_type in event_types_queryset: for event_type in event_types_queryset:
event_type_data = { event_type_data = {
"id": event_type.id, "id": event_type.id,
"event_type": event_type.event_type, "event_type": event_type.event_type,
"event_type_icon": request.build_absolute_uri(event_type.event_type_icon.url) if event_type.event_type_icon else None "event_type_icon": event_type.event_type_icon.url if event_type.event_type_icon else None
} }
event_types.append(event_type_data) event_types.append(event_type_data)
return JsonResponse({"status": "success", "event_types": event_types})
print(event_types)
return JsonResponse({
"status": "success",
"event_types": event_types
})
except Exception as e: except Exception as e:
return JsonResponse( return JsonResponse({"status": "error", "message": str(e)})
{"status": "error", "message": str(e)},
)
class EventListAPI(APIView): class EventListAPI(APIView):
permission_classes = [AllowAny]
@staticmethod
def _serialize_event(e, thumb_map):
"""Slim serialization for list views — only fields the Flutter app uses."""
img = thumb_map.get(e.id)
lat = e.latitude
lng = e.longitude
desc = e.description or ''
return {
'id': e.id,
'name': e.name or '',
'title': e.title or '',
'description': desc[:200] if len(desc) > 200 else desc,
'start_date': str(e.start_date) if e.start_date else '',
'end_date': str(e.end_date) if e.end_date else '',
'start_time': str(e.start_time) if e.start_time else '',
'end_time': str(e.end_time) if e.end_time else '',
'pincode': e.pincode or '',
'place': e.place or '',
'is_bookable': bool(e.is_bookable),
'event_type': e.event_type_id,
'event_status': e.event_status or '',
'venue_name': getattr(e, 'venue_name', '') or '',
'latitude': float(lat) if lat is not None else None,
'longitude': float(lng) if lng is not None else None,
'location_name': getattr(e, 'location_name', '') or '',
'thumb_img': img.event_image.url if img and img.event_image else '',
'is_eventify_event': bool(e.is_eventify_event),
'source': e.source or 'eventify',
}
def post(self, request): def post(self, request):
try: try:
print('*' * 100) try:
print(request.body) data = json.loads(request.body) if request.body else {}
print('*' * 100) except Exception:
user, token, data, error_response = validate_token_and_get_user(request) data = {}
if error_response:
return error_response
pincode = data.get("pincode") pincode = data.get("pincode", "all")
print('*' * 100) page = int(data.get("page", 1))
print(pincode) page_size = int(data.get("page_size", 50))
print('*' * 100) per_type = int(data.get("per_type", 0))
# pincode is optional - if not provided or 'all', return all events
events = Event.objects.all().order_by('-created_date') # Build base queryset (lazy - no DB hit yet)
MIN_EVENTS_THRESHOLD = 6
qs = Event.objects.all()
if pincode and pincode != 'all':
pincode_qs = qs.filter(pincode=pincode)
# Fallback to all events if pincode has too few
if pincode_qs.count() >= MIN_EVENTS_THRESHOLD:
qs = pincode_qs
# else: keep qs as Event.objects.all()
event_list = [] if per_type > 0 and page == 1:
# Diverse mode: one bounded query per event type
type_ids = list(qs.values_list('event_type_id', flat=True).distinct())
events_page = []
for tid in sorted(type_ids):
chunk = list(qs.filter(event_type_id=tid).order_by('-created_date')[:per_type])
events_page.extend(chunk)
total_count = qs.count()
end = len(events_page)
else:
# Standard pagination at DB level
total_count = qs.count()
qs = qs.order_by('-created_date')
start = (page - 1) * page_size
end = start + page_size
events_page = list(qs[start:end])
for e in events: # Fetch images ONLY for the events we will return
data_dict = model_to_dict(e) page_ids = [e.id for e in events_page]
try: primary_images = EventImages.objects.filter(event_id__in=page_ids, is_primary=True)
thumb_img = EventImages.objects.get(event=e.id, is_primary=True) thumb_map = {img.event_id: img for img in primary_images}
data_dict['thumb_img'] = request.build_absolute_uri(thumb_img.event_image.url)
except EventImages.DoesNotExist:
data_dict['thumb_img'] = ''
event_list.append(data_dict) # Serialize with direct attribute access (fast)
event_list = [self._serialize_event(e, thumb_map) for e in events_page]
print('*' * 100)
print(event_list)
print('*' * 100)
return JsonResponse({ return JsonResponse({
"status": "success", "status": "success",
"events": event_list "events": event_list,
"total_count": total_count,
"page": page,
"page_size": page_size,
"has_next": end < total_count,
}) })
except Exception as e: except Exception as e:
return JsonResponse( return JsonResponse({"status": "error", "message": str(e)})
{"status": "error", "message": str(e)},
)
class EventDetailAPI(APIView): class EventDetailAPI(APIView):
permission_classes = [AllowAny]
def post(self, request): def post(self, request):
try: try:
user, token, data, error_response = validate_token_and_get_user(request) try:
if error_response: data = json.loads(request.body) if request.body else {}
return error_response except Exception:
data = {}
event_id = data.get("event_id") event_id = data.get("event_id")
events = Event.objects.get(id=event_id) events = Event.objects.get(id=event_id)
event_images = EventImages.objects.filter(event=event_id) event_images = EventImages.objects.filter(event=event_id)
event_data = model_to_dict(events) event_data = model_to_dict(events)
@@ -110,21 +144,17 @@ class EventDetailAPI(APIView):
for ei in event_images: for ei in event_images:
event_img = {} event_img = {}
event_img['is_primary'] = ei.is_primary event_img['is_primary'] = ei.is_primary
event_img['image'] = request.build_absolute_uri(ei.event_image.url) event_img['image'] = ei.event_image.url
event_images_list.append(event_img) event_images_list.append(event_img)
event_data["images"] = event_images_list event_data["images"] = event_images_list
print(event_data)
return JsonResponse(event_data) return JsonResponse(event_data)
except Exception as e: except Exception as e:
return JsonResponse( return JsonResponse({"status": "error", "message": str(e)})
{"status": "error", "message": str(e)},
)
class EventImagesListAPI(APIView): class EventImagesListAPI(APIView):
authentication_classes = []
permission_classes = [AllowAny]
def post(self, request): def post(self, request):
try: try:
user, token, data, error_response = validate_token_and_get_user(request) user, token, data, error_response = validate_token_and_get_user(request)
@@ -138,7 +168,7 @@ class EventImagesListAPI(APIView):
res_data["status"] = "success" res_data["status"] = "success"
event_images_list = [] event_images_list = []
for ei in event_images: for ei in event_images:
event_images_list.append(request.build_absolute_uri(ei.event_image.url)) event_images_list.append(ei.event_image.url)
res_data["images"] = event_images_list res_data["images"] = event_images_list
@@ -154,6 +184,8 @@ class EventImagesListAPI(APIView):
@method_decorator(csrf_exempt, name='dispatch') @method_decorator(csrf_exempt, name='dispatch')
class EventsByCategoryAPI(APIView): class EventsByCategoryAPI(APIView):
authentication_classes = []
permission_classes = [AllowAny]
def post(self, request): def post(self, request):
try: try:
user, token, data, error_response = validate_token_and_get_user(request) user, token, data, error_response = validate_token_and_get_user(request)
@@ -172,9 +204,7 @@ class EventsByCategoryAPI(APIView):
for event in events_dict: for event in events_dict:
try: try:
event['event_image'] = request.build_absolute_uri( event['event_image'] = EventImages.objects.get(event=event['id'], is_primary=True).event_image.url
EventImages.objects.get(event=event['id'], is_primary=True).event_image.url
)
except EventImages.DoesNotExist: except EventImages.DoesNotExist:
event['event_image'] = '' event['event_image'] = ''
# event['start_date'] = convert_date_to_dd_mm_yyyy(event['start_date']) # event['start_date'] = convert_date_to_dd_mm_yyyy(event['start_date'])
@@ -193,6 +223,8 @@ class EventsByCategoryAPI(APIView):
@method_decorator(csrf_exempt, name='dispatch') @method_decorator(csrf_exempt, name='dispatch')
class EventsByMonthYearAPI(APIView): class EventsByMonthYearAPI(APIView):
authentication_classes = []
permission_classes = [AllowAny]
""" """
API to get events by month and year. API to get events by month and year.
Returns dates that have events, total count, and date-wise breakdown. Returns dates that have events, total count, and date-wise breakdown.
@@ -315,6 +347,8 @@ class EventsByMonthYearAPI(APIView):
@method_decorator(csrf_exempt, name='dispatch') @method_decorator(csrf_exempt, name='dispatch')
class EventsByDateAPI(APIView): class EventsByDateAPI(APIView):
authentication_classes = []
permission_classes = [AllowAny]
""" """
API to get events occurring on a specific date. API to get events occurring on a specific date.
Returns complete event information with primary images. Returns complete event information with primary images.
@@ -352,7 +386,7 @@ class EventsByDateAPI(APIView):
data_dict = model_to_dict(e) data_dict = model_to_dict(e)
try: try:
thumb_img = EventImages.objects.get(event=e.id, is_primary=True) thumb_img = EventImages.objects.get(event=e.id, is_primary=True)
data_dict['thumb_img'] = request.build_absolute_uri(thumb_img.event_image.url) data_dict['thumb_img'] = thumb_img.event_image.url
except EventImages.DoesNotExist: except EventImages.DoesNotExist:
data_dict['thumb_img'] = '' data_dict['thumb_img'] = ''
@@ -371,6 +405,8 @@ class EventsByDateAPI(APIView):
@method_decorator(csrf_exempt, name='dispatch') @method_decorator(csrf_exempt, name='dispatch')
class FeaturedEventsAPI(APIView): class FeaturedEventsAPI(APIView):
authentication_classes = []
permission_classes = [AllowAny]
"""Returns events where is_featured=True — used for the homepage hero carousel.""" """Returns events where is_featured=True — used for the homepage hero carousel."""
def post(self, request): def post(self, request):
@@ -385,7 +421,7 @@ class FeaturedEventsAPI(APIView):
data_dict = model_to_dict(e) data_dict = model_to_dict(e)
try: try:
thumb = EventImages.objects.get(event=e.id, is_primary=True) thumb = EventImages.objects.get(event=e.id, is_primary=True)
data_dict['thumb_img'] = request.build_absolute_uri(thumb.event_image.url) data_dict['thumb_img'] = thumb.event_image.url
except EventImages.DoesNotExist: except EventImages.DoesNotExist:
data_dict['thumb_img'] = '' data_dict['thumb_img'] = ''
event_list.append(data_dict) event_list.append(data_dict)
@@ -397,6 +433,8 @@ class FeaturedEventsAPI(APIView):
@method_decorator(csrf_exempt, name='dispatch') @method_decorator(csrf_exempt, name='dispatch')
class TopEventsAPI(APIView): class TopEventsAPI(APIView):
authentication_classes = []
permission_classes = [AllowAny]
"""Returns events where is_top_event=True — used for the Top Events section.""" """Returns events where is_top_event=True — used for the Top Events section."""
def post(self, request): def post(self, request):
@@ -411,7 +449,7 @@ class TopEventsAPI(APIView):
data_dict = model_to_dict(e) data_dict = model_to_dict(e)
try: try:
thumb = EventImages.objects.get(event=e.id, is_primary=True) thumb = EventImages.objects.get(event=e.id, is_primary=True)
data_dict['thumb_img'] = request.build_absolute_uri(thumb.event_image.url) data_dict['thumb_img'] = thumb.event_image.url
except EventImages.DoesNotExist: except EventImages.DoesNotExist:
data_dict['thumb_img'] = '' data_dict['thumb_img'] = ''
event_list.append(data_dict) event_list.append(data_dict)

274
mobile_api/views/reviews.py Normal file
View File

@@ -0,0 +1,274 @@
"""
Customer-facing review API views.
Writes to admin_api.Review so admin panel sees reviews immediately.
"""
import json
from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from rest_framework.views import APIView
from rest_framework.permissions import AllowAny
from django.db.models import Avg, Count, Q
from admin_api.models import Review, ReviewInteraction
from events.models import Event
from mobile_api.utils import validate_token_and_get_user
_STATUS_TO_JSON = {'live': 'PUBLISHED', 'pending': 'PENDING', 'rejected': 'FLAGGED'}
_JSON_TO_STATUS = {'PUBLISHED': 'live', 'PENDING': 'pending', 'FLAGGED': 'rejected'}
def _serialize_review(r, user_interactions=None):
"""Serialize a Review to match the customer app's expected shape."""
interactions = user_interactions or {}
try:
display = r.display_name or r.reviewer.get_full_name() or r.reviewer.username
except Exception:
display = r.display_name or ''
try:
uname = r.reviewer.username
except Exception:
uname = ''
return {
'id': r.id,
'event_id': r.event_id,
'username': uname,
'display_name': display,
'rating': r.rating,
'comment': r.review_text,
'status': _STATUS_TO_JSON.get(r.status, r.status),
'is_verified': r.is_verified,
'helpful_count': r.helpful_count,
'flag_count': r.flag_count,
'has_helpful': interactions.get('HELPFUL', False),
'has_flagged': interactions.get('FLAG', False),
'created_at': r.submission_date.isoformat() if r.submission_date else '',
}
def _rating_distribution(event_id):
"""Return {1:count, 2:count, ..., 5:count} for live reviews."""
dist = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0}
qs = Review.objects.filter(event_id=event_id, status='live').values('rating').annotate(c=Count('id'))
for row in qs:
dist[row['rating']] = row['c']
return dist
def _aggregates(event_id):
"""Return (average_rating, review_count) for live reviews."""
agg = Review.objects.filter(event_id=event_id, status='live').aggregate(
avg=Avg('rating'), cnt=Count('id')
)
return round(float(agg['avg'] or 0), 1), agg['cnt'] or 0
@method_decorator(csrf_exempt, name='dispatch')
class ReviewSubmitView(APIView):
"""POST /api/reviews/submit -- Submit or update a review."""
authentication_classes = []
permission_classes = [AllowAny]
def post(self, request):
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
event_id = data.get('event_id')
rating = data.get('rating')
comment = data.get('comment', '') or ''
is_verified = bool(data.get('is_verified', False))
if not event_id or not rating:
return JsonResponse({'status': 'error', 'message': 'event_id and rating are required'}, status=400)
try:
rating = int(rating)
if rating < 1 or rating > 5:
raise ValueError
except (ValueError, TypeError):
return JsonResponse({'status': 'error', 'message': 'Rating must be 1-5'}, status=400)
try:
event = Event.objects.get(pk=event_id)
except Event.DoesNotExist:
return JsonResponse({'status': 'error', 'message': 'Event not found'}, status=404)
# Upsert: one review per user per event
review, created = Review.objects.update_or_create(
reviewer=user,
event=event,
defaults={
'rating': rating,
'review_text': comment,
'is_verified': is_verified,
'status': 'live', # auto-approve for customer submissions
'display_name': user.get_full_name() or user.username,
}
)
avg_rating, review_count = _aggregates(event_id)
return JsonResponse({
'status': 'success',
'review': _serialize_review(review),
'average_rating': avg_rating,
'review_count': review_count,
})
@method_decorator(csrf_exempt, name='dispatch')
class MobileReviewListView(APIView):
"""POST /api/reviews/list -- Get published reviews for an event."""
authentication_classes = []
permission_classes = [AllowAny]
def post(self, request):
try:
body = json.loads(request.body) if request.body else {}
except json.JSONDecodeError:
body = {}
event_id = body.get('event_id')
username = body.get('username', '')
page = int(body.get('page', 1))
page_size = min(int(body.get('page_size', 10)), 50)
if not event_id:
return JsonResponse({'status': 'error', 'message': 'event_id is required'}, status=400)
qs = Review.objects.filter(event_id=event_id, status='live').select_related('reviewer')
qs = qs.order_by('-is_verified', '-helpful_count', '-submission_date')
total = qs.count()
reviews = list(qs[(page - 1) * page_size: page * page_size])
# Bulk lookup interactions for current user
user_interactions = {}
if username and reviews:
review_ids = [r.id for r in reviews]
interactions = ReviewInteraction.objects.filter(
username=username, review_id__in=review_ids
)
for inter in interactions:
if inter.review_id not in user_interactions:
user_interactions[inter.review_id] = {}
user_interactions[inter.review_id][inter.interaction_type] = True
formatted = [
_serialize_review(r, user_interactions.get(r.id, {}))
for r in reviews
]
avg_rating, review_count = _aggregates(event_id)
distribution = _rating_distribution(event_id)
# Get user's own review (any status)
user_review = None
if username:
try:
from accounts.models import User
u = User.objects.get(username=username)
ur = Review.objects.filter(event_id=event_id, reviewer=u).first()
if ur:
user_review = _serialize_review(ur)
except Exception:
pass
return JsonResponse({
'status': 'success',
'reviews': formatted,
'total': total,
'page': page,
'page_size': page_size,
'average_rating': avg_rating,
'review_count': review_count,
'distribution': distribution,
'user_review': user_review,
})
@method_decorator(csrf_exempt, name='dispatch')
class ReviewHelpfulView(APIView):
"""POST /api/reviews/helpful -- Mark a review as helpful."""
authentication_classes = []
permission_classes = [AllowAny]
def post(self, request):
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
review_id = data.get('review_id')
if not review_id:
return JsonResponse({'status': 'error', 'message': 'review_id is required'}, status=400)
try:
review = Review.objects.get(pk=review_id)
except Review.DoesNotExist:
return JsonResponse({'status': 'error', 'message': 'Review not found'}, status=404)
if review.reviewer == user:
return JsonResponse({'status': 'error', 'message': 'Cannot mark your own review as helpful'}, status=400)
_, created = ReviewInteraction.objects.get_or_create(
review=review,
username=user.username,
interaction_type='HELPFUL'
)
if created:
new_count = ReviewInteraction.objects.filter(review=review, interaction_type='HELPFUL').count()
review.helpful_count = new_count
review.save(update_fields=['helpful_count'])
return JsonResponse({
'status': 'success',
'helpful_count': review.helpful_count,
})
@method_decorator(csrf_exempt, name='dispatch')
class ReviewFlagView(APIView):
"""POST /api/reviews/flag -- Flag/report a review."""
authentication_classes = []
permission_classes = [AllowAny]
def post(self, request):
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
review_id = data.get('review_id')
if not review_id:
return JsonResponse({'status': 'error', 'message': 'review_id is required'}, status=400)
try:
review = Review.objects.get(pk=review_id)
except Review.DoesNotExist:
return JsonResponse({'status': 'error', 'message': 'Review not found'}, status=404)
if review.reviewer == user:
return JsonResponse({'status': 'error', 'message': 'Cannot flag your own review'}, status=400)
_, created = ReviewInteraction.objects.get_or_create(
review=review,
username=user.username,
interaction_type='FLAG'
)
new_count = ReviewInteraction.objects.filter(review=review, interaction_type='FLAG').count()
new_status = review.status
if new_count >= 3 and review.status == 'live':
new_status = 'rejected'
review.status = 'rejected'
review.reject_reason = 'inappropriate'
review.flag_count = new_count
review.save(update_fields=['flag_count', 'status', 'reject_reason'])
return JsonResponse({
'status': 'success',
'flag_count': new_count,
'review_status': _STATUS_TO_JSON.get(new_status, new_status),
})

View File

@@ -84,6 +84,7 @@ class LoginView(View):
response = { response = {
'message': 'Login successful', 'message': 'Login successful',
'token': token.key, 'token': token.key,
'eventify_id': user.eventify_id,
'username': user.username, 'username': user.username,
'email': user.email, 'email': user.email,
'phone_number': user.phone_number, 'phone_number': user.phone_number,
@@ -97,7 +98,7 @@ class LoginView(View):
'place': user.place, 'place': user.place,
'latitude': user.latitude, 'latitude': user.latitude,
'longitude': user.longitude, 'longitude': user.longitude,
'profile_photo': request.build_absolute_uri(user.profile_picture.url) if user.profile_picture else '' 'profile_photo': user.profile_picture.url if user.profile_picture else ''
} }
print('4') print('4')
print(response) print(response)

View File

@@ -58,7 +58,7 @@ def _partner_to_dict(partner, request=None):
# Add document file URL if exists # Add document file URL if exists
if partner.kyc_compliance_document_file: if partner.kyc_compliance_document_file:
if request: if request:
data["kyc_compliance_document_file"] = request.build_absolute_uri(partner.kyc_compliance_document_file.url) data["kyc_compliance_document_file"] = partner.kyc_compliance_document_file.url
else: else:
data["kyc_compliance_document_file"] = partner.kyc_compliance_document_file.url data["kyc_compliance_document_file"] = partner.kyc_compliance_document_file.url
else: else:
@@ -168,7 +168,7 @@ def _build_kyc_documents(partner, request):
name = f"{type_label} - {partner.name}" name = f"{type_label} - {partner.name}"
if partner.kyc_compliance_document_file: if partner.kyc_compliance_document_file:
if request: if request:
url = request.build_absolute_uri(partner.kyc_compliance_document_file.url) url = partner.kyc_compliance_document_file.url
else: else:
url = partner.kyc_compliance_document_file.url url = partner.kyc_compliance_document_file.url
else: else:
@@ -835,6 +835,7 @@ def _user_to_dict(user, request=None):
user, user,
fields=[ fields=[
"id", "id",
"eventify_id",
"username", "username",
"email", "email",
"phone_number", "phone_number",
@@ -854,7 +855,7 @@ def _user_to_dict(user, request=None):
# Add profile picture URL if exists # Add profile picture URL if exists
if user.profile_picture: if user.profile_picture:
if request: if request:
data["profile_picture"] = request.build_absolute_uri(user.profile_picture.url) data["profile_picture"] = user.profile_picture.url
else: else:
data["profile_picture"] = user.profile_picture.url data["profile_picture"] = user.profile_picture.url
else: else:

View File

@@ -45,7 +45,7 @@ class Partner(models.Model):
primary_contact_person_email = models.EmailField() primary_contact_person_email = models.EmailField()
primary_contact_person_phone = models.CharField(max_length=15) primary_contact_person_phone = models.CharField(max_length=15)
status = models.CharField(max_length=250, choices=STATUS_CHOICES, default='active') status = models.CharField(max_length=250, choices=STATUS_CHOICES, default='active', db_index=True)
address = models.TextField(blank=True, null=True) address = models.TextField(blank=True, null=True)
city = models.CharField(max_length=250, blank=True, null=True) city = models.CharField(max_length=250, blank=True, null=True)
@@ -58,7 +58,7 @@ class Partner(models.Model):
longitude = models.DecimalField(max_digits=9, decimal_places=6, blank=True, null=True) longitude = models.DecimalField(max_digits=9, decimal_places=6, blank=True, null=True)
is_kyc_compliant = models.BooleanField(default=False) is_kyc_compliant = models.BooleanField(default=False)
kyc_compliance_status = models.CharField(max_length=250, choices=KYC_COMPLIANCE_STATUS_CHOICES, default='pending') kyc_compliance_status = models.CharField(max_length=250, choices=KYC_COMPLIANCE_STATUS_CHOICES, default='pending', db_index=True)
kyc_compliance_reason = models.TextField(blank=True, null=True) kyc_compliance_reason = models.TextField(blank=True, null=True)
kyc_compliance_document_type = models.CharField(max_length=250, choices=KYC_DOCUMENT_TYPE_CHOICES, blank=True, null=True) kyc_compliance_document_type = models.CharField(max_length=250, choices=KYC_DOCUMENT_TYPE_CHOICES, blank=True, null=True)
kyc_compliance_document_other_type = models.CharField(max_length=250, blank=True, null=True) kyc_compliance_document_other_type = models.CharField(max_length=250, blank=True, null=True)

9
requirements-docker.txt Normal file
View File

@@ -0,0 +1,9 @@
Django==4.2.21
Pillow==10.1.0
django-summernote
djangorestframework==3.14.0
django-cors-headers==4.3.0
gunicorn==21.2.0
django-extensions==3.2.3
psycopg2-binary==2.9.9
djangorestframework-simplejwt==5.3.1

View File

@@ -1,2 +1,3 @@
Django>=4.2 Django>=4.2
Pillow Pillow
django-summernote

View File

@@ -1,75 +1,81 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset='utf-8'> <meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'> <meta name='viewport' content='width=device-width, initial-scale=1'>
<title>Eventify</title> <title>Eventify</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- jQuery required for Summernote -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head> </head>
<body> <body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="{% url 'accounts:dashboard' %}">Eventify</a> <a class="navbar-brand" href="{% url 'accounts:dashboard' %}">Eventify</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navmenu"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navmenu">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navmenu"> <div class="collapse navbar-collapse" id="navmenu">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
<!-- Accessible by Admin, Manager, Staff --> <!-- Accessible by Admin, Manager, Staff -->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'accounts:dashboard' %}">Dashboard</a> <a class="nav-link" href="{% url 'accounts:dashboard' %}">Dashboard</a>
</li> </li>
{% if user.role == "admin" or user.role == "manager" %} {% if user.role == "admin" or user.role == "manager" %}
<!-- Admin + Manager --> <!-- Admin + Manager -->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'master_data:event_type_list' %}">Categories</a> <a class="nav-link" href="{% url 'master_data:event_type_list' %}">Categories</a>
</li> </li>
{% endif %} {% endif %}
{% if user.role in "admin manager staff" %} {% if user.role in "admin manager staff" %}
<!-- Admin + Manager + Staff --> <!-- Admin + Manager + Staff -->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'events:event_list' %}">Events</a> <a class="nav-link" href="{% url 'events:event_list' %}">Events</a>
</li> </li>
{% endif %} {% endif %}
{% if user.role == "admin" %} {% if user.role == "admin" %}
<!-- Admin only --> <!-- Admin only -->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'accounts:user_list' %}">Users</a> <a class="nav-link" href="{% url 'accounts:user_list' %}">Users</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
<ul class="navbar-nav"> <ul class="navbar-nav">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<li class="nav-item"><a class="nav-link" href="#"> <li class="nav-item"><a class="nav-link" href="#">
{% if user.first_name and user.last_name %} {% if user.first_name and user.last_name %}
{{ user.first_name }} {{ user.last_name }} {{ user.first_name }} {{ user.last_name }}
{% elif user.username %} {% elif user.username %}
{{ user.username }} {{ user.username }}
{% else %} {% else %}
{{ user.email }} {{ user.email }}
{% endif %} {% endif %}
</a></li> </a></li>
<li class="nav-item"><a class="nav-link text-danger" href="{% url 'accounts:logout' %}">Logout</a></li> <li class="nav-item"><a class="nav-link text-danger" href="{% url 'accounts:logout' %}">Logout</a>
{% else %} </li>
{% else %}
<li class="nav-item"><a class="nav-link" href="{% url 'accounts:login' %}">Login</a></li> <li class="nav-item"><a class="nav-link" href="{% url 'accounts:login' %}">Login</a></li>
{% endif %} {% endif %}
</ul> </ul>
</div>
</div> </div>
</div> </nav>
</nav> <div class="container mt-4">
<div class="container mt-4"> {% if messages %}
{% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div> <div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body> </body>
</html> </html>

View File

@@ -16,13 +16,35 @@
*{box-sizing:border-box} *{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} 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} .auth-wrapper{display:flex;min-height:100vh}
/* LEFT PANEL — video */
.auth-left{ .auth-left{
position:relative;
width:40%; width:40%;
min-width:320px; min-width:320px;
background:linear-gradient(180deg,var(--blue1),var(--blue2)); overflow:hidden;
color:#fff;padding:48px;display:flex;flex-direction:column;justify-content:center;gap:10px; 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 h1{font-size:36px;margin:0}
.auth-left p{opacity:.92;margin:0;font-size:16px} .auth-left p{opacity:.92;margin:0;font-size:16px}
@@ -52,10 +74,12 @@
.message.warning{background:#fff7e6;color:#7a4b00} .message.warning{background:#fff7e6;color:#7a4b00}
.errorlist{color:#b00020;margin:6px 0 0 0;font-size:13px} .errorlist{color:#b00020;margin:6px 0 0 0;font-size:13px}
/* responsive */ /* responsive — mobile: hide video, show compact gradient header */
@media (max-width:900px){ @media (max-width:900px){
.auth-wrapper{flex-direction:column} .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-right{padding:20px}
.auth-card{border-radius:14px;padding:28px} .auth-card{border-radius:14px;padding:28px}
} }
@@ -64,8 +88,14 @@
<body> <body>
<div class="auth-wrapper"> <div class="auth-wrapper">
<div class="auth-left"> <div class="auth-left">
<div class="brand">Eventify</div> <video autoplay muted loop playsinline preload="auto" poster="https://images.pexels.com/videos/36761729/kerala-kerala-tourism-36761729.jpeg?auto=compress&cs=tinysrgb&w=750">
<p>{% block left_subtext %}Your events at your fingertips.{% endblock %}</p> <source src="https://videos.pexels.com/video-files/36761729/15579487_1920_1080_30fps.mp4" type="video/mp4">
<source src="https://videos.pexels.com/video-files/36761729/15579486_1280_720_30fps.mp4" type="video/mp4">
</video>
<div class="auth-left-content">
<div class="brand">Eventify</div>
<p>{% block left_subtext %}Your events at your fingertips.{% endblock %}</p>
</div>
</div> </div>
<div class="auth-right"> <div class="auth-right">

View File

@@ -1,49 +1,63 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<div class="container mt-4"> <div class="container mt-4">
<h3>{% if object %}Edit{% else %}Add{% endif %} Event</h3> <h3>{% if object %}Edit{% else %}Add{% endif %} Event</h3>
<form method="post" novalidate> <form method="post" novalidate>
{% csrf_token %} {% csrf_token %}
{{ form.media }}
{% for field in form %} {% for field in form %}
<div class="mb-3"> <div class="mb-3">
{{ field.label_tag }} {{ field.label_tag }}
{{ field }} {% if field.name == 'source' %}
{% for error in field.errors %} <div class="mt-2">
<div class="text-danger">{{ error }}</div> {% for radio in field %}
{% endfor %} <div class="form-check">
{{ radio.tag }}
<label class="form-check-label" for="{{ radio.id_for_label }}">{{ radio.choice_label }}</label>
</div> </div>
{% endfor %}
</div>
{% else %}
{{ field }}
{% endif %}
{% for error in field.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %} {% endfor %}
</div>
{% endfor %}
<div class="mb-3 mt-4">
<button class="btn btn-primary">Save</button> <button class="btn btn-primary">Save</button>
<a class="btn btn-secondary" href="{% url 'events:event_list' %}">Cancel</a> <a class="btn btn-secondary" href="{% url 'events:event_list' %}">Cancel</a>
</form> </div>
</div> </form>
</div>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
const allYearEventCheckbox = document.getElementById('id_all_year_event'); const allYearEventCheckbox = document.getElementById('id_all_year_event');
const startDateField = document.getElementById('id_start_date'); const startDateField = document.getElementById('id_start_date');
const endDateField = document.getElementById('id_end_date'); const endDateField = document.getElementById('id_end_date');
const startTimeField = document.getElementById('id_start_time'); const startTimeField = document.getElementById('id_start_time');
const endTimeField = document.getElementById('id_end_time'); const endTimeField = document.getElementById('id_end_time');
function toggleDateTimeFields() { function toggleDateTimeFields() {
const isDisabled = allYearEventCheckbox.checked; const isDisabled = allYearEventCheckbox.checked;
startDateField.disabled = isDisabled; startDateField.disabled = isDisabled;
endDateField.disabled = isDisabled; endDateField.disabled = isDisabled;
startTimeField.disabled = isDisabled; startTimeField.disabled = isDisabled;
endTimeField.disabled = isDisabled; endTimeField.disabled = isDisabled;
} }
// Set initial state // Set initial state
toggleDateTimeFields(); toggleDateTimeFields();
// Listen for checkbox changes // Listen for checkbox changes
if (allYearEventCheckbox) { if (allYearEventCheckbox) {
allYearEventCheckbox.addEventListener('change', toggleDateTimeFields); allYearEventCheckbox.addEventListener('change', toggleDateTimeFields);
} }
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -1,9 +1,20 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<div class="d-flex justify-content-between mb-3"> <div class="row mb-3">
<h3>Events</h3> <div class="col-md-6">
<a class="btn btn-success" href="{% url 'events:event_add' %}">Add Event</a> <h3>Events</h3>
</div>
<div class="col-md-4">
<form method="get" action="." class="d-flex">
<input class="form-control me-2" type="search" name="q" placeholder="Search events..." aria-label="Search" value="{{ request.GET.q }}">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
<div class="col-md-2 text-end">
<a class="btn btn-success" href="{% url 'events:event_add' %}">Add Event</a>
</div>
</div> </div>
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@@ -44,4 +55,35 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<!-- Pagination -->
{% if is_paginated %}
<nav aria-label="Page navigation">
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">&laquo; First</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">Previous</a>
</li>
{% endif %}
<li class="page-item disabled">
<span class="page-link">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
</li>
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">Next</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">Last &raquo;</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock %} {% endblock %}

29
update_events.py Normal file
View File

@@ -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.")

19
user.py Normal file
View File

@@ -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!")