Spaces:
Runtime error
Runtime error
Commit
·
c68b343
1
Parent(s):
185f6c1
Fix Django backend deployment on HF Spaces
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- Dockerfile +17 -0
- basho_backend/apps/__init__.py +0 -0
- basho_backend/apps/accounts/__init__.py +0 -0
- basho_backend/apps/accounts/admin.py +5 -0
- basho_backend/apps/accounts/apps.py +6 -0
- basho_backend/apps/accounts/managers.py +21 -0
- basho_backend/apps/accounts/migrations/0001_initial.py +45 -0
- basho_backend/apps/accounts/migrations/0002_user_profile_image.py +18 -0
- basho_backend/apps/accounts/migrations/0003_alter_user_profile_image.py +18 -0
- basho_backend/apps/accounts/migrations/0004_remove_user_profile_image_user_avatar.py +22 -0
- basho_backend/apps/accounts/migrations/__init__.py +0 -0
- basho_backend/apps/accounts/models.py +39 -0
- basho_backend/apps/accounts/otp.py +4 -0
- basho_backend/apps/accounts/services.py +107 -0
- basho_backend/apps/accounts/tests.py +3 -0
- basho_backend/apps/accounts/urls.py +38 -0
- basho_backend/apps/accounts/views.py +304 -0
- basho_backend/apps/corporate/__init__.py +0 -0
- basho_backend/apps/corporate/admin.py +49 -0
- basho_backend/apps/corporate/apps.py +5 -0
- basho_backend/apps/corporate/migrations/0001_initial.py +32 -0
- basho_backend/apps/corporate/migrations/0002_alter_corporateinquiry_options.py +17 -0
- basho_backend/apps/corporate/migrations/__init__.py +0 -0
- basho_backend/apps/corporate/models.py +39 -0
- basho_backend/apps/corporate/tests.py +3 -0
- basho_backend/apps/corporate/urls.py +6 -0
- basho_backend/apps/corporate/views.py +66 -0
- basho_backend/apps/experiences/__init__.py +0 -0
- basho_backend/apps/experiences/admin.py +116 -0
- basho_backend/apps/experiences/apps.py +5 -0
- basho_backend/apps/experiences/migrations/0001_initial.py +43 -0
- basho_backend/apps/experiences/migrations/0002_booking_payment_amount_booking_payment_status.py +23 -0
- basho_backend/apps/experiences/migrations/0003_studiobooking_upcomingevent.py +36 -0
- basho_backend/apps/experiences/migrations/0004_workshop_workshopslot_workshopregistration.py +69 -0
- basho_backend/apps/experiences/migrations/0005_alter_booking_options_alter_experience_options_and_more.py +210 -0
- basho_backend/apps/experiences/migrations/0006_alter_booking_options_alter_experience_options_and_more.py +201 -0
- basho_backend/apps/experiences/migrations/0007_booking_otp_booking_otp_expires_at_experienceslot_and_more.py +46 -0
- basho_backend/apps/experiences/migrations/0008_remove_booking_otp_remove_booking_otp_expires_at_and_more.py +62 -0
- basho_backend/apps/experiences/migrations/0009_rename_booked_participants_experienceslot_booked_slots_and_more.py +37 -0
- basho_backend/apps/experiences/migrations/0010_alter_experience_image.py +18 -0
- basho_backend/apps/experiences/migrations/__init__.py +0 -0
- basho_backend/apps/experiences/models.py +256 -0
- basho_backend/apps/experiences/serializers.py +186 -0
- basho_backend/apps/experiences/tests.py +3 -0
- basho_backend/apps/experiences/urls.py +67 -0
- basho_backend/apps/experiences/views.py +434 -0
- basho_backend/apps/main/__init__.py +0 -0
- basho_backend/apps/main/admin.py +3 -0
- basho_backend/apps/main/apps.py +6 -0
- basho_backend/apps/main/migrations/__init__.py +0 -0
Dockerfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 4 |
+
ENV PYTHONUNBUFFERED=1
|
| 5 |
+
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
COPY requirements.txt .
|
| 9 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 10 |
+
|
| 11 |
+
COPY . .
|
| 12 |
+
|
| 13 |
+
WORKDIR /app/kdon
|
| 14 |
+
|
| 15 |
+
EXPOSE 7860
|
| 16 |
+
|
| 17 |
+
CMD ["gunicorn", "basho_backend.basho_backend.wsgi:application", "--bind", "0.0.0.0:7860"]
|
basho_backend/apps/__init__.py
ADDED
|
File without changes
|
basho_backend/apps/accounts/__init__.py
ADDED
|
File without changes
|
basho_backend/apps/accounts/admin.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.contrib import admin
|
| 2 |
+
from .models import User, EmailOTP
|
| 3 |
+
|
| 4 |
+
admin.site.register(User)
|
| 5 |
+
admin.site.register(EmailOTP)
|
basho_backend/apps/accounts/apps.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.apps import AppConfig
|
| 2 |
+
|
| 3 |
+
class AccountsConfig(AppConfig):
|
| 4 |
+
default_auto_field = "django.db.models.BigAutoField"
|
| 5 |
+
name = "apps.accounts"
|
| 6 |
+
label = "accounts"
|
basho_backend/apps/accounts/managers.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.contrib.auth.models import BaseUserManager
|
| 2 |
+
|
| 3 |
+
class UserManager(BaseUserManager):
|
| 4 |
+
def create_user(self, username, email, password=None):
|
| 5 |
+
if not email:
|
| 6 |
+
raise ValueError("Email is required")
|
| 7 |
+
|
| 8 |
+
user = self.model(
|
| 9 |
+
username=username,
|
| 10 |
+
email=self.normalize_email(email),
|
| 11 |
+
)
|
| 12 |
+
user.set_password(password)
|
| 13 |
+
user.save(using=self._db)
|
| 14 |
+
return user
|
| 15 |
+
|
| 16 |
+
def create_superuser(self, username, email, password):
|
| 17 |
+
user = self.create_user(username, email, password)
|
| 18 |
+
user.is_staff = True
|
| 19 |
+
user.is_superuser = True
|
| 20 |
+
user.save(using=self._db)
|
| 21 |
+
return user
|
basho_backend/apps/accounts/migrations/0001_initial.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2026-01-08 12:43
|
| 2 |
+
|
| 3 |
+
import django.utils.timezone
|
| 4 |
+
from django.db import migrations, models
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Migration(migrations.Migration):
|
| 8 |
+
|
| 9 |
+
initial = True
|
| 10 |
+
|
| 11 |
+
dependencies = [
|
| 12 |
+
('auth', '0012_alter_user_first_name_max_length'),
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
operations = [
|
| 16 |
+
migrations.CreateModel(
|
| 17 |
+
name='EmailOTP',
|
| 18 |
+
fields=[
|
| 19 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 20 |
+
('email', models.EmailField(max_length=254)),
|
| 21 |
+
('otp', models.CharField(max_length=6)),
|
| 22 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 23 |
+
],
|
| 24 |
+
),
|
| 25 |
+
migrations.CreateModel(
|
| 26 |
+
name='User',
|
| 27 |
+
fields=[
|
| 28 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 29 |
+
('password', models.CharField(max_length=128, verbose_name='password')),
|
| 30 |
+
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
| 31 |
+
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
| 32 |
+
('username', models.CharField(max_length=150, unique=True)),
|
| 33 |
+
('email', models.EmailField(max_length=254, unique=True)),
|
| 34 |
+
('is_email_verified', models.BooleanField(default=False)),
|
| 35 |
+
('is_active', models.BooleanField(default=True)),
|
| 36 |
+
('is_staff', models.BooleanField(default=False)),
|
| 37 |
+
('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
|
| 38 |
+
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
| 39 |
+
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
| 40 |
+
],
|
| 41 |
+
options={
|
| 42 |
+
'abstract': False,
|
| 43 |
+
},
|
| 44 |
+
),
|
| 45 |
+
]
|
basho_backend/apps/accounts/migrations/0002_user_profile_image.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2026-01-10 13:04
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('accounts', '0001_initial'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.AddField(
|
| 14 |
+
model_name='user',
|
| 15 |
+
name='profile_image',
|
| 16 |
+
field=models.ImageField(blank=True, null=True, upload_to='profile_pics/'),
|
| 17 |
+
),
|
| 18 |
+
]
|
basho_backend/apps/accounts/migrations/0003_alter_user_profile_image.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2026-01-12 19:52
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('accounts', '0002_user_profile_image'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.AlterField(
|
| 14 |
+
model_name='user',
|
| 15 |
+
name='profile_image',
|
| 16 |
+
field=models.ImageField(blank=True, default='profile_pics/default/default.png', upload_to='profile_pics/'),
|
| 17 |
+
),
|
| 18 |
+
]
|
basho_backend/apps/accounts/migrations/0004_remove_user_profile_image_user_avatar.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2026-01-15 15:47
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('accounts', '0003_alter_user_profile_image'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.RemoveField(
|
| 14 |
+
model_name='user',
|
| 15 |
+
name='profile_image',
|
| 16 |
+
),
|
| 17 |
+
migrations.AddField(
|
| 18 |
+
model_name='user',
|
| 19 |
+
name='avatar',
|
| 20 |
+
field=models.CharField(default='p1.png', max_length=50),
|
| 21 |
+
),
|
| 22 |
+
]
|
basho_backend/apps/accounts/migrations/__init__.py
ADDED
|
File without changes
|
basho_backend/apps/accounts/models.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.db import models
|
| 2 |
+
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
|
| 3 |
+
from django.utils import timezone
|
| 4 |
+
from .managers import UserManager
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class User(AbstractBaseUser, PermissionsMixin):
|
| 8 |
+
username = models.CharField(max_length=150, unique=True)
|
| 9 |
+
email = models.EmailField(unique=True)
|
| 10 |
+
|
| 11 |
+
avatar = models.CharField(
|
| 12 |
+
max_length=50,
|
| 13 |
+
default="p1.png"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
is_email_verified = models.BooleanField(default=False)
|
| 19 |
+
is_active = models.BooleanField(default=True)
|
| 20 |
+
is_staff = models.BooleanField(default=False)
|
| 21 |
+
|
| 22 |
+
date_joined = models.DateTimeField(default=timezone.now)
|
| 23 |
+
|
| 24 |
+
objects = UserManager()
|
| 25 |
+
|
| 26 |
+
USERNAME_FIELD = "username"
|
| 27 |
+
REQUIRED_FIELDS = ["email"]
|
| 28 |
+
|
| 29 |
+
def __str__(self):
|
| 30 |
+
return self.username
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class EmailOTP(models.Model):
|
| 34 |
+
email = models.EmailField()
|
| 35 |
+
otp = models.CharField(max_length=6)
|
| 36 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 37 |
+
|
| 38 |
+
def __str__(self):
|
| 39 |
+
return f"{self.email} - {self.otp}"
|
basho_backend/apps/accounts/otp.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
|
| 3 |
+
def generate_otp():
|
| 4 |
+
return str(random.randint(100000, 999999))
|
basho_backend/apps/accounts/services.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.core.mail import EmailMultiAlternatives
|
| 2 |
+
from django.conf import settings
|
| 3 |
+
|
| 4 |
+
def send_otp_email(email, otp):
|
| 5 |
+
subject = "Welcome to Basho byy Shivangi"
|
| 6 |
+
|
| 7 |
+
text_content = f"""
|
| 8 |
+
Welcome to Basho byy Shivangi!
|
| 9 |
+
|
| 10 |
+
We’re delighted to have you join us.
|
| 11 |
+
|
| 12 |
+
Your One-Time Password (OTP) is: {otp}
|
| 13 |
+
|
| 14 |
+
⏳This OTP is valid for 5 minutes.
|
| 15 |
+
Do not share this OTP with anyone.
|
| 16 |
+
|
| 17 |
+
Warm regards,
|
| 18 |
+
Team Basho byy Shivangi
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
html_content = f"""
|
| 22 |
+
<html>
|
| 23 |
+
<body style="font-family: Arial, sans-serif; color: #333;">
|
| 24 |
+
<h2>Welcome to Basho byy Shivangi!</h2>
|
| 25 |
+
|
| 26 |
+
<p>We’re delighted to have you join us.</p>
|
| 27 |
+
|
| 28 |
+
<p>Your One-Time Password (OTP) is:</p>
|
| 29 |
+
|
| 30 |
+
<p style="
|
| 31 |
+
font-size: 32px;
|
| 32 |
+
font-weight: bold;
|
| 33 |
+
letter-spacing: 4px;
|
| 34 |
+
color: #652810;
|
| 35 |
+
margin: 16px 0;
|
| 36 |
+
">
|
| 37 |
+
{otp}
|
| 38 |
+
</p>
|
| 39 |
+
|
| 40 |
+
<p>⏳ <strong>This OTP is valid for 5 minutes.</strong></p>
|
| 41 |
+
<p>Please do not share this OTP with anyone.</p>
|
| 42 |
+
|
| 43 |
+
<br />
|
| 44 |
+
<p>Warm regards,<br /><strong>Team Basho byy Shivangi</strong></p>
|
| 45 |
+
</body>
|
| 46 |
+
</html>
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
email_message = EmailMultiAlternatives(
|
| 50 |
+
subject=subject,
|
| 51 |
+
body=text_content,
|
| 52 |
+
from_email=settings.DEFAULT_FROM_EMAIL,
|
| 53 |
+
to=[email],
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
email_message.attach_alternative(html_content, "text/html")
|
| 57 |
+
email_message.send()
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def send_welcome_email(email, username):
|
| 61 |
+
subject = "Welcome to Basho byy Shivangi "
|
| 62 |
+
|
| 63 |
+
text_content = f"""
|
| 64 |
+
Welcome to Basho byy Shivangi, {username}!
|
| 65 |
+
|
| 66 |
+
Your account has been successfully created.
|
| 67 |
+
|
| 68 |
+
We’re delighted to have you as part of our community.
|
| 69 |
+
|
| 70 |
+
Warm regards,
|
| 71 |
+
Team Basho byy Shivangi
|
| 72 |
+
"""
|
| 73 |
+
|
| 74 |
+
html_content = f"""
|
| 75 |
+
<html>
|
| 76 |
+
<body style="font-family: Arial, sans-serif; color: #333;">
|
| 77 |
+
<h2>Welcome to Basho byy Shivangi </h2>
|
| 78 |
+
|
| 79 |
+
<p>Hi <strong>{username}</strong>,</p>
|
| 80 |
+
|
| 81 |
+
<p>
|
| 82 |
+
Your account has been <strong>successfully created</strong>.
|
| 83 |
+
We’re delighted to have you join our journey of handcrafted elegance.
|
| 84 |
+
</p>
|
| 85 |
+
|
| 86 |
+
<p>
|
| 87 |
+
You can now explore our collections, workshops, and custom creations.
|
| 88 |
+
</p>
|
| 89 |
+
|
| 90 |
+
<br />
|
| 91 |
+
|
| 92 |
+
<p style="color:#652810;">
|
| 93 |
+
Warm regards,<br /> <strong>Team Basho byy Shivangi!</strong>
|
| 94 |
+
</p>
|
| 95 |
+
</body>
|
| 96 |
+
</html>
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
email_message = EmailMultiAlternatives(
|
| 100 |
+
subject=subject,
|
| 101 |
+
body=text_content,
|
| 102 |
+
from_email=settings.DEFAULT_FROM_EMAIL,
|
| 103 |
+
to=[email],
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
email_message.attach_alternative(html_content, "text/html")
|
| 107 |
+
email_message.send()
|
basho_backend/apps/accounts/tests.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.test import TestCase
|
| 2 |
+
|
| 3 |
+
# Create your tests here.
|
basho_backend/apps/accounts/urls.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.urls import path
|
| 2 |
+
from .views import (
|
| 3 |
+
send_otp,
|
| 4 |
+
register_user,
|
| 5 |
+
login_user,
|
| 6 |
+
google_login,
|
| 7 |
+
google_register,
|
| 8 |
+
change_username,
|
| 9 |
+
me,
|
| 10 |
+
set_avatar,
|
| 11 |
+
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
urlpatterns = [
|
| 15 |
+
path("send-otp/", send_otp),
|
| 16 |
+
path("register/", register_user),
|
| 17 |
+
path("login/", login_user),
|
| 18 |
+
path("google-login/", google_login),
|
| 19 |
+
path("google-register/", google_register),
|
| 20 |
+
path("change-username/", change_username),
|
| 21 |
+
path("me/", me),
|
| 22 |
+
path("set-avatar/", set_avatar),
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
from .views import (
|
| 26 |
+
forgot_password_send_otp,
|
| 27 |
+
forgot_password_verify_otp,
|
| 28 |
+
forgot_password_reset,
|
| 29 |
+
me
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
urlpatterns += [
|
| 33 |
+
path("forgot-password/send-otp/", forgot_password_send_otp),
|
| 34 |
+
path("forgot-password/verify-otp/", forgot_password_verify_otp),
|
| 35 |
+
path("forgot-password/reset-password/", forgot_password_reset),
|
| 36 |
+
path("me/", me),
|
| 37 |
+
|
| 38 |
+
]
|
basho_backend/apps/accounts/views.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from rest_framework.decorators import api_view, permission_classes, authentication_classes
|
| 2 |
+
from rest_framework.permissions import IsAuthenticated
|
| 3 |
+
from rest_framework.response import Response
|
| 4 |
+
from django.contrib.auth import authenticate
|
| 5 |
+
from rest_framework_simplejwt.tokens import RefreshToken
|
| 6 |
+
from rest_framework_simplejwt.authentication import JWTAuthentication
|
| 7 |
+
from django.utils import timezone
|
| 8 |
+
from datetime import timedelta
|
| 9 |
+
from django.views.decorators.csrf import csrf_exempt
|
| 10 |
+
from django.contrib.auth import get_user_model
|
| 11 |
+
from django.utils.crypto import get_random_string
|
| 12 |
+
import re
|
| 13 |
+
|
| 14 |
+
from .models import User, EmailOTP
|
| 15 |
+
from .otp import generate_otp
|
| 16 |
+
from .services import send_otp_email, send_welcome_email
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
import os
|
| 20 |
+
from django.core.files import File
|
| 21 |
+
from django.conf import settings
|
| 22 |
+
from django.views.decorators.csrf import csrf_exempt
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# ---------------- HELPERS ----------------
|
| 27 |
+
|
| 28 |
+
def is_strong_password(password):
|
| 29 |
+
if len(password) < 8:
|
| 30 |
+
return False
|
| 31 |
+
if not re.search(r"[A-Z]", password):
|
| 32 |
+
return False
|
| 33 |
+
if not re.search(r"[^A-Za-z0-9]", password):
|
| 34 |
+
return False
|
| 35 |
+
return True
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# ---------------- AUTH ----------------
|
| 39 |
+
|
| 40 |
+
@api_view(["POST"])
|
| 41 |
+
def send_otp(request):
|
| 42 |
+
email = request.data.get("email")
|
| 43 |
+
username = request.data.get("username")
|
| 44 |
+
|
| 45 |
+
if not email or not username:
|
| 46 |
+
return Response({"error": "Email and username required"}, status=400)
|
| 47 |
+
|
| 48 |
+
if User.objects.filter(username=username).exists():
|
| 49 |
+
return Response({"error": "Username already exists"}, status=400)
|
| 50 |
+
|
| 51 |
+
if User.objects.filter(email=email).exists():
|
| 52 |
+
return Response({"error": "Email already registered"}, status=400)
|
| 53 |
+
|
| 54 |
+
otp = generate_otp()
|
| 55 |
+
|
| 56 |
+
EmailOTP.objects.update_or_create(
|
| 57 |
+
email=email,
|
| 58 |
+
defaults={"otp": otp, "created_at": timezone.now()}
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
send_otp_email(email, otp)
|
| 62 |
+
return Response({"success": "OTP sent"})
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
@api_view(["POST"])
|
| 66 |
+
def register_user(request):
|
| 67 |
+
email = request.data.get("email")
|
| 68 |
+
username = request.data.get("username")
|
| 69 |
+
password = request.data.get("password")
|
| 70 |
+
otp = request.data.get("otp")
|
| 71 |
+
|
| 72 |
+
if not all([email, username, password, otp]):
|
| 73 |
+
return Response({"error": "All fields are required"}, status=400)
|
| 74 |
+
|
| 75 |
+
otp_obj = EmailOTP.objects.filter(email=email, otp=otp).first()
|
| 76 |
+
if not otp_obj:
|
| 77 |
+
return Response({"error": "Incorrect OTP"}, status=400)
|
| 78 |
+
|
| 79 |
+
if timezone.now() - otp_obj.created_at > timedelta(minutes=5):
|
| 80 |
+
otp_obj.delete()
|
| 81 |
+
return Response({"error": "OTP expired"}, status=400)
|
| 82 |
+
|
| 83 |
+
if not is_strong_password(password):
|
| 84 |
+
return Response(
|
| 85 |
+
{"error": "Password must be at least 8 characters, include 1 uppercase letter and 1 special character"},
|
| 86 |
+
status=400
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
user = User.objects.create_user(
|
| 90 |
+
username=username,
|
| 91 |
+
email=email,
|
| 92 |
+
password=password
|
| 93 |
+
)
|
| 94 |
+
user.is_email_verified = True
|
| 95 |
+
user.save()
|
| 96 |
+
|
| 97 |
+
send_welcome_email(email, username)
|
| 98 |
+
otp_obj.delete()
|
| 99 |
+
|
| 100 |
+
refresh = RefreshToken.for_user(user)
|
| 101 |
+
|
| 102 |
+
return Response({
|
| 103 |
+
"access": str(refresh.access_token),
|
| 104 |
+
"refresh": str(refresh),
|
| 105 |
+
"username": user.username,
|
| 106 |
+
})
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
@api_view(["POST"])
|
| 110 |
+
def login_user(request):
|
| 111 |
+
user = authenticate(
|
| 112 |
+
username=request.data.get("username"),
|
| 113 |
+
password=request.data.get("password")
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
if not user:
|
| 117 |
+
return Response({"error": "Incorrect username or password"}, status=400)
|
| 118 |
+
|
| 119 |
+
refresh = RefreshToken.for_user(user)
|
| 120 |
+
|
| 121 |
+
return Response({
|
| 122 |
+
"access": str(refresh.access_token),
|
| 123 |
+
"refresh": str(refresh),
|
| 124 |
+
"username": user.username,
|
| 125 |
+
"email": user.email,
|
| 126 |
+
"avatar": user.avatar,
|
| 127 |
+
|
| 128 |
+
})
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
@api_view(["POST"])
|
| 132 |
+
def google_login(request):
|
| 133 |
+
email = request.data.get("email")
|
| 134 |
+
user = User.objects.filter(email=email).first()
|
| 135 |
+
|
| 136 |
+
if not user:
|
| 137 |
+
return Response({"error": "Account not found. Please register first."}, status=400)
|
| 138 |
+
|
| 139 |
+
refresh = RefreshToken.for_user(user)
|
| 140 |
+
|
| 141 |
+
return Response({
|
| 142 |
+
"access": str(refresh.access_token),
|
| 143 |
+
"refresh": str(refresh),
|
| 144 |
+
"username": user.username,
|
| 145 |
+
"email": user.email,
|
| 146 |
+
"avatar": user.avatar,
|
| 147 |
+
|
| 148 |
+
})
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
@api_view(["POST"])
|
| 152 |
+
def google_register(request):
|
| 153 |
+
email = request.data.get("email")
|
| 154 |
+
username = request.data.get("username")
|
| 155 |
+
|
| 156 |
+
password = get_random_string(12)
|
| 157 |
+
|
| 158 |
+
user = User.objects.create_user(
|
| 159 |
+
username=username,
|
| 160 |
+
email=email,
|
| 161 |
+
password=password
|
| 162 |
+
)
|
| 163 |
+
user.is_email_verified = True
|
| 164 |
+
user.save()
|
| 165 |
+
|
| 166 |
+
refresh = RefreshToken.for_user(user)
|
| 167 |
+
|
| 168 |
+
return Response({
|
| 169 |
+
"access": str(refresh.access_token),
|
| 170 |
+
"refresh": str(refresh),
|
| 171 |
+
"username": user.username,
|
| 172 |
+
})
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
# ---------------- CHANGE USERNAME (UNCHANGED LOGIC) ----------------
|
| 176 |
+
|
| 177 |
+
@csrf_exempt
|
| 178 |
+
@api_view(["POST"])
|
| 179 |
+
@permission_classes([IsAuthenticated])
|
| 180 |
+
def change_username(request):
|
| 181 |
+
user = request.user
|
| 182 |
+
new_username = request.data.get("username", "").strip()
|
| 183 |
+
|
| 184 |
+
if not new_username:
|
| 185 |
+
return Response({"error": "Username cannot be empty"}, status=400)
|
| 186 |
+
|
| 187 |
+
if User.objects.filter(username=new_username).exclude(id=user.id).exists():
|
| 188 |
+
return Response({"error": "Username already taken"}, status=400)
|
| 189 |
+
|
| 190 |
+
user.username = new_username
|
| 191 |
+
user.save(update_fields=["username"])
|
| 192 |
+
|
| 193 |
+
return Response({"username": user.username})
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
# ---------------- PROFILE PICTURE ----------------
|
| 197 |
+
|
| 198 |
+
@api_view(["POST"])
|
| 199 |
+
@authentication_classes([JWTAuthentication])
|
| 200 |
+
@permission_classes([IsAuthenticated])
|
| 201 |
+
def set_avatar(request):
|
| 202 |
+
user = request.user
|
| 203 |
+
avatar = request.data.get("avatar")
|
| 204 |
+
|
| 205 |
+
if not avatar:
|
| 206 |
+
return Response({"error": "Avatar is required"}, status=400)
|
| 207 |
+
|
| 208 |
+
# optional safety check
|
| 209 |
+
allowed_avatars = [
|
| 210 |
+
"p1.png", "p2.png", "p3.png", "p4.png", "p5.png",
|
| 211 |
+
"p6.png", "p7.png", "p8.png", "p9.png", "p10.png",
|
| 212 |
+
"p11.png", "p12.png", "p13.png", "p14.png",
|
| 213 |
+
"p15.png", "p16.png", "p17.png",
|
| 214 |
+
]
|
| 215 |
+
|
| 216 |
+
if avatar not in allowed_avatars:
|
| 217 |
+
return Response({"error": "Invalid avatar"}, status=400)
|
| 218 |
+
|
| 219 |
+
user.avatar = avatar
|
| 220 |
+
user.save(update_fields=["avatar"])
|
| 221 |
+
|
| 222 |
+
return Response({
|
| 223 |
+
"avatar": user.avatar
|
| 224 |
+
})
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
@api_view(["GET"])
|
| 228 |
+
@authentication_classes([JWTAuthentication])
|
| 229 |
+
@permission_classes([IsAuthenticated])
|
| 230 |
+
def me(request):
|
| 231 |
+
user = request.user
|
| 232 |
+
return Response({
|
| 233 |
+
"username": user.username,
|
| 234 |
+
"email": user.email,
|
| 235 |
+
"avatar": user.avatar,
|
| 236 |
+
|
| 237 |
+
})
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
# ---------------- FORGOT PASSWORD ----------------
|
| 242 |
+
|
| 243 |
+
@api_view(["POST"])
|
| 244 |
+
def forgot_password_send_otp(request):
|
| 245 |
+
email = request.data.get("email")
|
| 246 |
+
user = User.objects.filter(email=email).first()
|
| 247 |
+
|
| 248 |
+
if not user:
|
| 249 |
+
return Response({"error": "Incorrect email id entered"}, status=400)
|
| 250 |
+
|
| 251 |
+
otp = generate_otp()
|
| 252 |
+
EmailOTP.objects.update_or_create(
|
| 253 |
+
email=email,
|
| 254 |
+
defaults={"otp": otp, "created_at": timezone.now()}
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
send_otp_email(email, otp)
|
| 258 |
+
return Response({"success": "OTP sent"})
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
@api_view(["POST"])
|
| 262 |
+
def forgot_password_verify_otp(request):
|
| 263 |
+
email = request.data.get("email")
|
| 264 |
+
otp = request.data.get("otp")
|
| 265 |
+
|
| 266 |
+
otp_obj = EmailOTP.objects.filter(email=email, otp=otp).first()
|
| 267 |
+
if not otp_obj:
|
| 268 |
+
return Response({"error": "Incorrect OTP entered"}, status=400)
|
| 269 |
+
|
| 270 |
+
if timezone.now() - otp_obj.created_at > timedelta(minutes=5):
|
| 271 |
+
otp_obj.delete()
|
| 272 |
+
return Response({"error": "OTP expired"}, status=400)
|
| 273 |
+
|
| 274 |
+
return Response({"success": "OTP verified"})
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
@api_view(["POST"])
|
| 278 |
+
def forgot_password_reset(request):
|
| 279 |
+
email = request.data.get("email")
|
| 280 |
+
new_password = request.data.get("new_password")
|
| 281 |
+
confirm_password = request.data.get("confirm_password")
|
| 282 |
+
|
| 283 |
+
if new_password != confirm_password:
|
| 284 |
+
return Response({"error": "Passwords do not match"}, status=400)
|
| 285 |
+
|
| 286 |
+
if not is_strong_password(new_password):
|
| 287 |
+
return Response({"error": "Password must be strong"}, status=400)
|
| 288 |
+
|
| 289 |
+
user = User.objects.filter(email=email).first()
|
| 290 |
+
user.set_password(new_password)
|
| 291 |
+
user.save()
|
| 292 |
+
|
| 293 |
+
EmailOTP.objects.filter(email=email).delete()
|
| 294 |
+
return Response({"success": "Password reset successful"})
|
| 295 |
+
|
| 296 |
+
@api_view(["GET"])
|
| 297 |
+
@permission_classes([IsAuthenticated])
|
| 298 |
+
def me(request):
|
| 299 |
+
user = request.user
|
| 300 |
+
return Response({
|
| 301 |
+
"id": user.id,
|
| 302 |
+
"username": user.username,
|
| 303 |
+
"email": user.email,
|
| 304 |
+
})
|
basho_backend/apps/corporate/__init__.py
ADDED
|
File without changes
|
basho_backend/apps/corporate/admin.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.contrib import admin
|
| 2 |
+
from .models import CorporateInquiry
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
@admin.register(CorporateInquiry)
|
| 6 |
+
class CorporateInquiryAdmin(admin.ModelAdmin):
|
| 7 |
+
"""
|
| 8 |
+
Read-only Admin for Corporate Inquiries
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
# ✅ Show fields in list view
|
| 12 |
+
list_display = (
|
| 13 |
+
"company_name",
|
| 14 |
+
"contact_name",
|
| 15 |
+
"email",
|
| 16 |
+
"service_type",
|
| 17 |
+
"created_at",
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# ✅ Allow searching
|
| 21 |
+
search_fields = (
|
| 22 |
+
"company_name",
|
| 23 |
+
"contact_name",
|
| 24 |
+
"email",
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# ✅ Filters on right sidebar
|
| 28 |
+
list_filter = (
|
| 29 |
+
"service_type",
|
| 30 |
+
"created_at",
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# 🔒 Make ALL fields read-only
|
| 34 |
+
readonly_fields = [field.name for field in CorporateInquiry._meta.fields]
|
| 35 |
+
|
| 36 |
+
# ❌ Disable ADD permission
|
| 37 |
+
def has_add_permission(self, request):
|
| 38 |
+
return False
|
| 39 |
+
|
| 40 |
+
# ❌ Disable DELETE permission
|
| 41 |
+
def has_delete_permission(self, request, obj=None):
|
| 42 |
+
return False
|
| 43 |
+
|
| 44 |
+
# ❌ Disable EDIT (save) permission
|
| 45 |
+
def has_change_permission(self, request, obj=None):
|
| 46 |
+
# Allow viewing but not editing
|
| 47 |
+
if request.method in ["GET", "HEAD"]:
|
| 48 |
+
return True
|
| 49 |
+
return False
|
basho_backend/apps/corporate/apps.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.apps import AppConfig
|
| 2 |
+
|
| 3 |
+
class CorporateConfig(AppConfig):
|
| 4 |
+
default_auto_field = "django.db.models.BigAutoField"
|
| 5 |
+
name = "apps.corporate"
|
basho_backend/apps/corporate/migrations/0001_initial.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2026-01-09 19:44
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
initial = True
|
| 9 |
+
|
| 10 |
+
dependencies = [
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
operations = [
|
| 14 |
+
migrations.CreateModel(
|
| 15 |
+
name='CorporateInquiry',
|
| 16 |
+
fields=[
|
| 17 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 18 |
+
('company_name', models.CharField(max_length=255)),
|
| 19 |
+
('company_website', models.URLField(blank=True)),
|
| 20 |
+
('contact_name', models.CharField(max_length=255)),
|
| 21 |
+
('email', models.EmailField(max_length=254)),
|
| 22 |
+
('phone', models.CharField(blank=True, max_length=20)),
|
| 23 |
+
('service_type', models.CharField(choices=[('Corporate Gifting', 'Corporate Gifting'), ('Team Workshop', 'Team Workshop'), ('Brand Collaboration', 'Brand Collaboration')], max_length=50)),
|
| 24 |
+
('details', models.JSONField(blank=True, default=dict)),
|
| 25 |
+
('message', models.TextField(blank=True)),
|
| 26 |
+
('budget', models.CharField(blank=True, max_length=100)),
|
| 27 |
+
('timeline', models.CharField(blank=True, max_length=100)),
|
| 28 |
+
('consent', models.BooleanField(default=False)),
|
| 29 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 30 |
+
],
|
| 31 |
+
),
|
| 32 |
+
]
|
basho_backend/apps/corporate/migrations/0002_alter_corporateinquiry_options.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2026-01-12 15:27
|
| 2 |
+
|
| 3 |
+
from django.db import migrations
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('corporate', '0001_initial'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.AlterModelOptions(
|
| 14 |
+
name='corporateinquiry',
|
| 15 |
+
options={'ordering': ['-created_at']},
|
| 16 |
+
),
|
| 17 |
+
]
|
basho_backend/apps/corporate/migrations/__init__.py
ADDED
|
File without changes
|
basho_backend/apps/corporate/models.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.db import models
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class CorporateInquiry(models.Model):
|
| 5 |
+
SERVICE_CHOICES = [
|
| 6 |
+
("Corporate Gifting", "Corporate Gifting"),
|
| 7 |
+
("Team Workshop", "Team Workshop"),
|
| 8 |
+
("Brand Collaboration", "Brand Collaboration"),
|
| 9 |
+
]
|
| 10 |
+
|
| 11 |
+
company_name = models.CharField(max_length=255)
|
| 12 |
+
company_website = models.URLField(blank=True)
|
| 13 |
+
|
| 14 |
+
contact_name = models.CharField(max_length=255)
|
| 15 |
+
email = models.EmailField()
|
| 16 |
+
phone = models.CharField(max_length=20, blank=True)
|
| 17 |
+
|
| 18 |
+
service_type = models.CharField(
|
| 19 |
+
max_length=50,
|
| 20 |
+
choices=SERVICE_CHOICES
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
# dynamic form data stored safely
|
| 24 |
+
details = models.JSONField(default=dict, blank=True)
|
| 25 |
+
|
| 26 |
+
message = models.TextField(blank=True)
|
| 27 |
+
budget = models.CharField(max_length=100, blank=True)
|
| 28 |
+
timeline = models.CharField(max_length=100, blank=True)
|
| 29 |
+
|
| 30 |
+
consent = models.BooleanField(default=False)
|
| 31 |
+
|
| 32 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 33 |
+
|
| 34 |
+
def __str__(self):
|
| 35 |
+
return f"{self.company_name} | {self.service_type}"
|
| 36 |
+
|
| 37 |
+
class Meta:
|
| 38 |
+
ordering = ["-created_at"]
|
| 39 |
+
|
basho_backend/apps/corporate/tests.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.test import TestCase
|
| 2 |
+
|
| 3 |
+
# Create your tests here.
|
basho_backend/apps/corporate/urls.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.urls import path
|
| 2 |
+
from .views import corporate_inquiry
|
| 3 |
+
|
| 4 |
+
urlpatterns = [
|
| 5 |
+
path("corporate-inquiry/", corporate_inquiry),
|
| 6 |
+
]
|
basho_backend/apps/corporate/views.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from rest_framework.decorators import api_view
|
| 2 |
+
from rest_framework.response import Response
|
| 3 |
+
from rest_framework import status
|
| 4 |
+
|
| 5 |
+
from .models import CorporateInquiry
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@api_view(["POST"])
|
| 9 |
+
def corporate_inquiry(request):
|
| 10 |
+
data = request.data
|
| 11 |
+
|
| 12 |
+
# 🔒 REQUIRED FIELDS CHECK
|
| 13 |
+
required_fields = [
|
| 14 |
+
"companyName",
|
| 15 |
+
"contactName",
|
| 16 |
+
"email",
|
| 17 |
+
"serviceType",
|
| 18 |
+
"consent",
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
for field in required_fields:
|
| 22 |
+
if not data.get(field):
|
| 23 |
+
return Response(
|
| 24 |
+
{"error": f"{field} is required"},
|
| 25 |
+
status=status.HTTP_400_BAD_REQUEST,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# ✅ Extract dynamic service-specific fields safely
|
| 29 |
+
details = data.copy()
|
| 30 |
+
|
| 31 |
+
for key in [
|
| 32 |
+
"companyName",
|
| 33 |
+
"companyWebsite",
|
| 34 |
+
"contactName",
|
| 35 |
+
"email",
|
| 36 |
+
"phone",
|
| 37 |
+
"serviceType",
|
| 38 |
+
"message",
|
| 39 |
+
"budget",
|
| 40 |
+
"timeline",
|
| 41 |
+
"consent",
|
| 42 |
+
]:
|
| 43 |
+
details.pop(key, None)
|
| 44 |
+
|
| 45 |
+
inquiry = CorporateInquiry.objects.create(
|
| 46 |
+
company_name=data["companyName"],
|
| 47 |
+
company_website=data.get("companyWebsite", ""),
|
| 48 |
+
contact_name=data["contactName"],
|
| 49 |
+
email=data["email"],
|
| 50 |
+
phone=data.get("phone", ""),
|
| 51 |
+
service_type=data["serviceType"],
|
| 52 |
+
details=details,
|
| 53 |
+
message=data.get("message", ""),
|
| 54 |
+
budget=data.get("budget", ""),
|
| 55 |
+
timeline=data.get("timeline", ""),
|
| 56 |
+
consent=bool(data.get("consent")),
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
return Response(
|
| 60 |
+
{
|
| 61 |
+
"success": True,
|
| 62 |
+
"id": inquiry.id,
|
| 63 |
+
"message": "Inquiry submitted successfully",
|
| 64 |
+
},
|
| 65 |
+
status=status.HTTP_201_CREATED,
|
| 66 |
+
)
|
basho_backend/apps/experiences/__init__.py
ADDED
|
File without changes
|
basho_backend/apps/experiences/admin.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.contrib import admin
|
| 2 |
+
from .models import (
|
| 3 |
+
Experience,
|
| 4 |
+
Booking,
|
| 5 |
+
StudioBooking,
|
| 6 |
+
UpcomingEvent,
|
| 7 |
+
Workshop,
|
| 8 |
+
WorkshopSlot,
|
| 9 |
+
WorkshopRegistration,
|
| 10 |
+
ExperienceSlot,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
# ---------- BASIC MODELS ----------
|
| 14 |
+
@admin.register(Experience)
|
| 15 |
+
class ExperienceAdmin(admin.ModelAdmin):
|
| 16 |
+
list_display = ("title", "price", "is_active")
|
| 17 |
+
list_filter = ("is_active",)
|
| 18 |
+
search_fields = ("title",)
|
| 19 |
+
|
| 20 |
+
@admin.register(ExperienceSlot)
|
| 21 |
+
class ExperienceSlotAdmin(admin.ModelAdmin):
|
| 22 |
+
list_display = (
|
| 23 |
+
"experience",
|
| 24 |
+
"date",
|
| 25 |
+
"start_time",
|
| 26 |
+
"end_time",
|
| 27 |
+
"total_slots",
|
| 28 |
+
"booked_slots",
|
| 29 |
+
"is_active",
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
list_filter = ("experience", "date", "is_active")
|
| 33 |
+
search_fields = ("experience__title",)
|
| 34 |
+
ordering = ("date", "start_time")
|
| 35 |
+
|
| 36 |
+
readonly_fields = ("booked_slots",)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@admin.register(Booking)
|
| 40 |
+
class BookingAdmin(admin.ModelAdmin):
|
| 41 |
+
list_display = (
|
| 42 |
+
"id",
|
| 43 |
+
"full_name",
|
| 44 |
+
"experience",
|
| 45 |
+
"status",
|
| 46 |
+
"payment_status_display",
|
| 47 |
+
"created_at",
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
list_filter = ("status",)
|
| 51 |
+
|
| 52 |
+
def payment_status_display(self, obj):
|
| 53 |
+
if obj.payment_order:
|
| 54 |
+
return obj.payment_order.status
|
| 55 |
+
return "NO PAYMENT"
|
| 56 |
+
|
| 57 |
+
payment_status_display.short_description = "Payment Status"
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@admin.register(StudioBooking)
|
| 61 |
+
class StudioBookingAdmin(admin.ModelAdmin):
|
| 62 |
+
list_display = ("full_name", "email", "phone", "visit_date", "time_slot", "created_at")
|
| 63 |
+
search_fields = ("full_name", "email", "phone")
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@admin.register(UpcomingEvent)
|
| 67 |
+
class UpcomingEventAdmin(admin.ModelAdmin):
|
| 68 |
+
list_display = ("title", "date", "location")
|
| 69 |
+
search_fields = ("title",)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# ---------- WORKSHOPS ----------
|
| 73 |
+
@admin.register(Workshop)
|
| 74 |
+
class WorkshopAdmin(admin.ModelAdmin):
|
| 75 |
+
list_display = (
|
| 76 |
+
"name",
|
| 77 |
+
"type",
|
| 78 |
+
"level",
|
| 79 |
+
"price",
|
| 80 |
+
"featured",
|
| 81 |
+
"is_active",
|
| 82 |
+
)
|
| 83 |
+
list_filter = ("type", "level", "featured", "is_active")
|
| 84 |
+
search_fields = ("name", "description", "instructor")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
@admin.register(WorkshopSlot)
|
| 88 |
+
class WorkshopSlotAdmin(admin.ModelAdmin):
|
| 89 |
+
list_display = (
|
| 90 |
+
"workshop",
|
| 91 |
+
"date",
|
| 92 |
+
"start_time",
|
| 93 |
+
"end_time",
|
| 94 |
+
"available_spots",
|
| 95 |
+
"is_available",
|
| 96 |
+
)
|
| 97 |
+
list_filter = ("date", "is_available")
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@admin.register(WorkshopRegistration)
|
| 101 |
+
class WorkshopRegistrationAdmin(admin.ModelAdmin):
|
| 102 |
+
list_display = (
|
| 103 |
+
"id",
|
| 104 |
+
"name",
|
| 105 |
+
"workshop",
|
| 106 |
+
"status",
|
| 107 |
+
"payment_status_display",
|
| 108 |
+
"created_at",
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
def payment_status_display(self, obj):
|
| 112 |
+
if obj.payment_order:
|
| 113 |
+
return obj.payment_order.status
|
| 114 |
+
return "NO PAYMENT"
|
| 115 |
+
|
| 116 |
+
payment_status_display.short_description = "Payment Status"
|
basho_backend/apps/experiences/apps.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.apps import AppConfig
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class ExperiencesConfig(AppConfig):
|
| 5 |
+
name = "apps.experiences"
|
basho_backend/apps/experiences/migrations/0001_initial.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2026-01-07 15:56
|
| 2 |
+
|
| 3 |
+
import django.db.models.deletion
|
| 4 |
+
from django.db import migrations, models
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Migration(migrations.Migration):
|
| 8 |
+
|
| 9 |
+
initial = True
|
| 10 |
+
|
| 11 |
+
dependencies = [
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
operations = [
|
| 15 |
+
migrations.CreateModel(
|
| 16 |
+
name='Experience',
|
| 17 |
+
fields=[
|
| 18 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 19 |
+
('title', models.CharField(max_length=200)),
|
| 20 |
+
('tagline', models.CharField(max_length=200)),
|
| 21 |
+
('description', models.TextField()),
|
| 22 |
+
('duration', models.CharField(max_length=50)),
|
| 23 |
+
('people', models.CharField(max_length=50)),
|
| 24 |
+
('price', models.IntegerField()),
|
| 25 |
+
('image', models.ImageField(upload_to='experiences/')),
|
| 26 |
+
('is_active', models.BooleanField(default=True)),
|
| 27 |
+
],
|
| 28 |
+
),
|
| 29 |
+
migrations.CreateModel(
|
| 30 |
+
name='Booking',
|
| 31 |
+
fields=[
|
| 32 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 33 |
+
('full_name', models.CharField(max_length=100)),
|
| 34 |
+
('phone', models.CharField(max_length=15)),
|
| 35 |
+
('email', models.EmailField(max_length=254)),
|
| 36 |
+
('booking_date', models.DateField()),
|
| 37 |
+
('number_of_people', models.IntegerField(default=2)),
|
| 38 |
+
('status', models.CharField(choices=[('pending', 'Pending'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled')], default='pending', max_length=20)),
|
| 39 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 40 |
+
('experience', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bookings', to='experiences.experience')),
|
| 41 |
+
],
|
| 42 |
+
),
|
| 43 |
+
]
|
basho_backend/apps/experiences/migrations/0002_booking_payment_amount_booking_payment_status.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2026-01-07 21:07
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('experiences', '0001_initial'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.AddField(
|
| 14 |
+
model_name='booking',
|
| 15 |
+
name='payment_amount',
|
| 16 |
+
field=models.IntegerField(default=0),
|
| 17 |
+
),
|
| 18 |
+
migrations.AddField(
|
| 19 |
+
model_name='booking',
|
| 20 |
+
name='payment_status',
|
| 21 |
+
field=models.CharField(choices=[('pending', 'Pending'), ('paid', 'Paid'), ('failed', 'Failed')], default='pending', max_length=20),
|
| 22 |
+
),
|
| 23 |
+
]
|
basho_backend/apps/experiences/migrations/0003_studiobooking_upcomingevent.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2026-01-07 21:58
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('experiences', '0002_booking_payment_amount_booking_payment_status'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.CreateModel(
|
| 14 |
+
name='StudioBooking',
|
| 15 |
+
fields=[
|
| 16 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 17 |
+
('full_name', models.CharField(max_length=100)),
|
| 18 |
+
('phone', models.CharField(max_length=15)),
|
| 19 |
+
('email', models.EmailField(max_length=254)),
|
| 20 |
+
('visit_date', models.DateField()),
|
| 21 |
+
('time_slot', models.CharField(max_length=50)),
|
| 22 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 23 |
+
],
|
| 24 |
+
),
|
| 25 |
+
migrations.CreateModel(
|
| 26 |
+
name='UpcomingEvent',
|
| 27 |
+
fields=[
|
| 28 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 29 |
+
('title', models.CharField(max_length=200)),
|
| 30 |
+
('date', models.CharField(max_length=50)),
|
| 31 |
+
('location', models.CharField(max_length=200)),
|
| 32 |
+
('description', models.TextField()),
|
| 33 |
+
('badge', models.CharField(default='✨ Upcoming', max_length=50)),
|
| 34 |
+
],
|
| 35 |
+
),
|
| 36 |
+
]
|
basho_backend/apps/experiences/migrations/0004_workshop_workshopslot_workshopregistration.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2026-01-08 18:20
|
| 2 |
+
|
| 3 |
+
import django.db.models.deletion
|
| 4 |
+
from django.db import migrations, models
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Migration(migrations.Migration):
|
| 8 |
+
|
| 9 |
+
dependencies = [
|
| 10 |
+
('experiences', '0003_studiobooking_upcomingevent'),
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
operations = [
|
| 14 |
+
migrations.CreateModel(
|
| 15 |
+
name='Workshop',
|
| 16 |
+
fields=[
|
| 17 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 18 |
+
('name', models.CharField(max_length=200)),
|
| 19 |
+
('type', models.CharField(choices=[('group', 'Group'), ('private', 'Private'), ('experience', 'Experience')], max_length=20)),
|
| 20 |
+
('level', models.CharField(choices=[('beginner', 'Beginner'), ('intermediate', 'Intermediate'), ('advanced', 'Advanced')], max_length=20)),
|
| 21 |
+
('experience_type', models.CharField(blank=True, choices=[('couples_date', 'Couple’s Date'), ('birthday_party', 'Birthday Party'), ('corporate', 'Corporate'), ('masterclass', 'Masterclass')], max_length=30, null=True)),
|
| 22 |
+
('description', models.TextField()),
|
| 23 |
+
('long_description', models.TextField()),
|
| 24 |
+
('images', models.JSONField(default=list)),
|
| 25 |
+
('duration', models.CharField(max_length=50)),
|
| 26 |
+
('min_participants', models.PositiveIntegerField()),
|
| 27 |
+
('max_participants', models.PositiveIntegerField()),
|
| 28 |
+
('price', models.PositiveIntegerField()),
|
| 29 |
+
('price_per_person', models.BooleanField(default=True)),
|
| 30 |
+
('includes', models.JSONField(default=list)),
|
| 31 |
+
('requirements', models.JSONField(blank=True, null=True)),
|
| 32 |
+
('provided_materials', models.JSONField(default=list)),
|
| 33 |
+
('location', models.CharField(max_length=200)),
|
| 34 |
+
('instructor', models.CharField(max_length=100)),
|
| 35 |
+
('take_home', models.CharField(max_length=200)),
|
| 36 |
+
('certificate', models.BooleanField(default=False)),
|
| 37 |
+
('lunch_included', models.BooleanField(default=False)),
|
| 38 |
+
('featured', models.BooleanField(default=False)),
|
| 39 |
+
('is_active', models.BooleanField(default=True)),
|
| 40 |
+
],
|
| 41 |
+
),
|
| 42 |
+
migrations.CreateModel(
|
| 43 |
+
name='WorkshopSlot',
|
| 44 |
+
fields=[
|
| 45 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 46 |
+
('date', models.DateField()),
|
| 47 |
+
('start_time', models.TimeField()),
|
| 48 |
+
('end_time', models.TimeField()),
|
| 49 |
+
('available_spots', models.PositiveIntegerField()),
|
| 50 |
+
('is_available', models.BooleanField(default=True)),
|
| 51 |
+
('workshop', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slots', to='experiences.workshop')),
|
| 52 |
+
],
|
| 53 |
+
),
|
| 54 |
+
migrations.CreateModel(
|
| 55 |
+
name='WorkshopRegistration',
|
| 56 |
+
fields=[
|
| 57 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 58 |
+
('name', models.CharField(max_length=100)),
|
| 59 |
+
('email', models.EmailField(max_length=254)),
|
| 60 |
+
('phone', models.CharField(max_length=15)),
|
| 61 |
+
('number_of_participants', models.PositiveIntegerField()),
|
| 62 |
+
('special_requests', models.TextField(blank=True, null=True)),
|
| 63 |
+
('gst_number', models.CharField(blank=True, max_length=50, null=True)),
|
| 64 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 65 |
+
('workshop', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='registrations', to='experiences.workshop')),
|
| 66 |
+
('slot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='registrations', to='experiences.workshopslot')),
|
| 67 |
+
],
|
| 68 |
+
),
|
| 69 |
+
]
|
basho_backend/apps/experiences/migrations/0005_alter_booking_options_alter_experience_options_and_more.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2026-01-10 10:20
|
| 2 |
+
|
| 3 |
+
import django.core.validators
|
| 4 |
+
import django.utils.timezone
|
| 5 |
+
from django.db import migrations, models
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Migration(migrations.Migration):
|
| 9 |
+
|
| 10 |
+
dependencies = [
|
| 11 |
+
('experiences', '0004_workshop_workshopslot_workshopregistration'),
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
operations = [
|
| 15 |
+
migrations.AlterModelOptions(
|
| 16 |
+
name='booking',
|
| 17 |
+
options={'ordering': ['-created_at']},
|
| 18 |
+
),
|
| 19 |
+
migrations.AlterModelOptions(
|
| 20 |
+
name='experience',
|
| 21 |
+
options={'verbose_name_plural': 'Experiences'},
|
| 22 |
+
),
|
| 23 |
+
migrations.AlterModelOptions(
|
| 24 |
+
name='studiobooking',
|
| 25 |
+
options={'ordering': ['visit_date', 'time_slot'], 'verbose_name_plural': 'Studio Bookings'},
|
| 26 |
+
),
|
| 27 |
+
migrations.AlterModelOptions(
|
| 28 |
+
name='upcomingevent',
|
| 29 |
+
options={'ordering': ['date']},
|
| 30 |
+
),
|
| 31 |
+
migrations.AlterModelOptions(
|
| 32 |
+
name='workshop',
|
| 33 |
+
options={'ordering': ['name']},
|
| 34 |
+
),
|
| 35 |
+
migrations.AlterModelOptions(
|
| 36 |
+
name='workshopregistration',
|
| 37 |
+
options={'ordering': ['-created_at']},
|
| 38 |
+
),
|
| 39 |
+
migrations.AlterModelOptions(
|
| 40 |
+
name='workshopslot',
|
| 41 |
+
options={'ordering': ['date', 'start_time']},
|
| 42 |
+
),
|
| 43 |
+
migrations.AddField(
|
| 44 |
+
model_name='workshop',
|
| 45 |
+
name='created_at',
|
| 46 |
+
field=models.DateTimeField(default=django.utils.timezone.now),
|
| 47 |
+
),
|
| 48 |
+
migrations.AddField(
|
| 49 |
+
model_name='workshop',
|
| 50 |
+
name='updated_at',
|
| 51 |
+
field=models.DateTimeField(default=django.utils.timezone.now),
|
| 52 |
+
),
|
| 53 |
+
migrations.AddField(
|
| 54 |
+
model_name='workshopregistration',
|
| 55 |
+
name='payment_amount',
|
| 56 |
+
field=models.PositiveIntegerField(default=0),
|
| 57 |
+
),
|
| 58 |
+
migrations.AddField(
|
| 59 |
+
model_name='workshopregistration',
|
| 60 |
+
name='payment_status',
|
| 61 |
+
field=models.CharField(choices=[('pending', 'Pending'), ('paid', 'Paid'), ('failed', 'Failed')], default='pending', max_length=20),
|
| 62 |
+
),
|
| 63 |
+
migrations.AddField(
|
| 64 |
+
model_name='workshopregistration',
|
| 65 |
+
name='updated_at',
|
| 66 |
+
field=models.DateTimeField(default=django.utils.timezone.now),
|
| 67 |
+
),
|
| 68 |
+
migrations.AddField(
|
| 69 |
+
model_name='workshopslot',
|
| 70 |
+
name='created_at',
|
| 71 |
+
field=models.DateTimeField(default=django.utils.timezone.now),
|
| 72 |
+
),
|
| 73 |
+
migrations.AddField(
|
| 74 |
+
model_name='workshopslot',
|
| 75 |
+
name='updated_at',
|
| 76 |
+
field=models.DateTimeField(default=django.utils.timezone.now),
|
| 77 |
+
),
|
| 78 |
+
migrations.AlterField(
|
| 79 |
+
model_name='booking',
|
| 80 |
+
name='created_at',
|
| 81 |
+
field=models.DateTimeField(default=django.utils.timezone.now),
|
| 82 |
+
),
|
| 83 |
+
migrations.AlterField(
|
| 84 |
+
model_name='workshop',
|
| 85 |
+
name='certificate',
|
| 86 |
+
field=models.BooleanField(default=False, help_text='Is certificate provided?'),
|
| 87 |
+
),
|
| 88 |
+
migrations.AlterField(
|
| 89 |
+
model_name='workshop',
|
| 90 |
+
name='description',
|
| 91 |
+
field=models.TextField(help_text='Short description for cards'),
|
| 92 |
+
),
|
| 93 |
+
migrations.AlterField(
|
| 94 |
+
model_name='workshop',
|
| 95 |
+
name='duration',
|
| 96 |
+
field=models.CharField(help_text="e.g., '3 hours', '2-3 hours'", max_length=50),
|
| 97 |
+
),
|
| 98 |
+
migrations.AlterField(
|
| 99 |
+
model_name='workshop',
|
| 100 |
+
name='experience_type',
|
| 101 |
+
field=models.CharField(blank=True, choices=[('couples_date', "Couple's Date"), ('birthday_party', 'Birthday Party'), ('corporate', 'Corporate'), ('masterclass', 'Masterclass')], max_length=30, null=True),
|
| 102 |
+
),
|
| 103 |
+
migrations.AlterField(
|
| 104 |
+
model_name='workshop',
|
| 105 |
+
name='featured',
|
| 106 |
+
field=models.BooleanField(default=False, help_text='Show as featured workshop?'),
|
| 107 |
+
),
|
| 108 |
+
migrations.AlterField(
|
| 109 |
+
model_name='workshop',
|
| 110 |
+
name='images',
|
| 111 |
+
field=models.JSONField(default=list, help_text='List of image URLs/paths'),
|
| 112 |
+
),
|
| 113 |
+
migrations.AlterField(
|
| 114 |
+
model_name='workshop',
|
| 115 |
+
name='includes',
|
| 116 |
+
field=models.JSONField(default=list, help_text="List of what's included (materials, tools, etc.)"),
|
| 117 |
+
),
|
| 118 |
+
migrations.AlterField(
|
| 119 |
+
model_name='workshop',
|
| 120 |
+
name='instructor',
|
| 121 |
+
field=models.CharField(default='Shivangi', max_length=100),
|
| 122 |
+
),
|
| 123 |
+
migrations.AlterField(
|
| 124 |
+
model_name='workshop',
|
| 125 |
+
name='is_active',
|
| 126 |
+
field=models.BooleanField(default=True, help_text='Is workshop available for booking?'),
|
| 127 |
+
),
|
| 128 |
+
migrations.AlterField(
|
| 129 |
+
model_name='workshop',
|
| 130 |
+
name='location',
|
| 131 |
+
field=models.CharField(default='Basho Studio', max_length=200),
|
| 132 |
+
),
|
| 133 |
+
migrations.AlterField(
|
| 134 |
+
model_name='workshop',
|
| 135 |
+
name='long_description',
|
| 136 |
+
field=models.TextField(help_text='Detailed description for workshop page'),
|
| 137 |
+
),
|
| 138 |
+
migrations.AlterField(
|
| 139 |
+
model_name='workshop',
|
| 140 |
+
name='lunch_included',
|
| 141 |
+
field=models.BooleanField(default=False, help_text='Is lunch included?'),
|
| 142 |
+
),
|
| 143 |
+
migrations.AlterField(
|
| 144 |
+
model_name='workshop',
|
| 145 |
+
name='max_participants',
|
| 146 |
+
field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1)]),
|
| 147 |
+
),
|
| 148 |
+
migrations.AlterField(
|
| 149 |
+
model_name='workshop',
|
| 150 |
+
name='min_participants',
|
| 151 |
+
field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1)]),
|
| 152 |
+
),
|
| 153 |
+
migrations.AlterField(
|
| 154 |
+
model_name='workshop',
|
| 155 |
+
name='price',
|
| 156 |
+
field=models.PositiveIntegerField(help_text='Price in rupees'),
|
| 157 |
+
),
|
| 158 |
+
migrations.AlterField(
|
| 159 |
+
model_name='workshop',
|
| 160 |
+
name='price_per_person',
|
| 161 |
+
field=models.BooleanField(default=True, help_text='If True, price is per person. If False, price is for the group.'),
|
| 162 |
+
),
|
| 163 |
+
migrations.AlterField(
|
| 164 |
+
model_name='workshop',
|
| 165 |
+
name='provided_materials',
|
| 166 |
+
field=models.JSONField(default=list, help_text='Materials provided by studio'),
|
| 167 |
+
),
|
| 168 |
+
migrations.AlterField(
|
| 169 |
+
model_name='workshop',
|
| 170 |
+
name='requirements',
|
| 171 |
+
field=models.JSONField(blank=True, help_text='What participants need to bring/know', null=True),
|
| 172 |
+
),
|
| 173 |
+
migrations.AlterField(
|
| 174 |
+
model_name='workshop',
|
| 175 |
+
name='take_home',
|
| 176 |
+
field=models.CharField(help_text='What participants can take home', max_length=200),
|
| 177 |
+
),
|
| 178 |
+
migrations.AlterField(
|
| 179 |
+
model_name='workshopregistration',
|
| 180 |
+
name='created_at',
|
| 181 |
+
field=models.DateTimeField(default=django.utils.timezone.now),
|
| 182 |
+
),
|
| 183 |
+
migrations.AlterField(
|
| 184 |
+
model_name='workshopregistration',
|
| 185 |
+
name='number_of_participants',
|
| 186 |
+
field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1)]),
|
| 187 |
+
),
|
| 188 |
+
migrations.AlterField(
|
| 189 |
+
model_name='workshopslot',
|
| 190 |
+
name='available_spots',
|
| 191 |
+
field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(0)]),
|
| 192 |
+
),
|
| 193 |
+
migrations.AlterField(
|
| 194 |
+
model_name='workshopslot',
|
| 195 |
+
name='is_available',
|
| 196 |
+
field=models.BooleanField(default=True, help_text='Is this slot open for bookings?'),
|
| 197 |
+
),
|
| 198 |
+
migrations.AlterUniqueTogether(
|
| 199 |
+
name='workshopslot',
|
| 200 |
+
unique_together={('workshop', 'date', 'start_time')},
|
| 201 |
+
),
|
| 202 |
+
migrations.AddIndex(
|
| 203 |
+
model_name='workshopregistration',
|
| 204 |
+
index=models.Index(fields=['email'], name='experiences_email_b6e066_idx'),
|
| 205 |
+
),
|
| 206 |
+
migrations.AddIndex(
|
| 207 |
+
model_name='workshopregistration',
|
| 208 |
+
index=models.Index(fields=['created_at'], name='experiences_created_ce420a_idx'),
|
| 209 |
+
),
|
| 210 |
+
]
|
basho_backend/apps/experiences/migrations/0006_alter_booking_options_alter_experience_options_and_more.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2026-01-10 11:04
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('experiences', '0005_alter_booking_options_alter_experience_options_and_more'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.AlterModelOptions(
|
| 14 |
+
name='booking',
|
| 15 |
+
options={},
|
| 16 |
+
),
|
| 17 |
+
migrations.AlterModelOptions(
|
| 18 |
+
name='experience',
|
| 19 |
+
options={},
|
| 20 |
+
),
|
| 21 |
+
migrations.AlterModelOptions(
|
| 22 |
+
name='studiobooking',
|
| 23 |
+
options={},
|
| 24 |
+
),
|
| 25 |
+
migrations.AlterModelOptions(
|
| 26 |
+
name='upcomingevent',
|
| 27 |
+
options={},
|
| 28 |
+
),
|
| 29 |
+
migrations.AlterModelOptions(
|
| 30 |
+
name='workshop',
|
| 31 |
+
options={},
|
| 32 |
+
),
|
| 33 |
+
migrations.AlterModelOptions(
|
| 34 |
+
name='workshopregistration',
|
| 35 |
+
options={},
|
| 36 |
+
),
|
| 37 |
+
migrations.AlterModelOptions(
|
| 38 |
+
name='workshopslot',
|
| 39 |
+
options={},
|
| 40 |
+
),
|
| 41 |
+
migrations.RemoveIndex(
|
| 42 |
+
model_name='workshopregistration',
|
| 43 |
+
name='experiences_email_b6e066_idx',
|
| 44 |
+
),
|
| 45 |
+
migrations.RemoveIndex(
|
| 46 |
+
model_name='workshopregistration',
|
| 47 |
+
name='experiences_created_ce420a_idx',
|
| 48 |
+
),
|
| 49 |
+
migrations.AlterUniqueTogether(
|
| 50 |
+
name='workshopslot',
|
| 51 |
+
unique_together=set(),
|
| 52 |
+
),
|
| 53 |
+
migrations.RemoveField(
|
| 54 |
+
model_name='workshop',
|
| 55 |
+
name='created_at',
|
| 56 |
+
),
|
| 57 |
+
migrations.RemoveField(
|
| 58 |
+
model_name='workshop',
|
| 59 |
+
name='updated_at',
|
| 60 |
+
),
|
| 61 |
+
migrations.AlterField(
|
| 62 |
+
model_name='booking',
|
| 63 |
+
name='created_at',
|
| 64 |
+
field=models.DateTimeField(auto_now_add=True),
|
| 65 |
+
),
|
| 66 |
+
migrations.AlterField(
|
| 67 |
+
model_name='workshop',
|
| 68 |
+
name='certificate',
|
| 69 |
+
field=models.BooleanField(default=False),
|
| 70 |
+
),
|
| 71 |
+
migrations.AlterField(
|
| 72 |
+
model_name='workshop',
|
| 73 |
+
name='description',
|
| 74 |
+
field=models.TextField(),
|
| 75 |
+
),
|
| 76 |
+
migrations.AlterField(
|
| 77 |
+
model_name='workshop',
|
| 78 |
+
name='duration',
|
| 79 |
+
field=models.CharField(max_length=50),
|
| 80 |
+
),
|
| 81 |
+
migrations.AlterField(
|
| 82 |
+
model_name='workshop',
|
| 83 |
+
name='experience_type',
|
| 84 |
+
field=models.CharField(blank=True, choices=[('couples_date', 'Couple’s Date'), ('birthday_party', 'Birthday Party'), ('corporate', 'Corporate'), ('masterclass', 'Masterclass')], max_length=30, null=True),
|
| 85 |
+
),
|
| 86 |
+
migrations.AlterField(
|
| 87 |
+
model_name='workshop',
|
| 88 |
+
name='featured',
|
| 89 |
+
field=models.BooleanField(default=False),
|
| 90 |
+
),
|
| 91 |
+
migrations.AlterField(
|
| 92 |
+
model_name='workshop',
|
| 93 |
+
name='images',
|
| 94 |
+
field=models.JSONField(default=list),
|
| 95 |
+
),
|
| 96 |
+
migrations.AlterField(
|
| 97 |
+
model_name='workshop',
|
| 98 |
+
name='includes',
|
| 99 |
+
field=models.JSONField(default=list),
|
| 100 |
+
),
|
| 101 |
+
migrations.AlterField(
|
| 102 |
+
model_name='workshop',
|
| 103 |
+
name='instructor',
|
| 104 |
+
field=models.CharField(max_length=100),
|
| 105 |
+
),
|
| 106 |
+
migrations.AlterField(
|
| 107 |
+
model_name='workshop',
|
| 108 |
+
name='is_active',
|
| 109 |
+
field=models.BooleanField(default=True),
|
| 110 |
+
),
|
| 111 |
+
migrations.AlterField(
|
| 112 |
+
model_name='workshop',
|
| 113 |
+
name='location',
|
| 114 |
+
field=models.CharField(max_length=200),
|
| 115 |
+
),
|
| 116 |
+
migrations.AlterField(
|
| 117 |
+
model_name='workshop',
|
| 118 |
+
name='long_description',
|
| 119 |
+
field=models.TextField(),
|
| 120 |
+
),
|
| 121 |
+
migrations.AlterField(
|
| 122 |
+
model_name='workshop',
|
| 123 |
+
name='lunch_included',
|
| 124 |
+
field=models.BooleanField(default=False),
|
| 125 |
+
),
|
| 126 |
+
migrations.AlterField(
|
| 127 |
+
model_name='workshop',
|
| 128 |
+
name='max_participants',
|
| 129 |
+
field=models.PositiveIntegerField(),
|
| 130 |
+
),
|
| 131 |
+
migrations.AlterField(
|
| 132 |
+
model_name='workshop',
|
| 133 |
+
name='min_participants',
|
| 134 |
+
field=models.PositiveIntegerField(),
|
| 135 |
+
),
|
| 136 |
+
migrations.AlterField(
|
| 137 |
+
model_name='workshop',
|
| 138 |
+
name='price',
|
| 139 |
+
field=models.PositiveIntegerField(),
|
| 140 |
+
),
|
| 141 |
+
migrations.AlterField(
|
| 142 |
+
model_name='workshop',
|
| 143 |
+
name='price_per_person',
|
| 144 |
+
field=models.BooleanField(default=True),
|
| 145 |
+
),
|
| 146 |
+
migrations.AlterField(
|
| 147 |
+
model_name='workshop',
|
| 148 |
+
name='provided_materials',
|
| 149 |
+
field=models.JSONField(default=list),
|
| 150 |
+
),
|
| 151 |
+
migrations.AlterField(
|
| 152 |
+
model_name='workshop',
|
| 153 |
+
name='requirements',
|
| 154 |
+
field=models.JSONField(blank=True, null=True),
|
| 155 |
+
),
|
| 156 |
+
migrations.AlterField(
|
| 157 |
+
model_name='workshop',
|
| 158 |
+
name='take_home',
|
| 159 |
+
field=models.CharField(max_length=200),
|
| 160 |
+
),
|
| 161 |
+
migrations.AlterField(
|
| 162 |
+
model_name='workshopregistration',
|
| 163 |
+
name='created_at',
|
| 164 |
+
field=models.DateTimeField(auto_now_add=True),
|
| 165 |
+
),
|
| 166 |
+
migrations.AlterField(
|
| 167 |
+
model_name='workshopregistration',
|
| 168 |
+
name='number_of_participants',
|
| 169 |
+
field=models.PositiveIntegerField(),
|
| 170 |
+
),
|
| 171 |
+
migrations.AlterField(
|
| 172 |
+
model_name='workshopslot',
|
| 173 |
+
name='available_spots',
|
| 174 |
+
field=models.PositiveIntegerField(),
|
| 175 |
+
),
|
| 176 |
+
migrations.AlterField(
|
| 177 |
+
model_name='workshopslot',
|
| 178 |
+
name='is_available',
|
| 179 |
+
field=models.BooleanField(default=True),
|
| 180 |
+
),
|
| 181 |
+
migrations.RemoveField(
|
| 182 |
+
model_name='workshopregistration',
|
| 183 |
+
name='payment_amount',
|
| 184 |
+
),
|
| 185 |
+
migrations.RemoveField(
|
| 186 |
+
model_name='workshopregistration',
|
| 187 |
+
name='payment_status',
|
| 188 |
+
),
|
| 189 |
+
migrations.RemoveField(
|
| 190 |
+
model_name='workshopregistration',
|
| 191 |
+
name='updated_at',
|
| 192 |
+
),
|
| 193 |
+
migrations.RemoveField(
|
| 194 |
+
model_name='workshopslot',
|
| 195 |
+
name='created_at',
|
| 196 |
+
),
|
| 197 |
+
migrations.RemoveField(
|
| 198 |
+
model_name='workshopslot',
|
| 199 |
+
name='updated_at',
|
| 200 |
+
),
|
| 201 |
+
]
|
basho_backend/apps/experiences/migrations/0007_booking_otp_booking_otp_expires_at_experienceslot_and_more.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2026-01-10 19:09
|
| 2 |
+
|
| 3 |
+
import django.db.models.deletion
|
| 4 |
+
from django.db import migrations, models
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Migration(migrations.Migration):
|
| 8 |
+
|
| 9 |
+
dependencies = [
|
| 10 |
+
('experiences', '0006_alter_booking_options_alter_experience_options_and_more'),
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
operations = [
|
| 14 |
+
migrations.AddField(
|
| 15 |
+
model_name='booking',
|
| 16 |
+
name='otp',
|
| 17 |
+
field=models.CharField(blank=True, max_length=6, null=True),
|
| 18 |
+
),
|
| 19 |
+
migrations.AddField(
|
| 20 |
+
model_name='booking',
|
| 21 |
+
name='otp_expires_at',
|
| 22 |
+
field=models.DateTimeField(blank=True, null=True),
|
| 23 |
+
),
|
| 24 |
+
migrations.CreateModel(
|
| 25 |
+
name='ExperienceSlot',
|
| 26 |
+
fields=[
|
| 27 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 28 |
+
('date', models.DateField()),
|
| 29 |
+
('start_time', models.TimeField()),
|
| 30 |
+
('end_time', models.TimeField()),
|
| 31 |
+
('min_participants', models.PositiveIntegerField()),
|
| 32 |
+
('max_participants', models.PositiveIntegerField()),
|
| 33 |
+
('booked_participants', models.PositiveIntegerField(default=0)),
|
| 34 |
+
('is_active', models.BooleanField(default=True)),
|
| 35 |
+
('experience', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slots', to='experiences.experience')),
|
| 36 |
+
],
|
| 37 |
+
options={
|
| 38 |
+
'ordering': ['date', 'start_time'],
|
| 39 |
+
},
|
| 40 |
+
),
|
| 41 |
+
migrations.AddField(
|
| 42 |
+
model_name='booking',
|
| 43 |
+
name='slot',
|
| 44 |
+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='bookings', to='experiences.experienceslot'),
|
| 45 |
+
),
|
| 46 |
+
]
|
basho_backend/apps/experiences/migrations/0008_remove_booking_otp_remove_booking_otp_expires_at_and_more.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2026-01-12 18:51
|
| 2 |
+
|
| 3 |
+
import django.db.models.deletion
|
| 4 |
+
from django.db import migrations, models
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Migration(migrations.Migration):
|
| 8 |
+
|
| 9 |
+
dependencies = [
|
| 10 |
+
('experiences', '0007_booking_otp_booking_otp_expires_at_experienceslot_and_more'),
|
| 11 |
+
('orders', '0001_initial'),
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
operations = [
|
| 15 |
+
migrations.RemoveField(
|
| 16 |
+
model_name='booking',
|
| 17 |
+
name='otp',
|
| 18 |
+
),
|
| 19 |
+
migrations.RemoveField(
|
| 20 |
+
model_name='booking',
|
| 21 |
+
name='otp_expires_at',
|
| 22 |
+
),
|
| 23 |
+
migrations.RemoveField(
|
| 24 |
+
model_name='booking',
|
| 25 |
+
name='payment_status',
|
| 26 |
+
),
|
| 27 |
+
migrations.AddField(
|
| 28 |
+
model_name='booking',
|
| 29 |
+
name='payment_order',
|
| 30 |
+
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='experience_booking', to='orders.paymentorder'),
|
| 31 |
+
),
|
| 32 |
+
migrations.AddField(
|
| 33 |
+
model_name='workshopregistration',
|
| 34 |
+
name='payment_order',
|
| 35 |
+
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workshop_registration', to='orders.paymentorder'),
|
| 36 |
+
),
|
| 37 |
+
migrations.AddField(
|
| 38 |
+
model_name='workshopregistration',
|
| 39 |
+
name='status',
|
| 40 |
+
field=models.CharField(choices=[('pending', 'Pending'), ('confirmed', 'Confirmed'), ('failed', 'Failed')], default='pending', max_length=20),
|
| 41 |
+
),
|
| 42 |
+
migrations.AlterField(
|
| 43 |
+
model_name='booking',
|
| 44 |
+
name='number_of_people',
|
| 45 |
+
field=models.PositiveIntegerField(),
|
| 46 |
+
),
|
| 47 |
+
migrations.AlterField(
|
| 48 |
+
model_name='booking',
|
| 49 |
+
name='payment_amount',
|
| 50 |
+
field=models.PositiveIntegerField(),
|
| 51 |
+
),
|
| 52 |
+
migrations.AlterField(
|
| 53 |
+
model_name='booking',
|
| 54 |
+
name='status',
|
| 55 |
+
field=models.CharField(choices=[('pending', 'Pending'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('failed', 'Failed')], default='pending', max_length=20),
|
| 56 |
+
),
|
| 57 |
+
migrations.AlterField(
|
| 58 |
+
model_name='workshopregistration',
|
| 59 |
+
name='slot',
|
| 60 |
+
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='registrations', to='experiences.workshopslot'),
|
| 61 |
+
),
|
| 62 |
+
]
|
basho_backend/apps/experiences/migrations/0009_rename_booked_participants_experienceslot_booked_slots_and_more.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2026-01-15 09:40
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('experiences', '0008_remove_booking_otp_remove_booking_otp_expires_at_and_more'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.RenameField(
|
| 14 |
+
model_name='experienceslot',
|
| 15 |
+
old_name='booked_participants',
|
| 16 |
+
new_name='booked_slots',
|
| 17 |
+
),
|
| 18 |
+
migrations.RenameField(
|
| 19 |
+
model_name='experienceslot',
|
| 20 |
+
old_name='max_participants',
|
| 21 |
+
new_name='total_slots',
|
| 22 |
+
),
|
| 23 |
+
migrations.RemoveField(
|
| 24 |
+
model_name='experienceslot',
|
| 25 |
+
name='min_participants',
|
| 26 |
+
),
|
| 27 |
+
migrations.AddField(
|
| 28 |
+
model_name='experience',
|
| 29 |
+
name='max_participants',
|
| 30 |
+
field=models.PositiveIntegerField(blank=True, null=True),
|
| 31 |
+
),
|
| 32 |
+
migrations.AddField(
|
| 33 |
+
model_name='experience',
|
| 34 |
+
name='min_participants',
|
| 35 |
+
field=models.PositiveIntegerField(blank=True, null=True),
|
| 36 |
+
),
|
| 37 |
+
]
|
basho_backend/apps/experiences/migrations/0010_alter_experience_image.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0 on 2026-01-16 21:04
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('experiences', '0009_rename_booked_participants_experienceslot_booked_slots_and_more'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.AlterField(
|
| 14 |
+
model_name='experience',
|
| 15 |
+
name='image',
|
| 16 |
+
field=models.JSONField(blank=True, default=list),
|
| 17 |
+
),
|
| 18 |
+
]
|
basho_backend/apps/experiences/migrations/__init__.py
ADDED
|
File without changes
|
basho_backend/apps/experiences/models.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.db import models
|
| 2 |
+
from django.core.exceptions import ValidationError
|
| 3 |
+
|
| 4 |
+
class Experience(models.Model):
|
| 5 |
+
title = models.CharField(max_length=200)
|
| 6 |
+
tagline = models.CharField(max_length=200)
|
| 7 |
+
description = models.TextField()
|
| 8 |
+
duration = models.CharField(max_length=50)
|
| 9 |
+
people = models.CharField(max_length=50)
|
| 10 |
+
|
| 11 |
+
min_participants = models.PositiveIntegerField(null=True, blank=True)
|
| 12 |
+
max_participants = models.PositiveIntegerField(null=True, blank=True)
|
| 13 |
+
|
| 14 |
+
price = models.IntegerField()
|
| 15 |
+
image = models.JSONField(default=list, blank=True)
|
| 16 |
+
is_active = models.BooleanField(default=True)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def __str__(self):
|
| 20 |
+
return self.title
|
| 21 |
+
|
| 22 |
+
class ExperienceSlot(models.Model):
|
| 23 |
+
experience = models.ForeignKey(
|
| 24 |
+
Experience,
|
| 25 |
+
on_delete=models.CASCADE,
|
| 26 |
+
related_name="slots"
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
date = models.DateField()
|
| 30 |
+
start_time = models.TimeField()
|
| 31 |
+
end_time = models.TimeField()
|
| 32 |
+
total_slots = models.PositiveIntegerField()
|
| 33 |
+
booked_slots = models.PositiveIntegerField(default=0)
|
| 34 |
+
|
| 35 |
+
is_active = models.BooleanField(default=True)
|
| 36 |
+
|
| 37 |
+
def clean(self):
|
| 38 |
+
if self.start_time >= self.end_time:
|
| 39 |
+
raise ValidationError("Start time must be before end time.")
|
| 40 |
+
|
| 41 |
+
if self.booked_slots > self.total_slots:
|
| 42 |
+
raise ValidationError("Booked slots cannot exceed total slots.")
|
| 43 |
+
|
| 44 |
+
exp = self.experience
|
| 45 |
+
if exp.min_participants and exp.max_participants:
|
| 46 |
+
if exp.min_participants > exp.max_participants:
|
| 47 |
+
raise ValidationError(
|
| 48 |
+
"Experience min participants cannot exceed max participants."
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
def save(self, *args, **kwargs):
|
| 52 |
+
self.full_clean()
|
| 53 |
+
super().save(*args, **kwargs)
|
| 54 |
+
|
| 55 |
+
def __str__(self):
|
| 56 |
+
return f"{self.experience.title} | {self.date} {self.start_time}-{self.end_time}"
|
| 57 |
+
|
| 58 |
+
class Meta:
|
| 59 |
+
ordering = ["date", "start_time"]
|
| 60 |
+
|
| 61 |
+
class Booking(models.Model):
|
| 62 |
+
STATUS_CHOICES = (
|
| 63 |
+
("pending", "Pending"),
|
| 64 |
+
("confirmed", "Confirmed"),
|
| 65 |
+
("cancelled", "Cancelled"),
|
| 66 |
+
("failed", "Failed"),
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
experience = models.ForeignKey(
|
| 70 |
+
Experience,
|
| 71 |
+
on_delete=models.CASCADE,
|
| 72 |
+
related_name="bookings"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
slot = models.ForeignKey(
|
| 76 |
+
ExperienceSlot,
|
| 77 |
+
on_delete=models.PROTECT,
|
| 78 |
+
related_name="bookings",
|
| 79 |
+
null=True,
|
| 80 |
+
blank=True,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
full_name = models.CharField(max_length=100)
|
| 85 |
+
phone = models.CharField(max_length=15)
|
| 86 |
+
email = models.EmailField()
|
| 87 |
+
|
| 88 |
+
booking_date = models.DateField()
|
| 89 |
+
number_of_people = models.PositiveIntegerField()
|
| 90 |
+
|
| 91 |
+
status = models.CharField(
|
| 92 |
+
max_length=20,
|
| 93 |
+
choices=STATUS_CHOICES,
|
| 94 |
+
default="pending"
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
payment_amount = models.PositiveIntegerField()
|
| 98 |
+
|
| 99 |
+
# 🔗 LINK TO ORDERS APP
|
| 100 |
+
payment_order = models.OneToOneField(
|
| 101 |
+
"orders.PaymentOrder",
|
| 102 |
+
on_delete=models.SET_NULL,
|
| 103 |
+
null=True,
|
| 104 |
+
blank=True,
|
| 105 |
+
related_name="experience_booking"
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 109 |
+
|
| 110 |
+
def __str__(self):
|
| 111 |
+
return f"{self.full_name} - {self.experience.title}"
|
| 112 |
+
|
| 113 |
+
class StudioBooking(models.Model):
|
| 114 |
+
full_name = models.CharField(max_length=100)
|
| 115 |
+
phone = models.CharField(max_length=15)
|
| 116 |
+
email = models.EmailField()
|
| 117 |
+
visit_date = models.DateField()
|
| 118 |
+
time_slot = models.CharField(max_length=50)
|
| 119 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 120 |
+
|
| 121 |
+
def __str__(self):
|
| 122 |
+
return f"{self.full_name} - {self.visit_date} ({self.time_slot})"
|
| 123 |
+
|
| 124 |
+
class UpcomingEvent(models.Model):
|
| 125 |
+
title = models.CharField(max_length=200)
|
| 126 |
+
date = models.CharField(max_length=50) # simple string for now
|
| 127 |
+
location = models.CharField(max_length=200)
|
| 128 |
+
description = models.TextField()
|
| 129 |
+
badge = models.CharField(max_length=50, default="✨ Upcoming")
|
| 130 |
+
|
| 131 |
+
def __str__(self):
|
| 132 |
+
return self.title
|
| 133 |
+
|
| 134 |
+
class Workshop(models.Model):
|
| 135 |
+
WORKSHOP_TYPE_CHOICES = [
|
| 136 |
+
("group", "Group"),
|
| 137 |
+
("private", "Private"),
|
| 138 |
+
("experience", "Experience"),
|
| 139 |
+
]
|
| 140 |
+
|
| 141 |
+
LEVEL_CHOICES = [
|
| 142 |
+
("beginner", "Beginner"),
|
| 143 |
+
("intermediate", "Intermediate"),
|
| 144 |
+
("advanced", "Advanced"),
|
| 145 |
+
]
|
| 146 |
+
|
| 147 |
+
EXPERIENCE_TYPE_CHOICES = [
|
| 148 |
+
("couples_date", "Couple’s Date"),
|
| 149 |
+
("birthday_party", "Birthday Party"),
|
| 150 |
+
("corporate", "Corporate"),
|
| 151 |
+
("masterclass", "Masterclass"),
|
| 152 |
+
]
|
| 153 |
+
|
| 154 |
+
name = models.CharField(max_length=200)
|
| 155 |
+
type = models.CharField(max_length=20, choices=WORKSHOP_TYPE_CHOICES)
|
| 156 |
+
level = models.CharField(max_length=20, choices=LEVEL_CHOICES)
|
| 157 |
+
experience_type = models.CharField(
|
| 158 |
+
max_length=30,
|
| 159 |
+
choices=EXPERIENCE_TYPE_CHOICES,
|
| 160 |
+
blank=True,
|
| 161 |
+
null=True
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
description = models.TextField()
|
| 165 |
+
long_description = models.TextField()
|
| 166 |
+
|
| 167 |
+
images = models.JSONField(default=list)
|
| 168 |
+
|
| 169 |
+
duration = models.CharField(max_length=50)
|
| 170 |
+
|
| 171 |
+
min_participants = models.PositiveIntegerField()
|
| 172 |
+
max_participants = models.PositiveIntegerField()
|
| 173 |
+
|
| 174 |
+
price = models.PositiveIntegerField()
|
| 175 |
+
price_per_person = models.BooleanField(default=True)
|
| 176 |
+
|
| 177 |
+
includes = models.JSONField(default=list)
|
| 178 |
+
requirements = models.JSONField(blank=True, null=True)
|
| 179 |
+
provided_materials = models.JSONField(default=list)
|
| 180 |
+
|
| 181 |
+
location = models.CharField(max_length=200)
|
| 182 |
+
instructor = models.CharField(max_length=100)
|
| 183 |
+
take_home = models.CharField(max_length=200)
|
| 184 |
+
|
| 185 |
+
certificate = models.BooleanField(default=False)
|
| 186 |
+
lunch_included = models.BooleanField(default=False)
|
| 187 |
+
featured = models.BooleanField(default=False)
|
| 188 |
+
|
| 189 |
+
is_active = models.BooleanField(default=True)
|
| 190 |
+
|
| 191 |
+
def __str__(self):
|
| 192 |
+
return self.name
|
| 193 |
+
|
| 194 |
+
class WorkshopSlot(models.Model):
|
| 195 |
+
workshop = models.ForeignKey(
|
| 196 |
+
Workshop,
|
| 197 |
+
on_delete=models.CASCADE,
|
| 198 |
+
related_name="slots"
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
date = models.DateField()
|
| 202 |
+
start_time = models.TimeField()
|
| 203 |
+
end_time = models.TimeField()
|
| 204 |
+
|
| 205 |
+
available_spots = models.PositiveIntegerField()
|
| 206 |
+
is_available = models.BooleanField(default=True)
|
| 207 |
+
|
| 208 |
+
def __str__(self):
|
| 209 |
+
return f"{self.workshop.name} | {self.date} {self.start_time}"
|
| 210 |
+
|
| 211 |
+
class WorkshopRegistration(models.Model):
|
| 212 |
+
STATUS_CHOICES = (
|
| 213 |
+
("pending", "Pending"),
|
| 214 |
+
("confirmed", "Confirmed"),
|
| 215 |
+
("failed", "Failed"),
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
workshop = models.ForeignKey(
|
| 219 |
+
Workshop,
|
| 220 |
+
on_delete=models.CASCADE,
|
| 221 |
+
related_name="registrations"
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
slot = models.ForeignKey(
|
| 225 |
+
WorkshopSlot,
|
| 226 |
+
on_delete=models.PROTECT,
|
| 227 |
+
related_name="registrations"
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
name = models.CharField(max_length=100)
|
| 231 |
+
email = models.EmailField()
|
| 232 |
+
phone = models.CharField(max_length=15)
|
| 233 |
+
|
| 234 |
+
number_of_participants = models.PositiveIntegerField()
|
| 235 |
+
special_requests = models.TextField(blank=True, null=True)
|
| 236 |
+
gst_number = models.CharField(max_length=50, blank=True, null=True)
|
| 237 |
+
|
| 238 |
+
status = models.CharField(
|
| 239 |
+
max_length=20,
|
| 240 |
+
choices=STATUS_CHOICES,
|
| 241 |
+
default="pending"
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
# 🔗 LINK TO ORDERS APP
|
| 245 |
+
payment_order = models.OneToOneField(
|
| 246 |
+
"orders.PaymentOrder",
|
| 247 |
+
on_delete=models.SET_NULL,
|
| 248 |
+
null=True,
|
| 249 |
+
blank=True,
|
| 250 |
+
related_name="workshop_registration"
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 254 |
+
|
| 255 |
+
def __str__(self):
|
| 256 |
+
return f"{self.name} - {self.workshop.name}"
|
basho_backend/apps/experiences/serializers.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from rest_framework import serializers
|
| 2 |
+
from .models import (
|
| 3 |
+
Booking,
|
| 4 |
+
Experience,
|
| 5 |
+
StudioBooking,
|
| 6 |
+
UpcomingEvent,
|
| 7 |
+
Workshop,
|
| 8 |
+
WorkshopSlot,
|
| 9 |
+
WorkshopRegistration,
|
| 10 |
+
ExperienceSlot)
|
| 11 |
+
|
| 12 |
+
class BookingSerializer(serializers.ModelSerializer):
|
| 13 |
+
class Meta:
|
| 14 |
+
model = Booking
|
| 15 |
+
fields = [
|
| 16 |
+
"id",
|
| 17 |
+
"experience",
|
| 18 |
+
"slot",
|
| 19 |
+
"full_name",
|
| 20 |
+
"phone",
|
| 21 |
+
"email",
|
| 22 |
+
"booking_date",
|
| 23 |
+
"number_of_people",
|
| 24 |
+
"status",
|
| 25 |
+
]
|
| 26 |
+
read_only_fields = ["status"]
|
| 27 |
+
|
| 28 |
+
def validate(self, data):
|
| 29 |
+
slot = data.get("slot")
|
| 30 |
+
people = data.get("number_of_people")
|
| 31 |
+
|
| 32 |
+
if not slot:
|
| 33 |
+
raise serializers.ValidationError({
|
| 34 |
+
"slot": "Please select a valid time slot."
|
| 35 |
+
})
|
| 36 |
+
|
| 37 |
+
if not slot.is_active:
|
| 38 |
+
raise serializers.ValidationError({
|
| 39 |
+
"slot": "This slot is no longer available."
|
| 40 |
+
})
|
| 41 |
+
|
| 42 |
+
experience = slot.experience
|
| 43 |
+
|
| 44 |
+
# ✅ Booking-level rules (from Experience)
|
| 45 |
+
if experience.min_participants is not None:
|
| 46 |
+
if people < experience.min_participants:
|
| 47 |
+
raise serializers.ValidationError({
|
| 48 |
+
"number_of_people": f"Minimum {experience.min_participants} participants required."
|
| 49 |
+
})
|
| 50 |
+
|
| 51 |
+
if experience.max_participants is not None:
|
| 52 |
+
if people > experience.max_participants:
|
| 53 |
+
raise serializers.ValidationError({
|
| 54 |
+
"number_of_people": f"Maximum {experience.max_participants} participants allowed per booking."
|
| 55 |
+
})
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
# ✅ Slot capacity rule
|
| 59 |
+
available = slot.total_slots - slot.booked_slots
|
| 60 |
+
if people > available:
|
| 61 |
+
raise serializers.ValidationError({
|
| 62 |
+
"number_of_people": f"Only {available} slots left for this time."
|
| 63 |
+
})
|
| 64 |
+
|
| 65 |
+
return data
|
| 66 |
+
|
| 67 |
+
class ExperienceSlotSerializer(serializers.ModelSerializer):
|
| 68 |
+
startTime = serializers.TimeField(source="start_time")
|
| 69 |
+
endTime = serializers.TimeField(source="end_time")
|
| 70 |
+
availableSlots = serializers.SerializerMethodField()
|
| 71 |
+
|
| 72 |
+
class Meta:
|
| 73 |
+
model = ExperienceSlot
|
| 74 |
+
fields = [
|
| 75 |
+
"id",
|
| 76 |
+
"date",
|
| 77 |
+
"startTime",
|
| 78 |
+
"endTime",
|
| 79 |
+
"availableSlots",
|
| 80 |
+
]
|
| 81 |
+
|
| 82 |
+
def get_availableSlots(self, obj):
|
| 83 |
+
return max(0, obj.total_slots - obj.booked_slots)
|
| 84 |
+
|
| 85 |
+
class StudioBookingSerializer(serializers.ModelSerializer):
|
| 86 |
+
class Meta:
|
| 87 |
+
model = StudioBooking
|
| 88 |
+
fields = "__all__"
|
| 89 |
+
|
| 90 |
+
class UpcomingEventSerializer(serializers.ModelSerializer):
|
| 91 |
+
class Meta:
|
| 92 |
+
model = UpcomingEvent
|
| 93 |
+
fields = "__all__"
|
| 94 |
+
|
| 95 |
+
class WorkshopSlotSerializer(serializers.ModelSerializer):
|
| 96 |
+
startTime = serializers.TimeField(source="start_time")
|
| 97 |
+
endTime = serializers.TimeField(source="end_time")
|
| 98 |
+
availableSpots = serializers.IntegerField(source="available_spots")
|
| 99 |
+
isAvailable = serializers.BooleanField(source="is_available")
|
| 100 |
+
|
| 101 |
+
class Meta:
|
| 102 |
+
model = WorkshopSlot
|
| 103 |
+
fields = [
|
| 104 |
+
"id",
|
| 105 |
+
"date",
|
| 106 |
+
"startTime",
|
| 107 |
+
"endTime",
|
| 108 |
+
"availableSpots",
|
| 109 |
+
"isAvailable",
|
| 110 |
+
]
|
| 111 |
+
|
| 112 |
+
class WorkshopSerializer(serializers.ModelSerializer):
|
| 113 |
+
participants = serializers.SerializerMethodField()
|
| 114 |
+
schedule = WorkshopSlotSerializer(source="slots", many=True)
|
| 115 |
+
|
| 116 |
+
experienceType = serializers.CharField(source="experience_type", allow_null=True)
|
| 117 |
+
longDescription = serializers.CharField(source="long_description")
|
| 118 |
+
pricePerPerson = serializers.BooleanField(source="price_per_person")
|
| 119 |
+
takeHome = serializers.CharField(source="take_home")
|
| 120 |
+
providedMaterials = serializers.JSONField(source="provided_materials")
|
| 121 |
+
lunchIncluded = serializers.BooleanField(source="lunch_included")
|
| 122 |
+
|
| 123 |
+
class Meta:
|
| 124 |
+
model = Workshop
|
| 125 |
+
fields = [
|
| 126 |
+
"id",
|
| 127 |
+
"name",
|
| 128 |
+
"type",
|
| 129 |
+
"level",
|
| 130 |
+
"experienceType",
|
| 131 |
+
"description",
|
| 132 |
+
"longDescription",
|
| 133 |
+
"images",
|
| 134 |
+
"duration",
|
| 135 |
+
"participants",
|
| 136 |
+
"price",
|
| 137 |
+
"pricePerPerson",
|
| 138 |
+
"includes",
|
| 139 |
+
"requirements",
|
| 140 |
+
"location",
|
| 141 |
+
"instructor",
|
| 142 |
+
"takeHome",
|
| 143 |
+
"providedMaterials",
|
| 144 |
+
"certificate",
|
| 145 |
+
"lunchIncluded",
|
| 146 |
+
"featured",
|
| 147 |
+
"schedule",
|
| 148 |
+
]
|
| 149 |
+
|
| 150 |
+
def get_participants(self, obj):
|
| 151 |
+
return {
|
| 152 |
+
"min": obj.min_participants,
|
| 153 |
+
"max": obj.max_participants,
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
class WorkshopRegistrationSerializer(serializers.ModelSerializer):
|
| 157 |
+
class Meta:
|
| 158 |
+
model = WorkshopRegistration
|
| 159 |
+
fields = "__all__"
|
| 160 |
+
|
| 161 |
+
class ExperienceSerializer(serializers.ModelSerializer):
|
| 162 |
+
slots = ExperienceSlotSerializer(many=True, read_only=True)
|
| 163 |
+
participants = serializers.SerializerMethodField()
|
| 164 |
+
image = serializers.JSONField()
|
| 165 |
+
|
| 166 |
+
class Meta:
|
| 167 |
+
model = Experience
|
| 168 |
+
fields = [
|
| 169 |
+
"id",
|
| 170 |
+
"title",
|
| 171 |
+
"tagline",
|
| 172 |
+
"description",
|
| 173 |
+
"duration",
|
| 174 |
+
"people",
|
| 175 |
+
"price",
|
| 176 |
+
"image",
|
| 177 |
+
"is_active",
|
| 178 |
+
"participants",
|
| 179 |
+
"slots",
|
| 180 |
+
]
|
| 181 |
+
|
| 182 |
+
def get_participants(self, obj):
|
| 183 |
+
return {
|
| 184 |
+
"min": obj.min_participants,
|
| 185 |
+
"max": obj.max_participants,
|
| 186 |
+
}
|
basho_backend/apps/experiences/tests.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.test import TestCase
|
| 2 |
+
|
| 3 |
+
# Create your tests here.
|
basho_backend/apps/experiences/urls.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.urls import path
|
| 2 |
+
from .views import (
|
| 3 |
+
CreateBookingView,
|
| 4 |
+
CreateStudioBookingView,
|
| 5 |
+
ListUpcomingEventsView,
|
| 6 |
+
ListWorkshopsView,
|
| 7 |
+
ReleaseExperienceSlotView,
|
| 8 |
+
WorkshopDetailView,
|
| 9 |
+
ListWorkshopSlotsView,
|
| 10 |
+
CreateWorkshopRegistrationView,
|
| 11 |
+
ListExperienceSlotsView,
|
| 12 |
+
ListExperiencesView,
|
| 13 |
+
ListExperienceAvailableDatesView,
|
| 14 |
+
ListExperienceSlotsByDateView,
|
| 15 |
+
VerifyExperiencePaymentView,
|
| 16 |
+
my_workshops,
|
| 17 |
+
my_experiences
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
urlpatterns = [
|
| 23 |
+
# Experiences
|
| 24 |
+
path("book/", CreateBookingView.as_view(), name="create-booking"),
|
| 25 |
+
path("verify-payment/", VerifyExperiencePaymentView.as_view()),
|
| 26 |
+
#path("book/confirm/", ConfirmBookingView.as_view(), name="confirm-booking"),
|
| 27 |
+
path(
|
| 28 |
+
"<int:experience_id>/slots/",
|
| 29 |
+
ListExperienceSlotsView.as_view(),
|
| 30 |
+
name="experience-slots",
|
| 31 |
+
),
|
| 32 |
+
path("", ListExperiencesView.as_view(), name="list-experiences"),
|
| 33 |
+
path("release-slot/", ReleaseExperienceSlotView.as_view()),
|
| 34 |
+
path("<int:experience_id>/available-dates/", ListExperienceAvailableDatesView.as_view(), name="experience-available-dates"),
|
| 35 |
+
path("<int:experience_id>/slots-by-date/", ListExperienceSlotsByDateView.as_view(), name="experience-slots-by-date"),
|
| 36 |
+
path(
|
| 37 |
+
"<int:experience_id>/available-dates/",
|
| 38 |
+
ListExperienceAvailableDatesView.as_view(),),
|
| 39 |
+
|
| 40 |
+
path(
|
| 41 |
+
"<int:experience_id>/slots-by-date/",
|
| 42 |
+
ListExperienceSlotsByDateView.as_view(),
|
| 43 |
+
),
|
| 44 |
+
# Studio
|
| 45 |
+
path("studio-book/", CreateStudioBookingView.as_view(), name="create-studio-booking"),
|
| 46 |
+
|
| 47 |
+
# Events
|
| 48 |
+
path("events/", ListUpcomingEventsView.as_view(), name="list-upcoming-events"),
|
| 49 |
+
|
| 50 |
+
# Workshops (UNCHANGED)
|
| 51 |
+
path("workshops/", ListWorkshopsView.as_view(), name="list-workshops"),
|
| 52 |
+
path("workshops/<int:workshop_id>/", WorkshopDetailView.as_view(), name="workshop-detail"),
|
| 53 |
+
path(
|
| 54 |
+
"workshops/<int:workshop_id>/slots/",
|
| 55 |
+
ListWorkshopSlotsView.as_view(),
|
| 56 |
+
name="workshop-slots",
|
| 57 |
+
),
|
| 58 |
+
path(
|
| 59 |
+
"workshops/register/",
|
| 60 |
+
CreateWorkshopRegistrationView.as_view(),
|
| 61 |
+
name="workshop-register",
|
| 62 |
+
),
|
| 63 |
+
path("my-workshops/", my_workshops),
|
| 64 |
+
path("my-experiences/", my_experiences),
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
]
|
basho_backend/apps/experiences/views.py
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.shortcuts import get_object_or_404
|
| 2 |
+
from django.db import transaction
|
| 3 |
+
from rest_framework.exceptions import ValidationError
|
| 4 |
+
from rest_framework.views import APIView
|
| 5 |
+
from rest_framework.response import Response
|
| 6 |
+
from rest_framework import status
|
| 7 |
+
from django.http import JsonResponse
|
| 8 |
+
from django.core.mail import send_mail
|
| 9 |
+
from django.conf import settings
|
| 10 |
+
from rest_framework.permissions import IsAuthenticated
|
| 11 |
+
from rest_framework.decorators import api_view, permission_classes
|
| 12 |
+
import razorpay
|
| 13 |
+
from django.utils.decorators import method_decorator
|
| 14 |
+
from django.views.decorators.csrf import csrf_exempt
|
| 15 |
+
from django.conf import settings
|
| 16 |
+
from django.db.models import F
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
from apps.orders.models import PaymentOrder
|
| 21 |
+
from .models import (
|
| 22 |
+
Booking,
|
| 23 |
+
StudioBooking,
|
| 24 |
+
UpcomingEvent,
|
| 25 |
+
Workshop,
|
| 26 |
+
WorkshopSlot,
|
| 27 |
+
WorkshopRegistration,
|
| 28 |
+
Experience,
|
| 29 |
+
ExperienceSlot,
|
| 30 |
+
)
|
| 31 |
+
from .serializers import (
|
| 32 |
+
BookingSerializer,
|
| 33 |
+
StudioBookingSerializer,
|
| 34 |
+
UpcomingEventSerializer,
|
| 35 |
+
WorkshopSerializer,
|
| 36 |
+
WorkshopSlotSerializer,
|
| 37 |
+
WorkshopRegistrationSerializer,
|
| 38 |
+
ExperienceSlotSerializer,
|
| 39 |
+
ExperienceSerializer, # ✅ ADD THIS
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
razorpay_client = razorpay.Client(
|
| 43 |
+
auth=(settings.RAZORPAY_KEY_ID, settings.RAZORPAY_KEY_SECRET)
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
# =========================
|
| 47 |
+
# EXPERIENCE BOOKING (PAYMENT FIRST)
|
| 48 |
+
# =========================
|
| 49 |
+
|
| 50 |
+
class CreateBookingView(APIView):
|
| 51 |
+
def post(self, request):
|
| 52 |
+
serializer = BookingSerializer(data=request.data)
|
| 53 |
+
serializer.is_valid(raise_exception=True)
|
| 54 |
+
|
| 55 |
+
slot = serializer.validated_data["slot"]
|
| 56 |
+
people = serializer.validated_data["number_of_people"]
|
| 57 |
+
experience = serializer.validated_data["experience"]
|
| 58 |
+
|
| 59 |
+
# 🔒 ATOMIC BLOCK (prevents race conditions)
|
| 60 |
+
with transaction.atomic():
|
| 61 |
+
slot = ExperienceSlot.objects.select_for_update().get(id=slot.id)
|
| 62 |
+
|
| 63 |
+
available = slot.total_slots - slot.booked_slots
|
| 64 |
+
|
| 65 |
+
if people > available:
|
| 66 |
+
raise ValidationError({
|
| 67 |
+
"slot": f"Only {available} slots left for this time slot."
|
| 68 |
+
})
|
| 69 |
+
|
| 70 |
+
booking = serializer.save(
|
| 71 |
+
status="pending",
|
| 72 |
+
payment_amount=experience.price
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# ✅ Reserve seats
|
| 76 |
+
slot.booked_slots += people
|
| 77 |
+
slot.save()
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# ✅ CREATE RAZORPAY ORDER
|
| 81 |
+
razorpay_order = razorpay_client.order.create({
|
| 82 |
+
"amount": booking.payment_amount * 100,
|
| 83 |
+
"currency": "INR",
|
| 84 |
+
"payment_capture": 1
|
| 85 |
+
})
|
| 86 |
+
|
| 87 |
+
payment_order = PaymentOrder.objects.create(
|
| 88 |
+
user=request.user if request.user.is_authenticated else None,
|
| 89 |
+
order_type="EXPERIENCE",
|
| 90 |
+
linked_object_id=booking.id,
|
| 91 |
+
linked_app="experiences",
|
| 92 |
+
amount=booking.payment_amount,
|
| 93 |
+
razorpay_order_id=razorpay_order["id"],
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
booking.payment_order = payment_order
|
| 97 |
+
booking.save()
|
| 98 |
+
|
| 99 |
+
return Response({
|
| 100 |
+
"booking_id": booking.id,
|
| 101 |
+
"razorpay_order_id": payment_order.razorpay_order_id,
|
| 102 |
+
"amount": payment_order.amount,
|
| 103 |
+
}, status=status.HTTP_201_CREATED)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
razorpay_client = razorpay.Client(
|
| 108 |
+
auth=(settings.RAZORPAY_KEY_ID, settings.RAZORPAY_KEY_SECRET)
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
class ReleaseExperienceSlotView(APIView):
|
| 112 |
+
def post(self, request):
|
| 113 |
+
booking_id = request.data.get("booking_id")
|
| 114 |
+
|
| 115 |
+
if not booking_id:
|
| 116 |
+
return Response(
|
| 117 |
+
{"error": "booking_id required"},
|
| 118 |
+
status=status.HTTP_400_BAD_REQUEST
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
try:
|
| 122 |
+
with transaction.atomic():
|
| 123 |
+
booking = Booking.objects.select_for_update().get(id=booking_id)
|
| 124 |
+
|
| 125 |
+
# Only release if not confirmed
|
| 126 |
+
if booking.status != "pending":
|
| 127 |
+
return Response(
|
| 128 |
+
{"message": "Booking already processed"},
|
| 129 |
+
status=status.HTTP_200_OK
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
slot = booking.slot
|
| 133 |
+
slot.booked_slots -= booking.number_of_people
|
| 134 |
+
slot.save()
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
booking.status = "failed"
|
| 138 |
+
booking.save()
|
| 139 |
+
|
| 140 |
+
return Response(
|
| 141 |
+
{"message": "Slot released successfully"},
|
| 142 |
+
status=status.HTTP_200_OK
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
except Booking.DoesNotExist:
|
| 146 |
+
return Response(
|
| 147 |
+
{"error": "Booking not found"},
|
| 148 |
+
status=status.HTTP_404_NOT_FOUND
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
class VerifyExperiencePaymentView(APIView):
|
| 152 |
+
def post(self, request):
|
| 153 |
+
data = request.data
|
| 154 |
+
|
| 155 |
+
try:
|
| 156 |
+
# 1️⃣ Verify signature
|
| 157 |
+
razorpay_client.utility.verify_payment_signature({
|
| 158 |
+
"razorpay_order_id": data["razorpay_order_id"],
|
| 159 |
+
"razorpay_payment_id": data["razorpay_payment_id"],
|
| 160 |
+
"razorpay_signature": data["razorpay_signature"],
|
| 161 |
+
})
|
| 162 |
+
|
| 163 |
+
# 2️⃣ Fetch payment order
|
| 164 |
+
payment_order = PaymentOrder.objects.get(
|
| 165 |
+
razorpay_order_id=data["razorpay_order_id"]
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
# 3️⃣ Mark payment as paid
|
| 169 |
+
payment_order.status = "PAID"
|
| 170 |
+
payment_order.razorpay_payment_id = data["razorpay_payment_id"]
|
| 171 |
+
payment_order.save()
|
| 172 |
+
|
| 173 |
+
# 4️⃣ Confirm booking
|
| 174 |
+
booking = Booking.objects.get(id=payment_order.linked_object_id)
|
| 175 |
+
booking.status = "confirmed"
|
| 176 |
+
booking.save()
|
| 177 |
+
|
| 178 |
+
# 🔍 TEMP DEBUG — add just above send_mail
|
| 179 |
+
print("Experience fields:")
|
| 180 |
+
print(booking.experience._meta.get_fields())
|
| 181 |
+
|
| 182 |
+
# 5️⃣ Send email
|
| 183 |
+
if booking.email:
|
| 184 |
+
send_mail(
|
| 185 |
+
subject="Your Experience Booking is Confirmed 🎉",
|
| 186 |
+
message=f"""
|
| 187 |
+
Hi {booking.full_name},
|
| 188 |
+
|
| 189 |
+
Your experience booking has been successfully confirmed!
|
| 190 |
+
|
| 191 |
+
📅 Date: {booking.booking_date}
|
| 192 |
+
🎨 Experience: {booking.experience.title}
|
| 193 |
+
💰 Amount Paid: ₹{booking.payment_amount}
|
| 194 |
+
|
| 195 |
+
We look forward to welcoming you ✨
|
| 196 |
+
|
| 197 |
+
– Team Basho
|
| 198 |
+
""",
|
| 199 |
+
from_email=settings.DEFAULT_FROM_EMAIL,
|
| 200 |
+
recipient_list=[booking.email],
|
| 201 |
+
fail_silently=False,
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
return Response({
|
| 205 |
+
"message": "Successfully placed order / booked experience"
|
| 206 |
+
}, status=status.HTTP_200_OK)
|
| 207 |
+
|
| 208 |
+
except razorpay.errors.SignatureVerificationError:
|
| 209 |
+
return Response(
|
| 210 |
+
{"error": "Payment verification failed"},
|
| 211 |
+
status=status.HTTP_400_BAD_REQUEST
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
# =========================
|
| 215 |
+
# STUDIO BOOKING (NO PAYMENT)
|
| 216 |
+
# =========================
|
| 217 |
+
@method_decorator(csrf_exempt, name="dispatch")
|
| 218 |
+
class CreateStudioBookingView(APIView):
|
| 219 |
+
def post(self, request):
|
| 220 |
+
serializer = StudioBookingSerializer(data=request.data)
|
| 221 |
+
serializer.is_valid(raise_exception=True)
|
| 222 |
+
|
| 223 |
+
booking = serializer.save()
|
| 224 |
+
|
| 225 |
+
try:
|
| 226 |
+
send_mail(
|
| 227 |
+
subject="Your Studio Visit is Confirmed ✨",
|
| 228 |
+
message=(
|
| 229 |
+
f"Hi {booking.full_name},\n\n"
|
| 230 |
+
f"Your studio visit has been confirmed.\n\n"
|
| 231 |
+
f"Date: {booking.visit_date}\n"
|
| 232 |
+
f"Time Slot: {booking.time_slot}\n\n"
|
| 233 |
+
f"– Basho Studio"
|
| 234 |
+
),
|
| 235 |
+
from_email=settings.DEFAULT_FROM_EMAIL,
|
| 236 |
+
recipient_list=[booking.email],
|
| 237 |
+
fail_silently=True, # 🔥 THIS IS THE KEY
|
| 238 |
+
)
|
| 239 |
+
except Exception as e:
|
| 240 |
+
print("⚠️ Studio email failed:", str(e))
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
return Response(
|
| 244 |
+
{"message": "Studio booking confirmed"},
|
| 245 |
+
status=status.HTTP_201_CREATED
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
# =========================
|
| 250 |
+
# LISTING VIEWS
|
| 251 |
+
# =========================
|
| 252 |
+
|
| 253 |
+
class ListUpcomingEventsView(APIView):
|
| 254 |
+
def get(self, request):
|
| 255 |
+
events = UpcomingEvent.objects.all()
|
| 256 |
+
serializer = UpcomingEventSerializer(events, many=True)
|
| 257 |
+
return Response(serializer.data)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
class ListWorkshopsView(APIView):
|
| 261 |
+
def get(self, request):
|
| 262 |
+
workshops = Workshop.objects.filter(is_active=True)
|
| 263 |
+
serializer = WorkshopSerializer(workshops, many=True)
|
| 264 |
+
return Response(serializer.data, status=status.HTTP_200_OK)
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
class WorkshopDetailView(APIView):
|
| 268 |
+
def get(self, request, workshop_id):
|
| 269 |
+
workshop = get_object_or_404(Workshop, id=workshop_id, is_active=True)
|
| 270 |
+
serializer = WorkshopSerializer(workshop)
|
| 271 |
+
return Response(serializer.data, status=status.HTTP_200_OK)
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
class ListWorkshopSlotsView(APIView):
|
| 275 |
+
def get(self, request, workshop_id):
|
| 276 |
+
workshop = get_object_or_404(Workshop, id=workshop_id, is_active=True)
|
| 277 |
+
slots = WorkshopSlot.objects.filter(
|
| 278 |
+
workshop=workshop,
|
| 279 |
+
is_available=True
|
| 280 |
+
).order_by("date", "start_time")
|
| 281 |
+
|
| 282 |
+
serializer = WorkshopSlotSerializer(slots, many=True)
|
| 283 |
+
return Response(serializer.data)
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
class ListExperienceSlotsView(APIView):
|
| 287 |
+
def get(self, request, experience_id):
|
| 288 |
+
experience = get_object_or_404(Experience, id=experience_id, is_active=True)
|
| 289 |
+
slots = ExperienceSlot.objects.filter(
|
| 290 |
+
experience=experience,
|
| 291 |
+
is_active=True
|
| 292 |
+
).order_by("date", "start_time")
|
| 293 |
+
|
| 294 |
+
serializer = ExperienceSlotSerializer(slots, many=True)
|
| 295 |
+
return Response(serializer.data, status=status.HTTP_200_OK)
|
| 296 |
+
|
| 297 |
+
class ListExperiencesView(APIView):
|
| 298 |
+
def get(self, request):
|
| 299 |
+
experiences = Experience.objects.filter(is_active=True)
|
| 300 |
+
serializer = ExperienceSerializer(experiences, many=True)
|
| 301 |
+
return Response(serializer.data, status=status.HTTP_200_OK)
|
| 302 |
+
|
| 303 |
+
class ListExperienceAvailableDatesView(APIView):
|
| 304 |
+
def get(self, request, experience_id):
|
| 305 |
+
experience = get_object_or_404(
|
| 306 |
+
Experience,
|
| 307 |
+
id=experience_id,
|
| 308 |
+
is_active=True
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
# Slots that still have availability
|
| 312 |
+
slots = (
|
| 313 |
+
ExperienceSlot.objects
|
| 314 |
+
.filter(
|
| 315 |
+
experience=experience,
|
| 316 |
+
is_active=True,
|
| 317 |
+
total_slots__gt=F("booked_slots")
|
| 318 |
+
)
|
| 319 |
+
.values_list("date", flat=True)
|
| 320 |
+
.distinct()
|
| 321 |
+
.order_by("date")
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
# Convert dates to string (frontend-friendly)
|
| 325 |
+
dates = [d.isoformat() for d in slots]
|
| 326 |
+
|
| 327 |
+
return Response(dates, status=status.HTTP_200_OK)
|
| 328 |
+
|
| 329 |
+
class ListExperienceSlotsByDateView(APIView):
|
| 330 |
+
def get(self, request, experience_id):
|
| 331 |
+
date = request.query_params.get("date")
|
| 332 |
+
|
| 333 |
+
if not date:
|
| 334 |
+
return Response(
|
| 335 |
+
{"error": "date query param is required"},
|
| 336 |
+
status=status.HTTP_400_BAD_REQUEST
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
experience = get_object_or_404(
|
| 340 |
+
Experience,
|
| 341 |
+
id=experience_id,
|
| 342 |
+
is_active=True
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
slots = (
|
| 346 |
+
ExperienceSlot.objects
|
| 347 |
+
.filter(
|
| 348 |
+
experience=experience,
|
| 349 |
+
date=date,
|
| 350 |
+
is_active=True
|
| 351 |
+
)
|
| 352 |
+
.order_by("start_time")
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
serializer = ExperienceSlotSerializer(slots, many=True)
|
| 356 |
+
return Response(serializer.data, status=status.HTTP_200_OK)
|
| 357 |
+
|
| 358 |
+
# =========================
|
| 359 |
+
# WORKSHOP REGISTRATION (PAYMENT FIRST)
|
| 360 |
+
# =========================
|
| 361 |
+
|
| 362 |
+
class CreateWorkshopRegistrationView(APIView):
|
| 363 |
+
def post(self, request):
|
| 364 |
+
serializer = WorkshopRegistrationSerializer(data=request.data)
|
| 365 |
+
serializer.is_valid(raise_exception=True)
|
| 366 |
+
|
| 367 |
+
registration = serializer.save(status="pending")
|
| 368 |
+
|
| 369 |
+
amount = (
|
| 370 |
+
registration.workshop.price * registration.number_of_participants
|
| 371 |
+
if registration.workshop.price_per_person
|
| 372 |
+
else registration.workshop.price
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
+
razorpay_order = razorpay_client.order.create({
|
| 376 |
+
"amount": amount * 100, # paise
|
| 377 |
+
"currency": "INR",
|
| 378 |
+
"payment_capture": 1
|
| 379 |
+
})
|
| 380 |
+
|
| 381 |
+
payment_order = PaymentOrder.objects.create(
|
| 382 |
+
user=request.user if request.user.is_authenticated else None,
|
| 383 |
+
order_type="WORKSHOP",
|
| 384 |
+
linked_object_id=registration.id,
|
| 385 |
+
linked_app="experiences",
|
| 386 |
+
amount=amount,
|
| 387 |
+
razorpay_order_id=razorpay_order["id"],
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
registration.payment_order = payment_order
|
| 391 |
+
registration.save()
|
| 392 |
+
|
| 393 |
+
return Response(
|
| 394 |
+
{
|
| 395 |
+
"registration_id": registration.id,
|
| 396 |
+
"razorpay_order_id": payment_order.razorpay_order_id,
|
| 397 |
+
"amount": amount,
|
| 398 |
+
},
|
| 399 |
+
status=status.HTTP_201_CREATED
|
| 400 |
+
)
|
| 401 |
+
@api_view(["GET"])
|
| 402 |
+
@permission_classes([IsAuthenticated])
|
| 403 |
+
def my_workshops(request):
|
| 404 |
+
orders = PaymentOrder.objects.filter(
|
| 405 |
+
user=request.user,
|
| 406 |
+
order_type="WORKSHOP",
|
| 407 |
+
status="PAID"
|
| 408 |
+
).order_by("-created_at")
|
| 409 |
+
|
| 410 |
+
data = [{
|
| 411 |
+
"id": o.id,
|
| 412 |
+
"amount": o.amount,
|
| 413 |
+
"date": o.created_at,
|
| 414 |
+
"linked_object_id": o.linked_object_id
|
| 415 |
+
} for o in orders]
|
| 416 |
+
|
| 417 |
+
return JsonResponse({"workshops": data})
|
| 418 |
+
@api_view(["GET"])
|
| 419 |
+
@permission_classes([IsAuthenticated])
|
| 420 |
+
def my_experiences(request):
|
| 421 |
+
orders = PaymentOrder.objects.filter(
|
| 422 |
+
user=request.user,
|
| 423 |
+
order_type="EXPERIENCE",
|
| 424 |
+
status="PAID"
|
| 425 |
+
).order_by("-created_at")
|
| 426 |
+
|
| 427 |
+
data = [{
|
| 428 |
+
"id": o.id,
|
| 429 |
+
"amount": o.amount,
|
| 430 |
+
"date": o.created_at,
|
| 431 |
+
"linked_object_id": o.linked_object_id
|
| 432 |
+
} for o in orders]
|
| 433 |
+
|
| 434 |
+
return JsonResponse({"experiences": data})
|
basho_backend/apps/main/__init__.py
ADDED
|
File without changes
|
basho_backend/apps/main/admin.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.contrib import admin
|
| 2 |
+
|
| 3 |
+
# Register your models here.
|
basho_backend/apps/main/apps.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.apps import AppConfig
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class MainConfig(AppConfig):
|
| 5 |
+
default_auto_field = 'django.db.models.BigAutoField'
|
| 6 |
+
name = 'apps.main'
|
basho_backend/apps/main/migrations/__init__.py
ADDED
|
File without changes
|