inital commit
This commit is contained in:
1
accounts/__init__.py
Normal file
1
accounts/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
50
accounts/admin.py
Normal file
50
accounts/admin.py
Normal 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
6
accounts/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "accounts"
|
||||
79
accounts/migrations/0001_initial.py
Normal file
79
accounts/migrations/0001_initial.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
20
accounts/migrations/0002_alter_user_managers.py
Normal file
20
accounts/migrations/0002_alter_user_managers.py
Normal 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()),
|
||||
],
|
||||
),
|
||||
]
|
||||
1
accounts/migrations/__init__.py
Normal file
1
accounts/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
81
accounts/models.py
Normal file
81
accounts/models.py
Normal 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
91
accounts/serializers.py
Normal 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
65
accounts/tests.py
Normal 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
12
accounts/urls.py
Normal 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
36
accounts/views.py
Normal 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)
|
||||
Reference in New Issue
Block a user