cascading selection and fetch data from database (htmx and django)

I am new to Django here and sorry if this question is lame but i tried searching internet and the answers were complex for me to get it so please bare with me

The main moto of this project is that i should be able to add and distribute items and then view the contents in the database

This is how my model looks like

models.py

from django.db import models
from django.db.models import Sum
from django.db.models.signals import post_migrate
from django.dispatch import receiver

class Main_Category(models.Model):
    CATEGORIES = [
        ('KGiSL goodies', 'KGiSL goodies'),
        ('Golf goodies', 'Golf goodies'),
        ('gift items', 'gift items'),
        ('old logos', 'old logos'),
        ('new townhall tshit stock', 'new townhall tshit stock'),
        ('old t shirt stock', 'old t shirt stock'),
        ('stationaries', 'stationaries'),
    ]
    name = models.CharField(max_length=100, choices=CATEGORIES, default='KGiSL goodies')

    def __str__(self):
        return self.name
    
@receiver(post_migrate)
def create_initial_categories(sender, **kwargs):
    if sender.name == 'clothsreport':  # replace with your app name
        for category_name, _ in Main_Category.CATEGORIES:
            Main_Category.objects.get_or_create(name=category_name)


class Sub_Category(models.Model):
    SUB_CATEGORIES = [
        ('dark blue tee shirt', 'dark blue tee shirt'),
        ('hoodies', 'hoodies'),
        ('back pack', 'back pack'),
        ('back pack (r)', 'back pack (r)'),
        ('pen', 'pen'),
        ('headphones', 'headphones'),
        ('connect tee shirt', 'connect tee shirt'),
        ('connect shirt', 'connect shirt'),
        ('golf bags', 'golf bags'),
        ('bag with kit', 'bag with kit'),
        ('golf cap', 'golf cap'),
        ('towel', 'towel'),
        ('bags with kgisl logo', 'bags with kgisl logo'),
        ('samsonite laptop bags', 'samsonite laptop bags'),
        ('william watch', 'william watch'),
        ('paper clip diary', 'paper clip diary'),
        ('water bottle 750ml', 'water bottle 750ml'),
        ('water bottle 1l', 'water bottle 1l'),
        ('old white tees', 'old white tees'),
        ('old collar tees women black', 'old collar tees women black'), 
        ('nsure backbags', 'nsure backbags'),
        ('duffle bags', 'duffle bags'),
        ('sap practice', 'sap practice'),
        ('intelligent automation', 'intelligent automation'),
        ('quality engineering', 'quality engineering'),
        ('products and solutions', 'products and solutions'),
        ('infrastructure and management', 'infrastructure and management'),
        ('black plain', 'black plain'),
        ('black with logo printed', 'black with logo printed'),
        ('sap maroon', 'sap maroon'),
        ('psg grey color', 'psg grey color'),
        ('qe - azure grey', 'qe - azure grey'),
        ('ims blue color', 'ims blue color'),
        ('spiral diary', 'spiral diary'),
        ('lanyards', 'lanyards'),
        ('ID card holders', 'ID card holders'),
        ('paper bags', 'paper bags'),
    ]

    name = models.CharField(max_length=100, choices=SUB_CATEGORIES)
    main_category = models.ForeignKey(Main_Category, on_delete=models.CASCADE)
    
    def __str__(self):
        return self.name
    

@receiver(post_migrate)
def create_initial_categories(sender, **kwargs):
    if sender.name == 'clothsreport':
        category_map_dict = {
            'KGiSL goodies': [
                'dark blue tee shirt',
                'hoodies',
                'back pack',
                'back pack (r)',
                'pen',
                'headphones',
                'connect tee shirt',
                'connect shirt',
            ],
            'Golf goodies': [
                'golf bags',
                'bag with kit',
                'golf cap',
                'towel',
            ],
            'gift items': [
                'bags with kgisl logo',
                'samsonite laptop bags',
                'william watch',
                'paper clip diary',
                'water bottle 750ml',
                'water bottle 1l',
            ],
            'old logos': [
                'old white tees',
                'old collar tees women black',
                'nsure backbags',
                'duffle bags',
            ],
            'new townhall tshit stock': [
                'sap practice',
                'intelligent automation',
                'quality engineering',
                'products and solutions',
                'infrastructure and management',
            ],
            'old t shirt stock': [
                'black plain',
                'black with logo printed',
                'sap maroon',
                'psg grey color',
                'qe - azure grey',
                'ims blue color',
            ],
            'stationaries': [
                'spiral diary',
                'lanyards',
                'ID card holders',
                'paper bags',
            ],
        }  
        for main_category_name, sub_categories in category_map_dict.items():
            main_category = Main_Category.objects.get(name=main_category_name)
            for sub_category_name in sub_categories:
                Sub_Category.objects.get_or_create(name=sub_category_name, main_category=main_category)
                        
class Size(models.Model):
    SIZE_CHOICES = [
        ('XS', 'Extra Small'),
        ('S', 'Small'),
        ('M', 'Medium'),
        ('L', 'Large'),
        ('XL', 'Extra Large'),
        ('XXL', '2XL'),
        ('XXXL', '3XL'),
    ]

    size = models.CharField(max_length=4, choices=SIZE_CHOICES, unique=True)
    
    def __str__(self):
        return self.size
    
@receiver(post_migrate)
def create_initial_sizes(sender, **kwargs):
    if sender.name == 'clothsreport':
        sizes = ['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL']
        for size in sizes:
            Size.objects.get_or_create(size=size)

class Gender(models.Model):
    GENDER_CHOICES = [
        ('M', 'Male'),
        ('F', 'Female'),
        ('U', 'Unisex'),
    ]

    gender = models.CharField(max_length=1, choices=GENDER_CHOICES, null=True, blank=True, unique=True)

    def __str__(self):
        return self.gender

@receiver(post_migrate)
def create_genders(sender, **kwargs):
    if sender.name == 'clothsreport':
        genders = ['M', 'F', 'U']
        for gender in genders:
            Gender.objects.get_or_create(gender = gender)

class Transaction(models.Model):
    TRANSACTION_TYPES = [
        ('IN', 'Stock In'),
        ('OUT', 'Stock Out'),
    ]

    main_category = models.ForeignKey(Main_Category, on_delete=models.CASCADE, default='KGiSL goodies')
    category = models.ForeignKey(Sub_Category, on_delete=models.CASCADE)
    size = models.ForeignKey(Size, null=True, blank=True, on_delete=models.SET_NULL)
    gender = models.CharField(max_length=1, choices=Gender.GENDER_CHOICES, null=True, blank=True)
    quantity = models.IntegerField()
    transaction_type = models.CharField(max_length=3, choices=TRANSACTION_TYPES)
    date = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.category} - {self.quantity} - {self.get_transaction_type_display()} on {self.date}"

    def save(self, *args, **kwargs):
        if self.transaction_type == 'OUT':
            self.quantity = -abs(self.quantity)
        super().save(*args, **kwargs)

As you can see the for creating the database i made it this way with the main and subcategory

and then for the views.py and forms.py this is how i tried to render it as the user inputs the content the options change accordingly (the fields named SIZE and GENDER) should be rendered out only for the selected list of options so i gave them in my forms.py

FORMS.PY

from django import forms 
from .models import Main_Category, Sub_Category, Size, Gender

acceptable_subcategory_list = [
    "dark blue tee shirt",
    "hoodies",
    "connect tee shirt", 
    "connect shirt",
    "old white tees",
    "old collar tees women black",
    "sap practice",
    "intelligent automation",
    "quality engineering",
    "products and solutions",
    "infrastructure and management",
    "black plain",
    "black with logo printed",
    "sap maroon",
    "psg grey color",
    "qe - azure grey",
    "ims blue color",
]

class FetchDetailsForm(forms.Form):
    main_category = forms.ModelChoiceField(
        queryset=Main_Category.objects.all(),
        widget=forms.Select(attrs={'hx-get': '/clothsreport/subcategories/', 'hx-target': '#id_sub_category'})
    )
    sub_category = forms.ModelChoiceField(
        queryset=Sub_Category.objects.none(),
        widget=forms.Select(attrs={'hx-get': '/clothsreport/sizegender/', 'hx-target': '#id_size'}),
    )
    
    size = forms.ModelChoiceField(queryset=Size.objects.all(), required=False)
    gender = forms.ModelChoiceField(queryset=Gender.objects.all(), required=False)

VIEW.PY

from django.shortcuts import render, HttpResponse
from .forms import FetchDetailsForm
from .models import Sub_Category, Size, Gender, Transaction, Main_Category
from django.db.models import Sum

def index(request):
    if request.method == 'POST':
        form = FetchDetailsForm(request.POST)
        if form.is_valid():
            main_category = form.cleaned_data['main_category']
            sub_category = form.cleaned_data['sub_category']
            size = form.cleaned_data['size']
            gender = form.cleaned_data['gender']
            quantity = request.POST.get('quantity')

            if 'add' in request.POST and quantity:
                transaction = Transaction(
                    main_category=main_category,
                    category=sub_category,
                    size=size,
                    gender=gender,
                    quantity=abs(int(quantity)),
                    transaction_type='IN'
                )
                transaction.save()
                return HttpResponse("Added successfully.")
            
            elif 'distribute' in request.POST and quantity:
                transaction = Transaction(
                    main_category=main_category,
                    category=sub_category,
                    size=size,
                    gender=gender,
                    quantity=-abs(int(quantity)),
                    transaction_type='OUT'
                )
                transaction.save()
                return HttpResponse("Distributed successfully.")
            
            elif 'view' in request.POST:
                transactions = Transaction.objects.filter(main_category=main_category)
                subcategory_totals = transactions.values('category__name').annotate(total_quantity=Sum('quantity'))
                return render(request, 'clothsreport/view.html', {'subcategory_totals': subcategory_totals, 'main_category': main_category})
    else:
        form = FetchDetailsForm()

    return render(request, 'clothsreport/index.html', context={'form': form})

def subcategories(request):
    main_category_id = request.GET.get('main_category')
    sub_categories = Sub_Category.objects.filter(main_category_id=main_category_id)
    return render(request, 'clothsreport/subcategories.html', {'sub_categories': sub_categories})

def sizegender(request):
    subcategory_id = request.GET.get('sub_category')
    subcategory = Sub_Category.objects.get(id=subcategory_id)
    sizes = Size.objects.all()
    genders = Gender.objects.all()
    return render(request, 'clothsreport/sizegender.html', {'sizes': sizes, 'genders': genders})

as you can see that there are that 3 buttons out in there and when ever i try to ADD or DISTRIBUTE or VIEW
i am just getting this

Screenshot from 2024-06-27 10-26-20

this query not valid thing is there any thing i can do

this is my view.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>View Stock</title>
</head>
<body>
    <h2>Stock Details for {{ main_category.name }}</h2>
    <table border="1">
        <thead>
            <tr>
                <th>Sub Category</th>
                <th>Total Quantity</th>
            </tr>
        </thead>
        <tbody>
            {% for item in subcategory_totals %}
            <tr>
                <td>{{ item.category__name }}</td>
                <td>{{ item.total_quantity }}</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
</body>
</html>

i tried doing this but i am getting stuck at that error itself

PLEASE HELP ME :sob::sob::sob::sob:

Welcome @Siddhu2502 !

The root cause of the error message that you are getting on your form is here:

This queryset is used both to identify the elements to be shown in the select list, but also to perform the validation of the selection made when the form is submitted.

You’ll need to manage this field more “manually” than what Django provides automatically.

You’ve got a couple different options to choose from, how you handle this is up to you.

  1. You could change the queryset to .all() instead of none. Then, alter the form on the GET to empty the selection list. (You would then need to create the clean method to ensure that only a valid value was submitted.)
  2. Change the field in the __init__ method so that it .none() on the GET, and filtered by the right value on a POST.

This type of topic has been discussed a few times here previously. See the complete thread at Passing an altered queryset to ModelChoiceField and the threads linked from it as a starting point. (They may not be targeted directly at what you’re needing to do here, but the basic patterns shown are relevent.)

1 Like

Thank you @KenWhitesell for this answer thanks for answering it very fast and clear …

just a small doubt by this “clean” method what does it mean ? a small example if possible ?

It’s all documented at Form and field validation | Django documentation | Django, and particularly the section at Cleaning and validating fields that depend on each other.