Multiple creations of a save instance

Hello, I am building a hire system for my workplace and I have hit a bit of a snag. The system should take a users information and save it, and then items are added to the user, so for example Joe Bloggs wants to hire a microphone for talking sake, then I have a model called new_hire that accommodates the information for each hire.

Below is my model

    STATUS_CHOICES = (
        ('current', 'Current'),
        ('returned', 'Returned'),
    )
    

    user_name = models.CharField(max_length=100, verbose_name='user name', null=True)
    items = models.ManyToManyField(Hire_Items, related_name='hires', verbose_name='items')
    date = models.DateField(default=datetime.datetime.today, verbose_name='date', null=True)
    returned = models.BooleanField(default=False, verbose_name='returned',null=True)
    charge_code = models.CharField(max_length=100, verbose_name='charge code', null=True)
    authority = models.CharField(max_length=100, verbose_name='authority', null=True)
    contact_address = models.CharField(max_length=100, verbose_name='contact address', null=True)
    telephone_no = models.CharField(max_length=100, verbose_name='telephone number', null=True)
    location = models.CharField(max_length=100, verbose_name='location', null=True)
    date_out = models.DateField(default=datetime.datetime.today, verbose_name='date out',null=True)
    date_in = models.DateField(default=datetime.datetime.today, verbose_name='date in', null=True)
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='current', verbose_name='hire status')
    additional_items = models.TextField(verbose_name='Additional Items', blank=True, null=True, 
                                     help_text='List any additional items not in the system')
    def __str__(self):
        return f"{self.user_name} - Hire #{self.id}

This is where things get a bit complicated. So I have two apps in my project, the original hire_app which does all the initial signup and gathers information about a hire, and a hire_total app, which is where the data from the hire is summed up and stored.

This is the view that I use for the hire form in my hire_app

def hire_form(request):
    if request.method == "POST":
        form = UserForm(request.POST)
        if form.is_valid():
            try:
                with transaction.atomic():
                    # Save the hire form as a NewHire instance
                    new_hire_instance = form.save(commit=False)
                    new_hire_instance.save()
                    
                    # Update item availability if needed
                    # new_hire_instance.item.available = False
                    # new_hire_instance.item.save()
                    
                    messages.success(request, "Hire successfully recorded!")
                    return redirect('table')  # Redirect to the list view
            except Exception as e:
                messages.error(request, f"Error saving hire: {str(e)}")
    else:
        form = UserForm()
    return render(request, "hire_form.html", {"form": form})

and this is the view from my hire_total app that saves the hire to the database

def save_hire(request):
    try:
        user_name = request.POST.get('user_name')
        if not user_name:
            return JsonResponse({
                'success': False,
                'message': 'No customer name provided.'
            })
        print("Save hire called")
        total = Hire_Total(request)
        hire_items = total.get_all_items()
        
        # Debug print
        
        
        if not hire_items:
            return JsonResponse({
                'success': False,
                'message': 'No items in the hire to save.'
            })
        print(f"Creating hire for user {user_name} with {hire_items.count()} items")
        # Create new hire instance
        new_hire_instance = new_hire.objects.create(
            user_name=user_name,
            status='current',
            date_out=datetime.datetime.today(),
        )

        # Debug print
        print(f"Created new hire with ID: {new_hire_instance.id}")

        # Add items to the many-to-many relationship
        for item in hire_items:
            new_hire_instance.items.add(item)
            print(f"Added item {item.id} to hire {new_hire_instance.id}")

        # Verify items were added
        print(f"Items in hire after adding: {list(new_hire_instance.items.all())}")

        # Clear the hire total
        total.clear()

        return JsonResponse({
            'success': True,
            'message': 'Hire saved successfully',
            #'hire_id': new_hire_instance.id
        })
    except Exception as e:
            print(f"Error in save_hire: {str(e)}")
            return JsonResponse({
            'success': False,
            'message': f'Error saving hire: {str(e)}'
        })

As you can see I have been trying to solve the issue through printing where the user name is associated with the hires to see if there is anything obvious with my code, but I’m stumped at this point. The system almost works the way I need it to, but whenever I save a hire with items, it saves one instance with all the hire information but no items, and one with just a name and the items that have been hired. I suspect I have made a simple error and any help would be greatly appreciated.

Welcome @thejunglist !

Which model is this? Is this your “user” model or your “new_hire” model?

When and where is this save_hire request being called?

These should not be two separate apps. That’s an artificial segmentation that provides no benefits logically or structurally - and potentially causing confusion regarding the operation of your system.

Side note: Unless you’re going to need to do something with new_hire_instance, there’s no need to break this down into two separate steps:

All that’s required here is the form.save()

Hello Ken!

Thanks for getting back to me so quickly, I did think that it was unnecessary to have two separate apps but I’m still learning and this project is a combination of a tutorial i found and my own trial and error. I didn’t realize that had cut off the declaration.

The full model is below:

class new_hire(models.Model):
    STATUS_CHOICES = (
        ('current', 'Current'),
        ('returned', 'Returned'),
    )
    

    user_name = models.CharField(max_length=100, verbose_name='user name', null=True)
    items = models.ManyToManyField(Hire_Items, related_name='hires', verbose_name='items')
    date = models.DateField(default=datetime.datetime.today, verbose_name='date', null=True)
    returned = models.BooleanField(default=False, verbose_name='returned',null=True)
    charge_code = models.CharField(max_length=100, verbose_name='charge code', null=True)
    authority = models.CharField(max_length=100, verbose_name='authority', null=True)
    contact_address = models.CharField(max_length=100, verbose_name='contact address', null=True)
    telephone_no = models.CharField(max_length=100, verbose_name='telephone number', null=True)
    location = models.CharField(max_length=100, verbose_name='location', null=True)
    date_out = models.DateField(default=datetime.datetime.today, verbose_name='date out',null=True)
    date_in = models.DateField(default=datetime.datetime.today, verbose_name='date in', null=True)
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='current', verbose_name='hire status')
    additional_items = models.TextField(verbose_name='Additional Items', blank=True, null=True, 
                                     help_text='List any additional items not in the system')
    def __str__(self):
        return f"{self.user_name} - Hire #{self.id}"


class ExtraHireItem(models.Model):
    hire = models.ForeignKey(new_hire, on_delete=models.CASCADE, related_name='extra_items')
    description = models.CharField(max_length=200)
    quantity = models.IntegerField(default=1)
    
    def __str__(self):
        return f"{self.description} (Qty: {self.quantity})"  

The save hire is being called through a javascript function:

         // Single event handler for save button
         $('#save-hire').off('click').on('click', function (e) {
    e.preventDefault();
    $(this).prop('disabled', true);

    var userName = "";
    var hireId = "";
    {% if current_hires %}
        userName = "{{ current_hires.0.user_name|escapejs }}";
        hireId = "{{ current_hires.0.id }}";
    {% endif %}
    
    console.log("User name to save:", userName);
    console.log("Hire ID:", hireId);

    if (!userName) {
        alert("No user name found. Please ensure customer details are entered.");
        $(this).prop('disabled', false);
        return;
    }

    $.ajax({
        type: 'POST',
        url: "{% url 'save_hire' %}",
        data: {
            csrfmiddlewaretoken: '{{ csrf_token }}',
            user_name: userName,
            hire_id: hireId,  // Pass the existing hire ID if it exists
        },
        success: function (response) {
            if (response.success) {
                alert("Hire saved successfully!");
                window.location.href = "{% url 'all_hires' %}";
            } else {
                alert(response.message || "Error saving hire");
                $('#save-hire').prop('disabled', false);
            }
        },
        error: function (error) {
            console.log("Error saving hire:", error);
            alert("Error saving hire. Please try again.");
            $('#save-hire').prop('disabled', false);
        }
    });});
'''
As you can see from my javascript call, the save_hire request is called with the information input into the form. Now that I am typing this out, do you think this is where the error may be coming from? It seems like this call is saving all the information for the user, but the actual items are being saved separately with just a name. 

And yes, the two apps have caused me a lot of confusion throughout this whole project. I will definitely be refactoring down to one app!

I’m sorry, I’m still not really following what is going on here.

What I think I understand so far -

  • You have a view named hire_form
    • it renders a template named hire_form.html
    • it handles the submission of a form. (what form?)
    • How is this view invoked by the page? Is the form submitted or is the data submitted via AJAX?
  • You have another view named save_hire
    • This view is called by an AJAX submission.
    • Where does this data come from?
    • Is there another form involved?

Am I understanding the fundamentals here so far?

Why do you have what appears to be two separate submission processes?

  • Is all this data supposed to be submitted from the same page at the same time?

Hello Ken,

Let me give you a speedy tour of the program to see how it (sort of) works.

After the user has logged in/signed up, they enter information for the person hiring the equipment on this template:

{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}

<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card mt-4">
                <div class="card-header text-center">
                    <h2>Who is this hire for?</h2>
                </div>
                <div class="card-body">
                    {% crispy form %}
                </div>
            </div>
        </div>
    </div>
</div>

{% endblock %}

The user form is below:

class UserForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_method = 'post'
        self.helper.form_class = 'form-horizontal'
        self.helper.label_class = 'col-lg-2'
        self.helper.field_class = 'col-lg-8'
        self.helper.layout = Layout(
            Field('user_name'),
            Field('authority'),
            Field('contact_address'),
            Field('telephone_no'),
            Field('location'),
            Field('charge_code'),
            Field('date_out'),
            Field('date_in'),
            Div(
                Submit('submit', 'Save', css_class='btn btn-primary'),
                css_class='text-center'
            )
        )

    class Meta:
        model = new_hire
        fields = ['user_name', 'authority', 'contact_address', 
                 'telephone_no', 'location', 'charge_code', 
                 'date_out', 'date_in']
        

Once this form has been filled out the previously mentioned view is called:

def hire_form(request):
    if request.method == "POST":
        form = UserForm(request.POST)
        if form.is_valid():
            try:
                with transaction.atomic():
                    # Save the hire form as a NewHire instance
                    new_hire_instance = form.save(commit=False)
                    new_hire_instance.save()
                    
                    # Update item availability if needed
                    # new_hire_instance.item.available = False
                    # new_hire_instance.item.save()
                    
                    messages.success(request, "Hire successfully recorded!")
                    return redirect('table')  # Redirect to the list view
            except Exception as e:
                messages.error(request, f"Error saving hire: {str(e)}")
    else:
        form = UserForm()
    return render(request, "hire_form.html", {"form": form})

This then saves the users information to the hire. We then select the items for hire through their id and associate them with the current hire in this view:

def add_items_to_current_hires(request, hire_id):
     hire = Hire_Total.objects.get(hire_id=hire_id)
     if request.method == 'POST':
          totalform =  Hire_Total_Form(request.POST, instance=hire)
          if totalform.is_valid():
               totalform.save()
               return redirect('current_hires.html')

Note that this view is in the hire_total app.

Then the save view is called, which is the same as in my previous response. As I say what should happen is that the person hiring the items and the items them selves should be displayed on a summary page shown below:

{% extends 'base.html' %}
{% load render_table from django_tables2 %}
{% load django_bootstrap5 %}
{% block content %}

<div class="container">
    <!-- Current Hires Table -->
    <h2 class="mb-4">Current Hires</h2>
    {% if current_hires %}
        <table class="table table-striped">
            <thead>
                <tr>
                    <th>Hire #</th>
                    <th>Customer Name</th>
                    <th>Date Out</th>
                    <th>Authority</th>
                    <th>Location</th>
                    <th>View Details</th>
                </tr>
            </thead>
            <tbody>
                {% for hire in current_hires %}
                    <tr>
                        <td>{{ hire.id }}</td>
                        <td>{{ hire.user_name }}</td>
                        <td>{{ hire.date_out }}</td>
                        <td>{{ hire.authority }}</td>
                        <td>{{ hire.location }}</td>
                        <td>
                            <a href="{% url 'hire_detail' hire.id %}" class="btn btn-info btn-sm">View Details</a>
                        </td>
                            
                    </tr>
                {% endfor %}
            </tbody>
        </table>
    {% else %}
        <p>No current hires found.</p>
    {% endif %}

    <!-- Returned Hires Table -->
    <h2 class="mt-5 mb-4">Returned Hires</h2>
    {% if returned_hires %}
        <table class="table table-striped">
            <thead>
                <tr>
                    <th>Hire #</th>
                    <th>Customer Name</th>
                    <th>Date Out</th>
                    <th>Date Returned</th>
                    <th>Authority</th>
                    <th>Location</th>
                    <th>View Details</th>
                </tr>
            </thead>
            <tbody>
                {% for hire in returned_hires %}
                    <tr>
                        <td>{{ hire.id }}</td>
                        <td>{{ hire.user_name }}</td>
                        <td>{{ hire.date_out }}</td>
                        <td>{{ hire.date_in }}</td>
                        <td>{{ hire.authority }}</td>
                        <td>{{ hire.location }}</td>
                        <td>
                            <a href="{% url 'hire_detail' hire.id %}" class="btn btn-info btn-sm">View Details</a>
                        </td>
                    </tr>
                {% endfor %}
            </tbody>
        </table>
    {% else %}
        <p>No returned hires found.</p>
    {% endif %}
</div>

{% endblock %}

This is the view to view all the hires

def all_hires(request):
    # Separate the hires into current and returned
    current_hires = new_hire.objects.filter(status='current', returned=False).order_by('-date_out')
    returned_hires = new_hire.objects.filter(status='returned', returned=True).order_by('-date_in')
    
    return render(request, "all_hires.html", {
        'current_hires': current_hires,
        'returned_hires': returned_hires
    })

to get the selected items, I have used a separate Hire_Total.py file because that is what the tutorial i followed suggested. The code for this is below:

from  hire_app.models import Hire_Items, new_hire
from django.db import models


class Hire_Total():
    def __init__(self, request):
        self.session = request.session
        # Get the current session key if it exists
        hire_total = self.session.get('hire_total', {})
        # If the user is new, no session key! create one
        if 'hire_total' not in request.session:
            self.session['hire_total'] = {}
        # Make sure hire is available on all pages of site
        self.hire_total = hire_total

    def add(self, hire_items):
        hire_id = str(hire_items.id)  # Convert to string for session compatibility
        if hire_id not in self.hire_total:
            self.hire_total[hire_id] = {
                'model': str(hire_items.model),
                'id': hire_items.id  # Store the ID for later retrieval
            }
        # Save hire total in session - THIS WAS THE MAIN ISSUE
        self.session['hire_total'] = self.hire_total  # Changed from 'session_key' to 'hire_total'
        self.session.modified = True

    def get_all_items(self):
        # Gets all items in the current hire session for saving
        hire_ids = self.hire_total.keys()
        # Convert string IDs back to integers for querying
        hire_ids = [int(id) for id in hire_ids]
        return Hire_Items.objects.filter(id__in=hire_ids)

    def get_items(self):
        # Get ids from hire
        hire_ids = list(self.hire_total.keys())
        # Convert string IDs back to integers for querying
        hire_ids = [int(id) for id in hire_ids]
        # Use ids to lookup items
        items = Hire_Items.objects.filter(id__in=hire_ids)
        return items
    

This is used to update the total items hired to the user.

I hope this has cleared it up a bit, but I’m seriously considering just starting it again as it has become more complex than i think it needs to be. Any help would be greatly appreciated.