Skip to content

Commit f640994

Browse files
Initial commit: add base models for core eCommerce entities
0 parents  commit f640994

21 files changed

+538
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.venv
2+
.idea

api/__init__.py

Whitespace-only changes.

api/admin.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from django.contrib import admin
2+
3+
# Register your models here.

api/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class ApiConfig(AppConfig):
5+
default_auto_field = 'django.db.models.BigAutoField'
6+
name = 'api'

api/migrations/0001_initial.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Generated by Django 5.2 on 2025-04-12 12:41
2+
3+
import api.utils
4+
import django.contrib.auth.models
5+
import django.contrib.auth.validators
6+
import django.db.models.deletion
7+
import django.utils.timezone
8+
from django.conf import settings
9+
from django.db import migrations, models
10+
11+
12+
class Migration(migrations.Migration):
13+
14+
initial = True
15+
16+
dependencies = [
17+
('auth', '0012_alter_user_first_name_max_length'),
18+
]
19+
20+
operations = [
21+
migrations.CreateModel(
22+
name='Category',
23+
fields=[
24+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
25+
('name', models.CharField(max_length=255)),
26+
('slug', models.SlugField(max_length=255, unique=True)),
27+
],
28+
),
29+
migrations.CreateModel(
30+
name='User',
31+
fields=[
32+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
33+
('password', models.CharField(max_length=128, verbose_name='password')),
34+
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
35+
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
36+
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
37+
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
38+
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
39+
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
40+
('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')),
41+
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
42+
('email', models.EmailField(max_length=254, unique=True)),
43+
('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')),
44+
('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')),
45+
],
46+
options={
47+
'verbose_name': 'user',
48+
'verbose_name_plural': 'users',
49+
'abstract': False,
50+
},
51+
managers=[
52+
('objects', django.contrib.auth.models.UserManager()),
53+
],
54+
),
55+
migrations.CreateModel(
56+
name='Product',
57+
fields=[
58+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
59+
('name', models.CharField(max_length=255)),
60+
('slug', models.SlugField(max_length=255, unique=True)),
61+
('description', models.TextField()),
62+
('price', models.DecimalField(decimal_places=2, max_digits=10)),
63+
('stock', models.IntegerField()),
64+
('image', models.ImageField(blank=True, null=True, upload_to=api.utils.product_upload_to_unique)),
65+
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='api.category')),
66+
],
67+
),
68+
migrations.CreateModel(
69+
name='Profile',
70+
fields=[
71+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
72+
('bio', models.TextField(blank=True)),
73+
('location', models.CharField(blank=True, max_length=100)),
74+
('birth_date', models.DateField(blank=True, null=True)),
75+
('picture_url', models.ImageField(blank=True, null=True, upload_to=api.utils.profile_upload_to_unique)),
76+
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
77+
],
78+
),
79+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.2 on 2025-04-12 12:45
2+
3+
import uuid
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('api', '0001_initial'),
11+
]
12+
13+
operations = [
14+
migrations.RemoveField(
15+
model_name='product',
16+
name='id',
17+
),
18+
migrations.AddField(
19+
model_name='product',
20+
name='product_id',
21+
field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False),
22+
),
23+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2 on 2025-04-12 12:59
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api', '0002_remove_product_id_product_product_id'),
10+
]
11+
12+
operations = [
13+
migrations.RenameField(
14+
model_name='profile',
15+
old_name='picture_url',
16+
new_name='picture',
17+
),
18+
]

api/migrations/__init__.py

Whitespace-only changes.

api/models.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import uuid
2+
3+
from django.contrib.auth.models import AbstractUser
4+
from django.db import models
5+
from django.utils.text import slugify
6+
from taggit.managers import TaggableManager
7+
8+
from .utils import product_upload_to_unique, profile_upload_to_unique
9+
10+
11+
class User(AbstractUser):
12+
email = models.EmailField(unique=True)
13+
14+
15+
class Profile(models.Model):
16+
user = models.OneToOneField(User, on_delete=models.CASCADE)
17+
bio = models.TextField(blank=True)
18+
location = models.CharField(max_length=100, blank=True)
19+
birth_date = models.DateField(null=True, blank=True)
20+
picture = models.ImageField(upload_to=profile_upload_to_unique, null=True, blank=True)
21+
22+
def __str__(self):
23+
return f'{self.user.username} Profile'
24+
25+
26+
class Category(models.Model):
27+
name = models.CharField(max_length=255)
28+
slug = models.SlugField(max_length=255, unique=True)
29+
30+
def __str__(self):
31+
return self.name
32+
33+
34+
class Product(models.Model):
35+
product_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
36+
name = models.CharField(max_length=255)
37+
slug = models.SlugField(max_length=255, unique=True)
38+
description = models.TextField()
39+
price = models.DecimalField(max_digits=10, decimal_places=2)
40+
stock = models.IntegerField()
41+
image = models.ImageField(
42+
null=True,
43+
blank=True,
44+
upload_to=product_upload_to_unique
45+
)
46+
47+
category = models.ForeignKey(
48+
Category,
49+
related_name="products",
50+
on_delete=models.CASCADE,
51+
52+
)
53+
54+
objects = models.Manager()
55+
tags = TaggableManager
56+
57+
def save(self, *args, **kwargs):
58+
self.slug = slugify(self.name)
59+
super().save(*args, **kwargs)
60+
61+
def __str__(self):
62+
return self.name

api/serializers.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from rest_framework import serializers
2+
3+
from .models import Category, Product, User, Profile
4+
5+
6+
class UserSerializer(serializers.ModelSerializer):
7+
class ProfileSerializer(serializers.ModelSerializer):
8+
class Meta:
9+
model = Profile
10+
fields = ['bio', 'location', 'birth_date', 'picture']
11+
12+
profile = ProfileSerializer()
13+
14+
class Meta:
15+
model = User
16+
fields = ['username', 'profile', 'first_name', 'last_name']
17+
18+
19+
class CategorySerializer(serializers.ModelSerializer):
20+
class Meta:
21+
model = Category
22+
fields = ['name', 'slug']
23+
extra_kwargs = {'slug': {'read_only': True}} # Make the slug field read-only
24+
25+
26+
class ProductSerializer(serializers.ModelSerializer):
27+
category = CategorySerializer()
28+
29+
tags = serializers.ListField(
30+
child=serializers.CharField(),
31+
source='tags.names',
32+
required=False,
33+
)
34+
35+
def to_internal_value(self, data):
36+
if self.instance is None and 'category' not in data:
37+
raise serializers.ValidationError(
38+
{'category': 'This field is required when creating a product.'}
39+
)
40+
return super().to_internal_value(data)
41+
42+
def create(self, validated_data):
43+
# Custom create method to handle tags
44+
instance = super().create(**validated_data) # Create the Product instance
45+
tags = validated_data.pop('tags', None) # Extract tags from validated data
46+
if tags:
47+
instance.tags.set(*tags) # Assign tags to the product
48+
return instance
49+
50+
def update(self, instance, validated_data):
51+
# Custom update method to handle tags
52+
tags = validated_data.pop('tags', None) # Extract tags from validated data
53+
if tags:
54+
instance.tags.clear() # Clear existing tags
55+
instance.tags.set(*tags) # Assign new tags
56+
else:
57+
instance.tags.clear() # Clear tags if none are provided
58+
59+
return super().update(instance, validated_data)
60+
61+
class Meta:
62+
model = Product
63+
fields = [
64+
'product_id',
65+
'name',
66+
'slug',
67+
'description',
68+
'price',
69+
'stock',
70+
'image',
71+
'category',
72+
'tags'
73+
]

0 commit comments

Comments
 (0)