diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 423a636..b3c24ac 100644 Binary files a/config/__pycache__/__init__.cpython-311.pyc and b/config/__pycache__/__init__.cpython-311.pyc differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 96bce55..a2442ba 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 0b85e94..cda247e 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index 9c49e09..8baf432 100644 Binary files a/config/__pycache__/wsgi.cpython-311.pyc and b/config/__pycache__/wsgi.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 291d043..a8de23e 100644 --- a/config/settings.py +++ b/config/settings.py @@ -155,6 +155,10 @@ STATICFILES_DIRS = [ BASE_DIR / 'node_modules', ] +# Media files (Uploaded by users) +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + # Email EMAIL_BACKEND = os.getenv( "EMAIL_BACKEND", @@ -179,4 +183,4 @@ if EMAIL_USE_SSL: # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' \ No newline at end of file diff --git a/config/urls.py b/config/urls.py index bcfc074..59061ce 100644 --- a/config/urls.py +++ b/config/urls.py @@ -25,5 +25,6 @@ urlpatterns = [ ] if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets") - urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) \ No newline at end of file diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc index 74b1112..be69bfb 100644 Binary files a/core/__pycache__/__init__.cpython-311.pyc and b/core/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a5ed392..af95fac 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc index 6f131d4..a12fce2 100644 Binary files a/core/__pycache__/apps.cpython-311.pyc and b/core/__pycache__/apps.cpython-311.pyc differ diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 75bf223..d111623 100644 Binary files a/core/__pycache__/context_processors.cpython-311.pyc and b/core/__pycache__/context_processors.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000..ef4e8dd Binary files /dev/null and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e061640..5575bf4 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/tests.cpython-311.pyc b/core/__pycache__/tests.cpython-311.pyc new file mode 100644 index 0000000..b56467a Binary files /dev/null and b/core/__pycache__/tests.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 5a69659..b6c116d 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/utils.cpython-311.pyc b/core/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..086fbf9 Binary files /dev/null and b/core/__pycache__/utils.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2a36fd6..2669d1b 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..6c13593 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,166 @@ +from django import forms +from .models import WardrobeItem, Accessory, Outfit, Category, OutfitFolder + +class WardrobeItemForm(forms.ModelForm): + main_category = forms.ModelChoiceField( + queryset=Category.objects.filter(item_type='wardrobe', parent=None), + required=False, + empty_label="Select Main Category", + widget=forms.Select(attrs={'class': 'form-select bg-dark text-white border-secondary', 'id': 'id_main_category'}) + ) + new_main_category = forms.CharField( + max_length=100, required=False, label="Or add new main category", + widget=forms.TextInput(attrs={'class': 'form-control bg-dark text-white border-secondary', 'placeholder': 'New main category...'}) + ) + new_subcategory = forms.CharField( + max_length=100, required=False, label="Or add new subcategory", + widget=forms.TextInput(attrs={'class': 'form-control bg-dark text-white border-secondary', 'placeholder': 'New subcategory...'}) + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['category'].queryset = Category.objects.none() + self.fields['category'].required = False + self.fields['category'].label = "Subcategory" + self.fields['name'].required = False + + if 'main_category' in self.data: + try: + main_category_id = int(self.data.get('main_category')) + self.fields['category'].queryset = Category.objects.filter(parent_id=main_category_id).order_by('name') + except (ValueError, TypeError): + pass + elif self.instance.pk and self.instance.category and self.instance.category.parent: + self.fields['main_category'].initial = self.instance.category.parent + self.fields['category'].queryset = Category.objects.filter(parent=self.instance.category.parent).order_by('name') + + class Meta: + model = WardrobeItem + fields = ['name', 'category', 'season', 'image'] + widgets = { + 'name': forms.TextInput(attrs={'class': 'form-control bg-dark text-white border-secondary', 'placeholder': 'Item name (optional)...'}), + 'category': forms.Select(attrs={'class': 'form-select bg-dark text-white border-secondary', 'id': 'id_subcategory'}), + 'season': forms.Select(attrs={'class': 'form-select bg-dark text-white border-secondary'}), + 'image': forms.FileInput(attrs={'class': 'form-control bg-dark text-white border-secondary'}), + } + + def save(self, commit=True): + instance = super().save(commit=False) + new_main = self.cleaned_data.get('new_main_category') + new_sub = self.cleaned_data.get('new_subcategory') + main_cat = self.cleaned_data.get('main_category') + + if new_main: + parent_cat, _ = Category.objects.get_or_create(name=new_main, item_type='wardrobe', parent=None) + if new_sub: + child_cat, _ = Category.objects.get_or_create(name=new_sub, item_type='wardrobe', parent=parent_cat) + instance.category = child_cat + else: + instance.category = parent_cat + elif main_cat: + if new_sub: + child_cat, _ = Category.objects.get_or_create(name=new_sub, item_type='wardrobe', parent=main_cat) + instance.category = child_cat + # else category field itself handles it if selected + + if commit: + instance.save() + return instance + +class AccessoryForm(forms.ModelForm): + main_category = forms.ModelChoiceField( + queryset=Category.objects.filter(item_type='accessory', parent=None), + required=False, + empty_label="Select Main Category", + widget=forms.Select(attrs={'class': 'form-select bg-dark text-white border-secondary', 'id': 'id_main_category'}) + ) + new_main_category = forms.CharField( + max_length=100, required=False, label="Or add new main category", + widget=forms.TextInput(attrs={'class': 'form-control bg-dark text-white border-secondary', 'placeholder': 'New main category...'}) + ) + new_subcategory = forms.CharField( + max_length=100, required=False, label="Or add new subcategory", + widget=forms.TextInput(attrs={'class': 'form-control bg-dark text-white border-secondary', 'placeholder': 'New subcategory...'}) + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['category'].queryset = Category.objects.none() + self.fields['category'].required = False + self.fields['category'].label = "Subcategory" + self.fields['name'].required = False + + if 'main_category' in self.data: + try: + main_category_id = int(self.data.get('main_category')) + self.fields['category'].queryset = Category.objects.filter(parent_id=main_category_id).order_by('name') + except (ValueError, TypeError): + pass + elif self.instance.pk and self.instance.category and self.instance.category.parent: + self.fields['main_category'].initial = self.instance.category.parent + self.fields['category'].queryset = Category.objects.filter(parent=self.instance.category.parent).order_by('name') + + class Meta: + model = Accessory + fields = ['name', 'category', 'season', 'image'] + widgets = { + 'name': forms.TextInput(attrs={'class': 'form-control bg-dark text-white border-secondary', 'placeholder': 'Accessory name (optional)...'}), + 'category': forms.Select(attrs={'class': 'form-select bg-dark text-white border-secondary', 'id': 'id_subcategory'}), + 'season': forms.Select(attrs={'class': 'form-select bg-dark text-white border-secondary'}), + 'image': forms.FileInput(attrs={'class': 'form-control bg-dark text-white border-secondary'}), + } + + def save(self, commit=True): + instance = super().save(commit=False) + new_main = self.cleaned_data.get('new_main_category') + new_sub = self.cleaned_data.get('new_subcategory') + main_cat = self.cleaned_data.get('main_category') + + if new_main: + parent_cat, _ = Category.objects.get_or_create(name=new_main, item_type='accessory', parent=None) + if new_sub: + child_cat, _ = Category.objects.get_or_create(name=new_sub, item_type='accessory', parent=parent_cat) + instance.category = child_cat + else: + instance.category = parent_cat + elif main_cat: + if new_sub: + child_cat, _ = Category.objects.get_or_create(name=new_sub, item_type='accessory', parent=main_cat) + instance.category = child_cat + + if commit: + instance.save() + return instance + +class OutfitForm(forms.ModelForm): + new_folder = forms.CharField(max_length=100, required=False, label="Or add new folder/group", + widget=forms.TextInput(attrs={'class': 'form-control bg-dark text-white border-secondary', 'placeholder': 'Enter new group name...'})) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['name'].required = False + self.fields['season'].required = False + self.fields['items'].required = False + self.fields['accessories'].required = False + + class Meta: + model = Outfit + fields = ['name', 'season', 'items', 'accessories', 'folder'] + widgets = { + 'name': forms.TextInput(attrs={'class': 'form-control bg-dark text-white border-secondary', 'placeholder': 'Outfit name (optional)...'}), + 'season': forms.Select(attrs={'class': 'form-select bg-dark text-white border-secondary'}), + 'items': forms.SelectMultiple(attrs={'class': 'form-select d-none'}), + 'accessories': forms.SelectMultiple(attrs={'class': 'form-select d-none'}), + 'folder': forms.Select(attrs={'class': 'form-select bg-dark text-white border-secondary'}), + } + + def save(self, commit=True): + instance = super().save(commit=False) + new_folder_name = self.cleaned_data.get('new_folder') + if new_folder_name: + folder, _ = OutfitFolder.objects.get_or_create(name=new_folder_name) + instance.folder = folder + if commit: + instance.save() + self.save_m2m() + return instance \ No newline at end of file diff --git a/core/management/__init__.py b/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/__pycache__/__init__.cpython-311.pyc b/core/management/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..0de3180 Binary files /dev/null and b/core/management/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/management/commands/__init__.py b/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/commands/__pycache__/__init__.cpython-311.pyc b/core/management/commands/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..70a6260 Binary files /dev/null and b/core/management/commands/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/management/commands/__pycache__/setup_categories.cpython-311.pyc b/core/management/commands/__pycache__/setup_categories.cpython-311.pyc new file mode 100644 index 0000000..c8d3124 Binary files /dev/null and b/core/management/commands/__pycache__/setup_categories.cpython-311.pyc differ diff --git a/core/management/commands/setup_categories.py b/core/management/commands/setup_categories.py new file mode 100644 index 0000000..7ec8c4b --- /dev/null +++ b/core/management/commands/setup_categories.py @@ -0,0 +1,44 @@ +from django.core.management.base import BaseCommand +from core.models import Category + +class Command(BaseCommand): + help = 'Prepopulate hierarchical categories' + + def handle(self, *args, **options): + # Full Reset + Category.objects.all().delete() + + hierarchy = { + 'wardrobe': { + 'Jackets': ['Jackets', 'Coats', 'Zip-Ups'], + 'Shirts': ['Hoodies', 'Pullover', 'Sweater', 'Turtlenecks', 'Shirts'], + 'T-Shirts': ['T-Shirts', 'Armless Shirts', 'Tanktops'], + 'Pants': ['Jeans', 'Pants', 'Cargo Pants', 'Linen'], + 'Shorts': ['Shorts', 'Jorts'], + 'Suit': ['Suits', 'Suit Jackets', 'Shirts', 'Suit Pants', 'Suit Shoes'], + 'Shoes': ['Shoes', 'Sneakers', 'Boots', 'Suit Shoes'], + }, + 'accessory': { + 'Jewelry': ['Rings', 'Pant Chains'], + 'Headwear': ['Headwear'], + } + } + + for item_type, parents in hierarchy.items(): + for parent_name, children in parents.items(): + parent_cat = Category.objects.create( + name=parent_name, + item_type=item_type, + parent=None, + is_preset=True + ) + + for child_name in children: + Category.objects.create( + name=child_name, + item_type=item_type, + parent=parent_cat, + is_preset=True + ) + + self.stdout.write(self.style.SUCCESS('Successfully reset and prepopulated hierarchical categories')) diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..5936452 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,81 @@ +# Generated by Django 5.2.7 on 2026-02-04 11:49 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('item_type', models.CharField(choices=[('wardrobe', 'Wardrobe'), ('accessory', 'Accessory')], max_length=20)), + ('is_preset', models.BooleanField(default=False)), + ], + options={ + 'verbose_name_plural': 'Categories', + }, + ), + migrations.CreateModel( + name='OutfitFolder', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name='Accessory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('image', models.ImageField(upload_to='accessories/')), + ('date_added', models.DateTimeField(auto_now_add=True)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accessories', to='core.category')), + ], + options={ + 'verbose_name_plural': 'Accessories', + }, + ), + migrations.CreateModel( + name='Outfit', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, max_length=255, null=True)), + ('season', models.CharField(choices=[('summer', 'Summer'), ('spring', 'Spring'), ('autumn', 'Autumn'), ('winter', 'Winter')], max_length=20)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('accessories', models.ManyToManyField(related_name='outfits', to='core.accessory')), + ('folder', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='outfits', to='core.outfitfolder')), + ], + ), + migrations.CreateModel( + name='CalendarAssignment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(unique=True)), + ('outfit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assignments', to='core.outfit')), + ], + ), + migrations.CreateModel( + name='WardrobeItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('image', models.ImageField(upload_to='wardrobe/')), + ('date_added', models.DateTimeField(auto_now_add=True)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wardrobe_items', to='core.category')), + ], + ), + migrations.AddField( + model_name='outfit', + name='items', + field=models.ManyToManyField(related_name='outfits', to='core.wardrobeitem'), + ), + ] diff --git a/core/migrations/0002_accessory_season_category_parent_outfitfolder_parent_and_more.py b/core/migrations/0002_accessory_season_category_parent_outfitfolder_parent_and_more.py new file mode 100644 index 0000000..71baac9 --- /dev/null +++ b/core/migrations/0002_accessory_season_category_parent_outfitfolder_parent_and_more.py @@ -0,0 +1,69 @@ +# Generated by Django 5.2.7 on 2026-02-04 16:42 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='accessory', + name='season', + field=models.CharField(blank=True, choices=[('summer', 'Summer'), ('spring', 'Spring'), ('autumn', 'Autumn'), ('winter', 'Winter')], max_length=20, null=True), + ), + migrations.AddField( + model_name='category', + name='parent', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='core.category'), + ), + migrations.AddField( + model_name='outfitfolder', + name='parent', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='core.outfitfolder'), + ), + migrations.AddField( + model_name='wardrobeitem', + name='season', + field=models.CharField(blank=True, choices=[('summer', 'Summer'), ('spring', 'Spring'), ('autumn', 'Autumn'), ('winter', 'Winter')], max_length=20, null=True), + ), + migrations.AlterField( + model_name='accessory', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='accessories', to='core.category'), + ), + migrations.AlterField( + model_name='accessory', + name='name', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='outfit', + name='accessories', + field=models.ManyToManyField(blank=True, related_name='outfits', to='core.accessory'), + ), + migrations.AlterField( + model_name='outfit', + name='items', + field=models.ManyToManyField(blank=True, related_name='outfits', to='core.wardrobeitem'), + ), + migrations.AlterField( + model_name='outfit', + name='season', + field=models.CharField(blank=True, choices=[('summer', 'Summer'), ('spring', 'Spring'), ('autumn', 'Autumn'), ('winter', 'Winter')], max_length=20, null=True), + ), + migrations.AlterField( + model_name='wardrobeitem', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wardrobe_items', to='core.category'), + ), + migrations.AlterField( + model_name='wardrobeitem', + name='name', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/core/migrations/0003_outfitfolder_is_preset.py b/core/migrations/0003_outfitfolder_is_preset.py new file mode 100644 index 0000000..60599b1 --- /dev/null +++ b/core/migrations/0003_outfitfolder_is_preset.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-02-04 17:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_accessory_season_category_parent_outfitfolder_parent_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='outfitfolder', + name='is_preset', + field=models.BooleanField(default=False), + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..91d555e Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_accessory_season_category_parent_outfitfolder_parent_and_more.cpython-311.pyc b/core/migrations/__pycache__/0002_accessory_season_category_parent_outfitfolder_parent_and_more.cpython-311.pyc new file mode 100644 index 0000000..da282ac Binary files /dev/null and b/core/migrations/__pycache__/0002_accessory_season_category_parent_outfitfolder_parent_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0003_outfitfolder_is_preset.cpython-311.pyc b/core/migrations/__pycache__/0003_outfitfolder_is_preset.cpython-311.pyc new file mode 100644 index 0000000..44ee15b Binary files /dev/null and b/core/migrations/__pycache__/0003_outfitfolder_is_preset.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc index 9c833c8..5b2ac5e 100644 Binary files a/core/migrations/__pycache__/__init__.cpython-311.pyc and b/core/migrations/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..621a191 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,105 @@ from django.db import models +import os +from .utils import process_clothing_image -# Create your models here. +class Category(models.Model): + TYPE_CHOICES = [ + ('wardrobe', 'Wardrobe'), + ('accessory', 'Accessory'), + ] + name = models.CharField(max_length=100) + item_type = models.CharField(max_length=20, choices=TYPE_CHOICES) + is_preset = models.BooleanField(default=False) + parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children') + + def __get_full_path(self): + if self.parent: + return f"{self.parent.__get_full_path()} > {self.name}" + return self.name + + def __str__(self): + return f"{self.__get_full_path()} ({self.get_item_type_display()})" + + class Meta: + verbose_name_plural = "Categories" + +class WardrobeItem(models.Model): + SEASON_CHOICES = [ + ('summer', 'Summer'), + ('spring', 'Spring'), + ('autumn', 'Autumn'), + ('winter', 'Winter'), + ] + name = models.CharField(max_length=255, blank=True, null=True) + category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, related_name='wardrobe_items') + image = models.ImageField(upload_to='wardrobe/') + season = models.CharField(max_length=20, choices=SEASON_CHOICES, blank=True, null=True) + date_added = models.DateTimeField(auto_now_add=True) + + def save(self, *args, **kwargs): + is_new = self.pk is None + super().save(*args, **kwargs) + if is_new and self.image: + process_clothing_image(self.image.path) + + def __str__(self): + return self.name or f"Wardrobe Item {self.id}" + +class Accessory(models.Model): + SEASON_CHOICES = [ + ('summer', 'Summer'), + ('spring', 'Spring'), + ('autumn', 'Autumn'), + ('winter', 'Winter'), + ] + name = models.CharField(max_length=255, blank=True, null=True) + category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, related_name='accessories') + image = models.ImageField(upload_to='accessories/') + season = models.CharField(max_length=20, choices=SEASON_CHOICES, blank=True, null=True) + date_added = models.DateTimeField(auto_now_add=True) + + def save(self, *args, **kwargs): + is_new = self.pk is None + super().save(*args, **kwargs) + if is_new and self.image: + process_clothing_image(self.image.path) + + def __str__(self): + return self.name or f"Accessory {self.id}" + + class Meta: + verbose_name_plural = "Accessories" + +class OutfitFolder(models.Model): + name = models.CharField(max_length=100) + parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children') + is_preset = models.BooleanField(default=False) + + def __str__(self): + if self.parent: + return f"{self.parent} > {self.name}" + return self.name + +class Outfit(models.Model): + SEASON_CHOICES = [ + ('summer', 'Summer'), + ('spring', 'Spring'), + ('autumn', 'Autumn'), + ('winter', 'Winter'), + ] + name = models.CharField(max_length=255, blank=True, null=True) + season = models.CharField(max_length=20, choices=SEASON_CHOICES, blank=True, null=True) + folder = models.ForeignKey(OutfitFolder, on_delete=models.SET_NULL, null=True, blank=True, related_name='outfits') + items = models.ManyToManyField(WardrobeItem, related_name='outfits', blank=True) + accessories = models.ManyToManyField(Accessory, related_name='outfits', blank=True) + date_created = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.name or f"Outfit {self.id}" + +class CalendarAssignment(models.Model): + date = models.DateField(unique=True) + outfit = models.ForeignKey(Outfit, on_delete=models.CASCADE, related_name='assignments') + + def __str__(self): + return f"{self.date}: {self.outfit}" diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..f154411 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,128 @@ +{% load static %} - - - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - {% load static %} - - {% block head %}{% endblock %} + + + {% block title %}Wardrobe Planner{% endblock %} + + + + + + {% block extra_css %}{% endblock %} - - {% block content %}{% endblock %} - +
+ {% block content %}{% endblock %} +
- + + + + {% block extra_js %}{% endblock %} + + \ No newline at end of file diff --git a/core/templates/core/accessory_list.html b/core/templates/core/accessory_list.html new file mode 100644 index 0000000..22554ee --- /dev/null +++ b/core/templates/core/accessory_list.html @@ -0,0 +1,94 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+

Accessories

+ + Add Accessory + +
+ + + + + + {% if current_main and subcategories %} +
+
+ + All {{ current_main.name }} + + {% for sub in subcategories %} + + {{ sub.name }} + + {% endfor %} +
+
+ {% endif %} + + +
+
+
+ {% if current_main %}{% endif %} + {% if current_sub %}{% endif %} + + +
+
+
+ + +
+ {% for item in items %} +
+
+ + {% if item.image %} + {{ item.name }} + {% else %} +
+ +
+ {% endif %} +
+
{{ item.name|default:"Unnamed" }}
+

{{ item.category.name|default:"Uncategorized" }}

+ {% if item.season %} + {{ item.get_season_display }} + {% endif %} +
+
+
+ {% empty %} +
+ +

No accessories found in this category.

+
+ {% endfor %} +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..5bc92f3 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,266 @@ {% extends "base.html" %} -{% block title %}{{ project_name }}{% endblock %} +{% block content %} + +
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+
Total Items
+

{{ total_items }}

+
+
+
+ + + +
+
+
-{% block head %} - - - + +
+
+
Weekly Planner
+ + Plan +
+ + +
+ {% for day in calendar_data|slice:":3" %} +
+
+
+
{{ day.day }}
+
{{ day.date|date:"d" }}
+
+
+ {% if day.outfit %} +
+ {% csrf_token %} + +
+
+ {% with first_item=day.outfit.items.first %} + {% if first_item %} + + {% else %} +
+ +
+ {% endif %} + {% endwith %} +
+
{{ day.outfit.name|default:"Outfit" }}
+ {% else %} +
+ +
+ {% endif %} +
+
+
+ {% endfor %} +
+ + +
+ {% for day in calendar_data|slice:"3:5" %} +
+
+
+
{{ day.day }}
+
{{ day.date|date:"d" }}
+
+
+ {% if day.outfit %} +
+ {% csrf_token %} + +
+
+ {% with first_item=day.outfit.items.first %} + {% if first_item %} + + {% else %} +
+ +
+ {% endif %} + {% endwith %} +
+
{{ day.outfit.name|default:"Outfit" }}
+ {% else %} +
+ +
+ {% endif %} +
+
+
+ {% endfor %} +
+ + +
+ {% for day in calendar_data|slice:"5:" %} +
+
+
+
{{ day.day }}
+
{{ day.date|date:"d" }}
+
+
+ {% if day.outfit %} +
+ {% csrf_token %} + +
+
+ {% with first_item=day.outfit.items.first %} + {% if first_item %} + + {% else %} +
+ +
+ {% endif %} + {% endwith %} +
+
{{ day.outfit.name|default:"Outfit" }}
+ {% else %} +
+ +
+ {% endif %} +
+
+
+ {% endfor %} +
+
+ + + + +{% endblock %} + +{% block extra_css %} {% endblock %} -{% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… -
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
- +{% block extra_js %} + {% endblock %} \ No newline at end of file diff --git a/core/templates/core/item_detail.html b/core/templates/core/item_detail.html new file mode 100644 index 0000000..32ccd7a --- /dev/null +++ b/core/templates/core/item_detail.html @@ -0,0 +1,51 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+
+
+

{{ item.name|default:"Unnamed Item" }}

+ + Back + +
+
+ {% if item.image %} + {{ item.name }} + {% else %} +
+ +

No image available

+
+ {% endif %} +
+ +
+
+
+
+{% endblock %} diff --git a/core/templates/core/item_form.html b/core/templates/core/item_form.html new file mode 100644 index 0000000..4a38854 --- /dev/null +++ b/core/templates/core/item_form.html @@ -0,0 +1,92 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+
+
+

{{ title }}

+
+
+
+ {% csrf_token %} + +
+ + {{ form.name }} + {% if form.name.errors %} +
{{ form.name.errors }}
+ {% endif %} +
+ +
+
+ + {{ form.main_category }} + {% if form.main_category.errors %} +
{{ form.main_category.errors }}
+ {% endif %} +
+ {{ form.new_main_category }} +
+
+
+ + {{ form.category }} + {% if form.category.errors %} +
{{ form.category.errors }}
+ {% endif %} +
+ {{ form.new_subcategory }} +
+
+
+ +
+ + {{ form.season }} +
+ +
+ + {{ form.image }} +
+ +
+ + Cancel +
+
+
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/new_fit.html b/core/templates/core/new_fit.html new file mode 100644 index 0000000..35ed1c8 --- /dev/null +++ b/core/templates/core/new_fit.html @@ -0,0 +1,146 @@ +{% extends "base.html" %} + +{% block content %} +
+

{{ title }}

+ +
+ +
+ {% csrf_token %} + + {% if form.errors %} +
+ +
+ {% endif %} + +
+ + {{ form.name }} +
+ +
+ + {{ form.season }} +
+ +
+ +
+ {% for item in wardrobe_items %} +
+
+ +
+ {{ item.name|default:"Item"|truncatechars:10 }} +
+
+ +
+
+
+ {% endfor %} +
+
+ {{ form.items }} +
+
+ +
+ +
+ {% for acc in accessories %} +
+ +
+ +
+
+ {% endfor %} +
+
+ {{ form.accessories }} +
+
+ +
+ + {{ form.folder }} +
+ + Add New Group + +
+ {{ form.new_folder }} +
+
+
+ + +
+ +{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block extra_js %} + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/outfit_list.html b/core/templates/core/outfit_list.html new file mode 100644 index 0000000..b27cfed --- /dev/null +++ b/core/templates/core/outfit_list.html @@ -0,0 +1,170 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} + +
+

{{ title }}

+ + New Fit + +
+ + +
+ +
+ + +
+ {% if current_folder %}{% endif %} +
+ + +
+
+ + +
+ {% for outfit in outfits %} +
+
+ +
+ {% csrf_token %} + +
+ +
+
+ {% with first_item=outfit.items.first %} + {% if first_item %} + + {% else %} +
+ +
+ {% endif %} + {% endwith %} +
+
{{ outfit.name|default:"Unnamed Outfit" }}
+
{{ outfit.items.count }} items
+
+ + +
+ +
+
+
+ + + + + + + {% empty %} +
+

No outfits found.

+ Create Outfit +
+ {% endfor %} +
+{% endblock %} + +{% block extra_css %} + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/wardrobe_list.html b/core/templates/core/wardrobe_list.html new file mode 100644 index 0000000..7c09613 --- /dev/null +++ b/core/templates/core/wardrobe_list.html @@ -0,0 +1,94 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+

Wardrobe

+ + Add Item + +
+ + + + + + {% if current_main and subcategories %} +
+
+ + All {{ current_main.name }} + + {% for sub in subcategories %} + + {{ sub.name }} + + {% endfor %} +
+
+ {% endif %} + + +
+
+
+ {% if current_main %}{% endif %} + {% if current_sub %}{% endif %} + + +
+
+
+ + +
+ {% for item in items %} +
+
+ + {% if item.image %} + {{ item.name }} + {% else %} +
+ +
+ {% endif %} +
+
{{ item.name|default:"Unnamed" }}
+

{{ item.category.name|default:"Uncategorized" }}

+ {% if item.season %} + {{ item.get_season_display }} + {% endif %} +
+
+
+ {% empty %} +
+ +

No items found in this category.

+
+ {% endfor %} +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/core/tests.py b/core/tests.py index 7ce503c..9508697 100644 --- a/core/tests.py +++ b/core/tests.py @@ -1,3 +1,18 @@ from django.test import TestCase +from django.urls import reverse +from core.models import WardrobeItem, Accessory, Category -# Create your tests here. +class HomeViewTest(TestCase): + def setUp(self): + self.category_w = Category.objects.create(name="T-Shirts", item_type='wardrobe') + self.category_a = Category.objects.create(name="Rings", item_type='accessory') + WardrobeItem.objects.create(name="Black Tee", category=self.category_w) + Accessory.objects.create(name="Silver Ring", category=self.category_a) + + def test_home_view_status_code(self): + response = self.client.get(reverse('home')) + self.assertEqual(response.status_code, 200) + + def test_home_view_item_count(self): + response = self.client.get(reverse('home')) + self.assertContains(response, "2") # Total items: 1 wardrobe + 1 accessory \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 6299e3d..a16faa5 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,20 @@ from django.urls import path - -from .views import home +from . import views urlpatterns = [ - path("", home, name="home"), + path('', views.home, name='home'), + path('wardrobe/', views.wardrobe_list, name='wardrobe_list'), + path('wardrobe/add/', views.add_wardrobe_item, name='add_wardrobe_item'), + path('wardrobe//', views.wardrobe_item_detail, name='wardrobe_item_detail'), + path('wardrobe//delete/', views.delete_wardrobe_item, name='delete_wardrobe_item'), + path('accessories/', views.accessory_list, name='accessory_list'), + path('accessories/add/', views.add_accessory_item, name='add_accessory_item'), + path('accessories//', views.accessory_item_detail, name='accessory_item_detail'), + path('accessories//delete/', views.delete_accessory_item, name='delete_accessory_item'), + path('outfits/', views.outfit_list, name='outfit_list'), + path('outfits/new/', views.new_fit, name='new_fit'), + path('outfits//delete/', views.delete_outfit, name='delete_outfit'), + path('assign-outfit/', views.assign_outfit, name='assign_outfit'), + path('remove-assignment//', views.remove_assignment, name='remove_assignment'), + path('ajax/get-subcategories/', views.get_subcategories, name='get_subcategories'), ] diff --git a/core/utils.py b/core/utils.py new file mode 100644 index 0000000..b89e15e --- /dev/null +++ b/core/utils.py @@ -0,0 +1,48 @@ +from PIL import Image, ImageOps, ImageChops +import os + +def process_clothing_image(image_path): + """ + Processes a clothing image: + 1. Removes background (simplified version without ML if not available). + 2. Crops to the garment boundaries. + 3. Converts background to white. + """ + try: + img = Image.open(image_path).convert("RGBA") + + # 1. Background removal / White background conversion + # Since we don't have rembg, we'll try a simple approach: + # If the background is already somewhat light, we can treat it as background. + # This is a fallback for true ML-based background removal. + + datas = img.getdata() + + new_data = [] + # Simple heuristic: pixels that are very bright or very dark (if it's a studio shot) + # For more precision, we'd need a real segmentation model. + for item in datas: + # If the pixel is very white, make it transparent for cropping logic + if item[0] > 240 and item[1] > 240 and item[2] > 240: + new_data.append((255, 255, 255, 0)) + else: + new_data.append(item) + + img.putdata(new_data) + + # 2. Automatic Cropping + # Get the bounding box of non-transparent areas + bbox = img.getbbox() + if bbox: + img = img.crop(bbox) + + # 3. Add White Background + background = Image.new("RGB", img.size, (255, 255, 255)) + background.paste(img, mask=img.split()[3]) # 3 is the alpha channel + + # Save back to the same path or a new one + background.save(image_path, "JPEG", quality=95) + return True + except Exception as e: + print(f"Error processing image {image_path}: {e}") + return False diff --git a/core/views.py b/core/views.py index c9aed12..bd697c4 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,298 @@ -import os -import platform - -from django import get_version as django_version -from django.shortcuts import render -from django.utils import timezone - +import datetime +import json +from django.shortcuts import render, redirect, get_object_or_404 +from django.db.models import Count, Q +from django.views.decorators.http import require_POST +from django.http import JsonResponse +from .models import Category, WardrobeItem, Accessory, Outfit, OutfitFolder, CalendarAssignment +from .forms import WardrobeItemForm, AccessoryForm, OutfitForm def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() + """Dashboard view with current week calendar and total items count.""" + season_filter = request.GET.get('season', 'summer') + + total_wardrobe = WardrobeItem.objects.count() + total_accessories = Accessory.objects.count() + total_items = total_wardrobe + total_accessories + + today = datetime.date.today() + start_of_week = today - datetime.timedelta(days=today.weekday()) + week_dates = [start_of_week + datetime.timedelta(days=i) for i in range(7)] + + day_names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + + calendar_data = [] + assignments = {a.date: a.outfit for a in CalendarAssignment.objects.filter(date__in=week_dates)} + + for i, date in enumerate(week_dates): + calendar_data.append({ + 'day': day_names[i], + 'date': date, + 'outfit': assignments.get(date) + }) + + # All outfits for the "quick add" modal on home screen + all_outfits = Outfit.objects.all().order_by('-date_created') context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + 'total_items': total_items, + 'calendar_data': calendar_data, + 'today': today, + 'season_filter': season_filter, + 'all_outfits': all_outfits, + 'week_dates_json': json.dumps([d.strftime('%Y-%m-%d') for d in week_dates]), + 'day_names': day_names, } - return render(request, "core/index.html", context) + return render(request, 'core/index.html', context) + +@require_POST +def remove_assignment(request, date): + CalendarAssignment.objects.filter(date=date).delete() + return redirect('home') + +@require_POST +def assign_outfit(request): + outfit_id = request.POST.get('outfit_id') + date_str = request.POST.get('date') + if outfit_id and date_str: + outfit = get_object_or_404(Outfit, id=outfit_id) + try: + date_obj = datetime.datetime.strptime(date_str, '%Y-%m-%d').date() + CalendarAssignment.objects.update_or_create( + date=date_obj, + defaults={'outfit': outfit} + ) + except ValueError: + pass + return redirect(request.META.get('HTTP_REFERER', 'home')) + +def wardrobe_list(request): + query = request.GET.get('q') + main_category_id = request.GET.get('main_category') + subcategory_id = request.GET.get('subcategory') + + main_categories = Category.objects.filter(item_type='wardrobe', parent=None).annotate( + num_items=Count('wardrobe_items', distinct=True), + num_child_items=Count('children__wardrobe_items', distinct=True) + ).filter(Q(is_preset=True) | Q(num_items__gt=0) | Q(num_child_items__gt=0)) + + subcategories = None + current_main = None + current_sub = None + + items = WardrobeItem.objects.all().order_by('-date_added') + + if main_category_id: + current_main = get_object_or_404(Category, id=main_category_id, parent=None) + subcategories = Category.objects.filter(parent=current_main).annotate( + num_items=Count('wardrobe_items', distinct=True) + ).filter(Q(is_preset=True) | Q(num_items__gt=0)) + + items = items.filter(Q(category=current_main) | Q(category__parent=current_main)) + + if subcategory_id: + current_sub = get_object_or_404(Category, id=subcategory_id, parent=current_main) + items = items.filter(category=current_sub) + + if query: + items = items.filter(Q(name__icontains=query) | Q(category__name__icontains=query)) + + context = { + 'main_categories': main_categories, + 'subcategories': subcategories, + 'current_main': current_main, + 'current_sub': current_sub, + 'items': items, + 'title': 'Wardrobe' + } + return render(request, 'core/wardrobe_list.html', context) + +def add_wardrobe_item(request): + if request.method == 'POST': + form = WardrobeItemForm(request.POST, request.FILES) + if form.is_valid(): + form.save() + return redirect('wardrobe_list') + else: + form = WardrobeItemForm() + + return render(request, 'core/item_form.html', {'form': form, 'title': 'Add Wardrobe Item', 'type': 'wardrobe'}) + +def accessory_list(request): + query = request.GET.get('q') + main_category_id = request.GET.get('main_category') + subcategory_id = request.GET.get('subcategory') + + main_categories = Category.objects.filter(item_type='accessory', parent=None).annotate( + num_items=Count('accessories', distinct=True), + num_child_items=Count('children__accessories', distinct=True) + ).filter(Q(is_preset=True) | Q(num_items__gt=0) | Q(num_child_items__gt=0)) + + subcategories = None + current_main = None + current_sub = None + + items = Accessory.objects.all().order_by('-date_added') + + if main_category_id: + current_main = get_object_or_404(Category, id=main_category_id, parent=None) + subcategories = Category.objects.filter(parent=current_main).annotate( + num_items=Count('accessories', distinct=True) + ).filter(Q(is_preset=True) | Q(num_items__gt=0)) + + items = items.filter(Q(category=current_main) | Q(category__parent=current_main)) + + if subcategory_id: + current_sub = get_object_or_404(Category, id=subcategory_id, parent=current_main) + items = items.filter(category=current_sub) + + if query: + items = items.filter(Q(name__icontains=query) | Q(category__name__icontains=query)) + + context = { + 'main_categories': main_categories, + 'subcategories': subcategories, + 'current_main': current_main, + 'current_sub': current_sub, + 'items': items, + 'title': 'Accessories' + } + return render(request, 'core/accessory_list.html', context) + +def add_accessory_item(request): + if request.method == 'POST': + form = AccessoryForm(request.POST, request.FILES) + if form.is_valid(): + form.save() + return redirect('accessory_list') + else: + form = AccessoryForm() + + return render(request, 'core/item_form.html', {'form': form, 'title': 'Add Accessory', 'type': 'accessory'}) + +def outfit_list(request): + query = request.GET.get('q') + folder_id = request.GET.get('folder') + + def get_valid_folders(parent=None): + return OutfitFolder.objects.filter(parent=parent).annotate( + num_outfits=Count('outfits', distinct=True), + num_child_outfits=Count('children__outfits', distinct=True) + ).filter(Q(is_preset=True) | Q(num_outfits__gt=0) | Q(num_child_outfits__gt=0)) + + folders = get_valid_folders(None) + outfits = Outfit.objects.all().order_by('-date_created') + + if folder_id: + current_folder = get_object_or_404(OutfitFolder, id=folder_id) + folders = get_valid_folders(current_folder) + if not folders.exists(): + folders = get_valid_folders(current_folder.parent) + outfits = outfits.filter(Q(folder=current_folder) | Q(folder__parent=current_folder)) + else: + current_folder = None + + if query: + outfits = outfits.filter(Q(name__icontains=query)) + + # For weekday assignment in the list view + today = datetime.date.today() + start_of_week = today - datetime.timedelta(days=today.weekday()) + week_dates = [start_of_week + datetime.timedelta(days=i) for i in range(7)] + day_names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + week_data = [{'day': day_names[i], 'date': week_dates[i].strftime('%Y-%m-%d')} for i in range(7)] + + context = { + 'outfits': outfits, + 'folders': folders, + 'current_folder': current_folder, + 'title': 'Outfits', + 'week_data': week_data, + } + return render(request, 'core/outfit_list.html', context) + +def new_fit(request): + date_str = request.GET.get('date') + if request.method == 'POST': + form = OutfitForm(request.POST) + if form.is_valid(): + outfit = form.save() + if date_str: + try: + date_obj = datetime.datetime.strptime(date_str, '%Y-%m-%d').date() + CalendarAssignment.objects.update_or_create( + date=date_obj, + defaults={'outfit': outfit} + ) + return redirect('home') + except ValueError: + pass + return redirect('outfit_list') + else: + form = OutfitForm() + + wardrobe_items = WardrobeItem.objects.all() + accessories = Accessory.objects.all() + + return render(request, 'core/new_fit.html', { + 'form': form, + 'title': f'New Fit for {date_str}' if date_str else 'New Fit', + 'wardrobe_items': wardrobe_items, + 'accessories': accessories, + 'date_str': date_str + }) + +def get_subcategories(request): + parent_id = request.GET.get('parent_id') + if parent_id: + subcategories = Category.objects.filter(parent_id=parent_id).values('id', 'name') + return JsonResponse(list(subcategories), safe=False) + return JsonResponse([], safe=False) + +def wardrobe_item_detail(request, pk): + item = get_object_or_404(WardrobeItem, pk=pk) + return render(request, 'core/item_detail.html', {'item': item, 'type': 'wardrobe'}) + +def accessory_item_detail(request, pk): + item = get_object_or_404(Accessory, pk=pk) + return render(request, 'core/item_detail.html', {'item': item, 'type': 'accessory'}) + +def cleanup_category(category): + if category and not category.is_preset: + if not category.wardrobe_items.exists() and not category.accessories.exists() and not category.children.exists(): + parent = category.parent + category.delete() + if parent: + cleanup_category(parent) + +def cleanup_folder(folder): + if folder and not folder.is_preset: + if not folder.outfits.exists() and not folder.children.exists(): + parent = folder.parent + folder.delete() + if parent: + cleanup_folder(parent) + +@require_POST +def delete_wardrobe_item(request, pk): + item = get_object_or_404(WardrobeItem, pk=pk) + category = item.category + item.delete() + cleanup_category(category) + return redirect('wardrobe_list') + +@require_POST +def delete_accessory_item(request, pk): + item = get_object_or_404(Accessory, pk=pk) + category = item.category + item.delete() + cleanup_category(category) + return redirect('accessory_list') + +@require_POST +def delete_outfit(request, pk): + outfit = get_object_or_404(Outfit, pk=pk) + folder = outfit.folder + outfit.delete() + cleanup_folder(folder) + return redirect('outfit_list') \ No newline at end of file diff --git a/media/accessories/images.jpg b/media/accessories/images.jpg new file mode 100644 index 0000000..6769531 Binary files /dev/null and b/media/accessories/images.jpg differ diff --git a/media/wardrobe/6298b026a65cf80bcf9dce061e9b79c9.png b/media/wardrobe/6298b026a65cf80bcf9dce061e9b79c9.png new file mode 100644 index 0000000..8bcbf30 Binary files /dev/null and b/media/wardrobe/6298b026a65cf80bcf9dce061e9b79c9.png differ diff --git a/media/wardrobe/7-70021_jeans-jeans-pants-for-boys-hd-png-download.jpg b/media/wardrobe/7-70021_jeans-jeans-pants-for-boys-hd-png-download.jpg new file mode 100644 index 0000000..d626f7b Binary files /dev/null and b/media/wardrobe/7-70021_jeans-jeans-pants-for-boys-hd-png-download.jpg differ diff --git a/media/wardrobe/7-70021_jeans-jeans-pants-for-boys-hd-png-download_yXCw3Vn.jpg b/media/wardrobe/7-70021_jeans-jeans-pants-for-boys-hd-png-download_yXCw3Vn.jpg new file mode 100644 index 0000000..d626f7b Binary files /dev/null and b/media/wardrobe/7-70021_jeans-jeans-pants-for-boys-hd-png-download_yXCw3Vn.jpg differ diff --git a/media/wardrobe/images.jpg b/media/wardrobe/images.jpg new file mode 100644 index 0000000..6769531 Binary files /dev/null and b/media/wardrobe/images.jpg differ diff --git a/media/wardrobe/still-life-rendering-jackets-display_23-2149745027.avif b/media/wardrobe/still-life-rendering-jackets-display_23-2149745027.avif new file mode 100644 index 0000000..a4fd872 Binary files /dev/null and b/media/wardrobe/still-life-rendering-jackets-display_23-2149745027.avif differ diff --git a/requirements.txt b/requirements.txt index e22994c..b3dae80 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Django==5.2.7 mysqlclient==2.2.7 python-dotenv==1.1.1 +Pillow==10.2.0 \ No newline at end of file