0

I have a Formset, where one of the fields is a multi-select checkbox, with the options available being determined by a foreign key relationship (to the business model; the form takes business as an arguement). This works, however upon submission of the Formset, nothing is saved to the database despite a POST request and redirect taking place (I would expect it to create an instance or instances of the Instructors model).

My models.py (including only relevant models):

    class Instructors(models.Model):
        uid = models.CharField(verbose_name="Unique ID", max_length=255, primary_key=True)
        business = models.ForeignKey(Business, blank=False, on_delete=models.CASCADE)
        first_name = models.CharField(verbose_name="First Name", max_length=255, blank=False)
        surname = models.CharField(verbose_name="Surname", max_length=255, blank=False)
        activities = models.ManyToManyField(Activity, blank=False)
    
        def __str__(self):
            return str(self.uid)
    
    class Business(models.Model): # if is_business on profile model = true, then get access to create Business Profile
        business_name = models.CharField(verbose_name='Business Name', max_length=255)
        business_description = models.TextField(verbose_name='Business Description', max_length=500)
        slug = models.SlugField(verbose_name='Slug', max_length=250, null=True, blank=True, unique=True)
        business_contact_number = models.CharField(verbose_name='Business Contact Number', max_length=32)
        business_email = models.EmailField(verbose_name='Business Email')
        business_profile_image = models.ImageField(verbose_name="Profile Picture", null=True, blank=True, upload_to='business_images/')
        creation_date = models.DateTimeField(auto_now_add=True)
        address = models.CharField(verbose_name="Street Address", max_length=100, null=True, blank=True)
        town = models.CharField(verbose_name="Town/City", max_length=100, null=True, blank=True)
        county = models.CharField(verbose_name="County", max_length=100, null=True, blank=True)
        post_code = models.CharField(verbose_name="Post Code", max_length=8, null=True, blank=True)
        country = models.CharField(verbose_name="Country", max_length=100, null=True, blank=True)
        longitude = models.CharField(verbose_name="Longitude", max_length=50, null=True, blank=True)
        latitude = models.CharField(verbose_name="Latitude", max_length=50, null=True, blank=True)
        activities = models.ManyToManyField(Activity, blank=True)
    
    
        def __str__(self):
            return str(self.business_name)
    class Activity(models.Model):
        """ List of all types of activity which are on the platform """
        title = models.CharField(max_length=255, primary_key=True, unique=True)
        tags = models.ManyToManyField(Tag, null=True, blank=True) # there is a problem with this
        slug = models.SlugField(verbose_name='Slug', max_length=250, null=True, blank=True, unique=True)
        activity_image = models.ImageField(verbose_name="Activity Image", blank=True, null=True, upload_to="activity_images/")
        date_added = models.DateTimeField(auto_now_add=True)
        def __str__(self):
            return self.title

My forms.py:

    class AddInstructorForm(forms.ModelForm):
        """ A form for adding instructors. """
    
        activities = forms.ModelMultipleChoiceField(queryset=None, widget=forms.CheckboxSelectMultiple(attrs={'name': 'activities'}))
    
        def __init__(self, business, *args, **kwargs):
            super(AddInstructorForm, self).__init__(*args, **kwargs)
            business_activities = Business.activities.through.objects.filter(business_id=business).values('activity')
            self.fields['activities'].queryset = business_activities.values_list('activity', flat=True)
    
        class Meta:
            model = Instructors
            fields = ['first_name', 'surname', 'activities']
    
            widgets = {
                'first_name': forms.TextInput(attrs={'class': 'form-control'}),
                'surname': forms.TextInput(attrs={'class': 'form-control'})
            }
    
    AddInstructorFormSet = formset_factory(AddInstructorForm, extra=0)

My views.py:

    class AddInstructorView(View, RandomStringMixin):
        template_name = 'businesshub/add_instructor.html'
    
        def get(self, request, *args, **kwargs):
            business = self.request.user.profile.business.id
            formset = AddInstructorFormSet(form_kwargs={'business': business})
            context = {
                'formset': formset,
                'slug': self.kwargs['slug'],
            }
            return render(request, self.template_name, context)
    
        def post(self, request, *args, **kwargs):
            business = self.request.user.profile.business
            formset = AddInstructorFormSet(request.POST, form_kwargs={'business': business})
            if formset.is_valid():
                instances = []
                for form in formset:
                    form.uid = str(business.id) + '_' + self.get_string(6, 6)
                    instructor = form.save(commit=False)
                    instructor.business = business
                    instances.append(instructor)
                Instructors.objects.bulk_create(instances)
                    
                
                user = self.request.user
                slug = user.profile.business.slug
                return redirect(reverse('businesshub:business_hub', kwargs={'slug': slug}))
            else:
                print("FORM IS NOT VALID")
                context = {
                    'formset': formset,
                    'slug': self.kwargs['slug'],
                }
                return render(request, self.template_name, context)

My html template:

    {% extends "base.html" %}
    
    {% block content %}
        <h1>Add Instructor</h1>
    
        <div class="container">
            <form method="POST" element="multipart/form-data">
                {% csrf_token %}
                <div id="formset-container">
                    {{ formset.management_form }}
                    {% for form in formset %}
                    <div class="formset-row">
                        <div class="mb-3">
                            <label for="title">First Name:</label>
                            {{ form.first_name }}
                        </div>
                        <div class="mb-3">
                            <label for="title">Surname:</label>
                            {{ form.surname }}
                        </div>
                        <div class="mb-3">
                            <label for="title">Select all activities this person can instruct on:</label>
                            {{ form.activities }}
                        </div>
                    </div>
                    {% endfor %}
                </div>
                <button id="add-form" type="button" class="btn btn-secondary mb-3">Add Instructor</button>
                <div class='mb-5'>
                    <a class='btn btn-secondary' href='{% url "businesshub:business_hub" slug=slug %}' role="button">Finish later</a>
                    <button type='submit' class='btn btn-primary'>Next</button>
                </div>
            </form>
        </div>
    
        <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
        <script>
            $(document).ready(function() {
                // Counter to track the number of form instances
                var formCount = {{ formset.total_form_count }};
        
                // Function to add a new form instance
                function addForm() {
                    var formTemplate = $('#form-template').html();
                    var formsetContainer = $('#formset-container');
                    var newForm = formTemplate.replace(/__prefix__/g, formCount);
                    formsetContainer.append(newForm);
                    formCount++;
                }
        
                // Bind click event to the "Add Instructor" button
                $('#add-form').click(function() {
                    addForm();
                });
        
                // Add initial form instances
                {% for form in formset %}
                    addForm();
                {% endfor %}
            });
        </script>
        
    
        <script type="text/template" id="form-template">
            <div class="formset-row">
                <div class="mb-3">
                    <label for="title">First Name:</label>
                    {{ formset.empty_form.first_name }}
                </div>
                <div class="mb-3">
                    <label for="title">Surname:</label>
                    {{ formset.empty_form.surname }}
                </div>
                <div class="mb-3">
                    <label for="title">Select all activities this person can instruct on:</label>
                    {{ formset.empty_form.activities }}
                </div>
            </div>
        </script>
    {% endblock %}
nlewis99
  • 71
  • 7

1 Answers1

1

The code you've shared looks quite good at first glance. The mechanism of taking the business from the logged-in user, filtering the activities based on the business, and creating multiple Instructors from the formset looks appropriate.

However, let's address some common points which could cause the data not being saved:

  1. HTML Form Issue: Ensure your form's attribute enctype is correctly spelled. It should be enctype="multipart/form-data" instead of element="multipart/form-data". This is crucial especially if you are dealing with file uploads.

    Change:

    <form method="POST" element="multipart/form-data">
    

    to:

    <form method="POST" enctype="multipart/form-data">
    
  2. Model Field: You are trying to set uid on the form, but you should be setting it on the model instance. Change this:

    form.uid = str(business.id) + '_' + self.get_string(6, 6)
    

    to:

    instructor.uid = str(business.id) + '_' + self.get_string(6, 6)
    
  3. ManyToMany Relationships: After you save the Instructor model, you also need to save the ManyToMany relationship. Since you're using commit=False to prevent saving the Instructor model immediately, you'll need to save the many-to-many data after setting the business:

    for form in formset:
        instructor = form.save(commit=False)
        instructor.uid = str(business.id) + '_' + self.get_string(6, 6)
        instructor.business = business
        instructor.save()  # Save the instructor before m2m
        form.save_m2m()  # Save the many-to-many data
    

    Note: This will result in separate insert operations and won't be as efficient as bulk_create. If you really want to use bulk_create, you'd have to handle the many-to-many relations differently (perhaps in another loop after all instructors are created).

  4. Validation Errors: You've added a print statement when the form is not valid. It might be helpful to print out the specific form errors to get a better idea of what's going wrong:

    else:
        print(formset.errors)
    
  5. ForeignKey Lookup: In your form's __init__ method, you can simplify the queryset for activities:

    self.fields['activities'].queryset = Activity.objects.filter(business=business)
    
  6. Transaction: Consider wrapping the saving part in a transaction, so if something fails, all changes get rolled back. You'd use from django.db import transaction and then wrap the saving logic inside with transaction.atomic():.

I hope one of these suggestions solves your issue! If none of these work, you might want to debug with more traditional methods like using print statements or a debugger to step through your code.

Mihail Andreev
  • 979
  • 4
  • 6
  • Thanks - had to tidy up a few other pieces but this solved my issue and I can now create a model instance. Only problem is it's only working for the first form I complete, think this may be to do with my html / javascript. – nlewis99 Aug 14 '23 at 21:06
  • probably this one https://stackoverflow.com/a/56628865/2796659, make sure you set right inputs attributes – Mihail Andreev Aug 15 '23 at 07:00