inital commit

This commit is contained in:
2026-04-10 20:51:43 -05:00
parent cd1f2eae29
commit 562a8525d0
85 changed files with 4820 additions and 2 deletions

1
accounts/__init__.py Normal file
View File

@@ -0,0 +1 @@

50
accounts/admin.py Normal file
View File

@@ -0,0 +1,50 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
from .models import CustomerProfile, User, VendorProfile
@admin.register(User)
class UserAdmin(DjangoUserAdmin):
ordering = ("email",)
list_display = ("email", "first_name", "last_name", "is_vendor", "is_customer", "is_staff")
search_fields = ("email", "first_name", "last_name", "phone_number")
list_filter = ("is_vendor", "is_customer", "is_staff", "is_superuser", "is_active")
fieldsets = (
(None, {"fields": ("email", "password")}),
("Personal info", {"fields": ("first_name", "last_name", "phone_number")}),
(
"Roles",
{"fields": ("is_vendor", "is_customer")},
),
(
"Permissions",
{"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")},
),
("Important dates", {"fields": ("last_login", "date_joined")}),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": ("email", "password1", "password2", "is_vendor", "is_customer"),
},
),
)
@admin.register(VendorProfile)
class VendorProfileAdmin(admin.ModelAdmin):
list_display = ("business_name", "user", "slug", "city", "country", "updated_at")
search_fields = ("business_name", "user__email", "contact_email")
list_filter = ("country", "state")
readonly_fields = ("created_at", "updated_at")
@admin.register(CustomerProfile)
class CustomerProfileAdmin(admin.ModelAdmin):
list_display = ("user", "preferred_contact_method", "updated_at")
search_fields = ("user__email", "emergency_contact_name")
readonly_fields = ("created_at", "updated_at")

6
accounts/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "accounts"

View File

@@ -0,0 +1,79 @@
# Generated by Django 6.0.4 on 2026-04-08 16:53
import django.contrib.auth.models
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('email', models.EmailField(max_length=254, unique=True)),
('phone_number', models.CharField(blank=True, max_length=32)),
('is_vendor', models.BooleanField(default=False)),
('is_customer', models.BooleanField(default=True)),
('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')),
('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')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='CustomerProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('preferred_contact_method', models.CharField(blank=True, max_length=32)),
('emergency_contact_name', models.CharField(blank=True, max_length=255)),
('emergency_contact_phone', models.CharField(blank=True, max_length=32)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='customer_profile', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='VendorProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('business_name', models.CharField(max_length=255)),
('slug', models.SlugField(blank=True, max_length=255, unique=True)),
('description', models.TextField(blank=True)),
('contact_phone', models.CharField(blank=True, max_length=32)),
('contact_email', models.EmailField(blank=True, max_length=254)),
('address_line1', models.CharField(blank=True, max_length=255)),
('address_line2', models.CharField(blank=True, max_length=255)),
('city', models.CharField(blank=True, max_length=100)),
('state', models.CharField(blank=True, max_length=100)),
('postal_code', models.CharField(blank=True, max_length=32)),
('country', models.CharField(blank=True, max_length=100)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='vendor_profile', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 6.0.4 on 2026-04-08 17:02
import accounts.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AlterModelManagers(
name='user',
managers=[
('objects', accounts.models.UserManager()),
],
),
]

View File

@@ -0,0 +1 @@

81
accounts/models.py Normal file
View File

@@ -0,0 +1,81 @@
from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.text import slugify
class UserManager(BaseUserManager):
use_in_migrations = True
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError("The Email field must be set")
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("is_active", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
return self.create_user(email, password, **extra_fields)
class User(AbstractUser):
username = None
email = models.EmailField(unique=True)
phone_number = models.CharField(max_length=32, blank=True)
is_vendor = models.BooleanField(default=False)
is_customer = models.BooleanField(default=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS: list[str] = []
objects = UserManager()
def __str__(self) -> str:
return self.email
class VendorProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="vendor_profile")
business_name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True, blank=True)
description = models.TextField(blank=True)
contact_phone = models.CharField(max_length=32, blank=True)
contact_email = models.EmailField(blank=True)
address_line1 = models.CharField(max_length=255, blank=True)
address_line2 = models.CharField(max_length=255, blank=True)
city = models.CharField(max_length=100, blank=True)
state = models.CharField(max_length=100, blank=True)
postal_code = models.CharField(max_length=32, blank=True)
country = models.CharField(max_length=100, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.business_name)
super().save(*args, **kwargs)
def __str__(self) -> str:
return self.business_name
class CustomerProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="customer_profile")
preferred_contact_method = models.CharField(max_length=32, blank=True)
emergency_contact_name = models.CharField(max_length=255, blank=True)
emergency_contact_phone = models.CharField(max_length=32, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self) -> str:
return f"Customer profile: {self.user.email}"

91
accounts/serializers.py Normal file
View File

@@ -0,0 +1,91 @@
from django.contrib.auth import get_user_model
from rest_framework import serializers
from .models import VendorProfile
User = get_user_model()
class VendorProfileSerializer(serializers.ModelSerializer):
class Meta:
model = VendorProfile
fields = (
"business_name",
"slug",
"description",
"contact_phone",
"contact_email",
"address_line1",
"address_line2",
"city",
"state",
"postal_code",
"country",
"created_at",
"updated_at",
)
read_only_fields = ("slug", "created_at", "updated_at")
class VendorRegistrationSerializer(serializers.Serializer):
email = serializers.EmailField()
password = serializers.CharField(write_only=True, min_length=8)
first_name = serializers.CharField(required=False, allow_blank=True, max_length=150)
last_name = serializers.CharField(required=False, allow_blank=True, max_length=150)
phone_number = serializers.CharField(required=False, allow_blank=True, max_length=32)
business_name = serializers.CharField(max_length=255)
description = serializers.CharField(required=False, allow_blank=True)
contact_phone = serializers.CharField(required=False, allow_blank=True, max_length=32)
contact_email = serializers.EmailField(required=False, allow_blank=True)
address_line1 = serializers.CharField(required=False, allow_blank=True, max_length=255)
address_line2 = serializers.CharField(required=False, allow_blank=True, max_length=255)
city = serializers.CharField(required=False, allow_blank=True, max_length=100)
state = serializers.CharField(required=False, allow_blank=True, max_length=100)
postal_code = serializers.CharField(required=False, allow_blank=True, max_length=32)
country = serializers.CharField(required=False, allow_blank=True, max_length=100)
def validate_email(self, value):
if User.objects.filter(email__iexact=value).exists():
raise serializers.ValidationError("A user with this email already exists.")
return value
def create(self, validated_data):
profile_data = {
"business_name": validated_data.pop("business_name"),
"description": validated_data.pop("description", ""),
"contact_phone": validated_data.pop("contact_phone", ""),
"contact_email": validated_data.pop("contact_email", ""),
"address_line1": validated_data.pop("address_line1", ""),
"address_line2": validated_data.pop("address_line2", ""),
"city": validated_data.pop("city", ""),
"state": validated_data.pop("state", ""),
"postal_code": validated_data.pop("postal_code", ""),
"country": validated_data.pop("country", ""),
}
password = validated_data.pop("password")
user = User.objects.create_user(
password=password,
is_vendor=True,
is_customer=False,
**validated_data,
)
VendorProfile.objects.create(user=user, **profile_data)
return user
class UserMeSerializer(serializers.ModelSerializer):
vendor_profile = VendorProfileSerializer(read_only=True)
class Meta:
model = User
fields = (
"id",
"email",
"first_name",
"last_name",
"phone_number",
"is_vendor",
"is_customer",
"vendor_profile",
)

65
accounts/tests.py Normal file
View File

@@ -0,0 +1,65 @@
from django.contrib.auth import get_user_model
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from .models import VendorProfile
User = get_user_model()
class AccountModelTests(APITestCase):
def test_custom_user_creation_and_roles(self):
user = User.objects.create_user(
email="customer@example.com",
password="Pass123456!",
is_customer=True,
is_vendor=False,
)
self.assertEqual(user.email, "customer@example.com")
self.assertTrue(user.check_password("Pass123456!"))
self.assertTrue(user.is_customer)
self.assertFalse(user.is_vendor)
class AccountApiTests(APITestCase):
def test_vendor_registration_and_jwt_flow(self):
register_payload = {
"email": "vendor@example.com",
"password": "Pass123456!",
"business_name": "Ocean Rentals",
}
register_res = self.client.post(reverse("register_vendor"), register_payload, format="json")
self.assertEqual(register_res.status_code, status.HTTP_201_CREATED)
self.assertTrue(User.objects.filter(email="vendor@example.com", is_vendor=True).exists())
self.assertTrue(VendorProfile.objects.filter(user__email="vendor@example.com").exists())
token_res = self.client.post(
reverse("token_obtain_pair"),
{"email": "vendor@example.com", "password": "Pass123456!"},
format="json",
)
self.assertEqual(token_res.status_code, status.HTTP_200_OK)
self.assertIn("access", token_res.data)
self.assertIn("refresh", token_res.data)
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token_res.data['access']}")
me_res = self.client.get(reverse("me"))
self.assertEqual(me_res.status_code, status.HTTP_200_OK)
self.assertEqual(me_res.data["email"], "vendor@example.com")
def test_vendor_profile_endpoint_forbidden_for_customer(self):
customer = User.objects.create_user(
email="customer@example.com",
password="Pass123456!",
is_customer=True,
is_vendor=False,
)
token_res = self.client.post(
reverse("token_obtain_pair"),
{"email": customer.email, "password": "Pass123456!"},
format="json",
)
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token_res.data['access']}")
res = self.client.get(reverse("vendor_profile_me"))
self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN)

12
accounts/urls.py Normal file
View File

@@ -0,0 +1,12 @@
from django.urls import path
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from .views import MeView, VendorProfileMeView, VendorRegistrationView
urlpatterns = [
path("register/vendor/", VendorRegistrationView.as_view(), name="register_vendor"),
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("me/", MeView.as_view(), name="me"),
path("vendor-profile/me/", VendorProfileMeView.as_view(), name="vendor_profile_me"),
]

36
accounts/views.py Normal file
View File

@@ -0,0 +1,36 @@
from rest_framework import generics, permissions
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import VendorProfile
from .serializers import UserMeSerializer, VendorProfileSerializer, VendorRegistrationSerializer
class VendorRegistrationView(generics.CreateAPIView):
serializer_class = VendorRegistrationSerializer
permission_classes = (permissions.AllowAny,)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
return Response(UserMeSerializer(user).data, status=201)
class MeView(APIView):
permission_classes = (permissions.IsAuthenticated,)
def get(self, request):
return Response(UserMeSerializer(request.user).data)
class VendorProfileMeView(generics.RetrieveUpdateAPIView):
serializer_class = VendorProfileSerializer
permission_classes = (permissions.IsAuthenticated,)
def get_object(self):
user = self.request.user
if not user.is_vendor:
raise PermissionDenied("Only vendors can access vendor profile setup.")
return VendorProfile.objects.get(user=user)