0

I'm building a django web app to allow customers to sign up for courses. On the page "training" all courses are listed. The users clicks on a course to get more details and apply for that course. The course model looks like this:

class Course(models.Model):
    course_text = models.CharField(max_length=200)
    slug = models.SlugField()
    leader_text = models.TextField()
    location = models.CharField(max_length=50)
    start_date = models.DateField('start date')
    duration_days = models.CharField (max_length=1)
    cost = models.DecimalField(max_digits=7, decimal_places=2)
    available = models.CharField(max_length=2)
    class Meta: 
        ordering = ['start_date']

I have an application model "Applicant" that looks like this:

class Applicant(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField(max_length=75)
    contact_date = models.DateTimeField(auto_now=True)
    courses = models.ManyToManyField(Course)

When the user click on a course the details of that course are rendered in the view and a ModelForm is also rendered for the application process. The view looks like this:

def view_course(request, slug):
    print Course.objects.all() #Just for testing

    if request.method == "POST":
        form = ApplicantForm(request.POST)
        if form.is_valid():
            model_instance = form.save(commit=False)
            model_instance.save()


            return HttpResponseRedirect('/thanks/')
    else:
        form = ApplicantForm()

    return render(request, 'training/view_course.html', {'course': get_object_or_404(Course, slug=slug), 'form': form})

All of the above works and I can view the users in my django admin as they apply. What I would like to do is gather the specific information of the course they are applying for and tie that to each user. I was hoping that the ManyToMany relationship on the "Course" model would help but if I include it in the rendered form it just gives options to choose any course.

I'm getting somewhere with querysets such as Course.objects.all() but I need to filter that on the current values from "Course" in the view. Is there a current command or similar to get the data that's held in the current session?

I'm using Django 1.7 and MySQL as the database.

Clarifications

I want to display the minimum on the form the user fills in to ease the application process. Therefore only first_name, last_name and email are required. I want this information to be linked to the Course the user is currently viewing so those values are automatically completed and not seen by the user.

As for many applicant instances in the db isn't that how ManyToMany relationships work? In my admin page I want to be able to click on a users and see the courses they're signed up for and also be able to click on a course to see how many users ave applied. My admin page is nowhere near that yet but I wanted to be able to get the current information saved before worrying about that. Here's what admin looks like:

from django.contrib import admin
from training.models import Course, Applicant

class CourseAdmin (admin.ModelAdmin):
    fieldsets = [
        (None,              {'fields': ['course_text', 'location', 'start_date', 'duration_days', 'cost', 'available']}),
        ('Course details',  {'fields': ['leader_text'], 'classes': ['collapse']}),
        ]

    list_display = ['course_text', 'location','start_date', 'was_published_recently']
    list_filter = ['start_date', 'location']
    search_fields = ['course_text']



admin.site.register(Course, CourseAdmin)

class ApplicantAdmin(admin.ModelAdmin):
    fieldsets = [
    (None,              {'fields': ['first_name', 'last_name', 'email',]}),
    ]
    list_display = ['first_name', 'last_name','email',]
    list_filter = ['first_name', 'last_name']
    search_fields = ['courses']

admin.site.register(Applicant, ApplicantAdmin)

A Solution of Sorts

First up thanks to spookylukey. Exactly right that I had two things going on and indeed it could become problematic having the same user creating more than one applicant object. I was trying to get away from the auth/auth idea as I think it leads to a lot of drop-offs. I see now why it's important.

That aside spookylukey's suggestion of using the Model.ManyToManyField.add() method worked. The code for the view.py is now:

def view_course(request, slug):

courseId = Course.objects.get(slug=slug) 

if request.method == "POST":
    form = ApplicantForm(request.POST)
    if form.is_valid():

        applicant = form.save(commit=False)
        applicant.save()
        form.save_m2m()
        #The add method working it's magic
        applicant.courses.add(courseId)

        return HttpResponseRedirect('/thanks/')
else:
    form = ApplicantForm()
#print request.session['course' : get_object_or_404(Course, slug=slug)] 
return render(request, 'training/view_course.html', {'course': get_object_or_404(Course, slug=slug), 'form': form})

the additional problem of viewing the relationship in the admin interface was solved by using InlineModelAdmin Objects. So the admin.py looks like this:

class SignupsInline(admin.TabularInline):
    model = Applicant.courses.through



class CourseAdmin (admin.ModelAdmin):
    #other styling goes here...

    inlines = [
        SignupsInline,
    ]

admin.site.register(Course, CourseAdmin)

class ApplicantAdmin(admin.ModelAdmin):
    #other styling goes here...

    inlines = [
        SignupsInline,
    ]
admin.site.register(Applicant, ApplicantAdmin)

To get more information about the associated applicant object (in the course admin section) or the course object (in the applicant admin section) I added the following function to the Applicant model (and a corresponding one to the Course model):

    def __unicode__(self):
       return u'%s %s %s' % (self.first_name, self.last_name, self.email)

Not great but serves its purpose for the time being.

Oliver Burdekin
  • 1,098
  • 2
  • 17
  • 36
  • I'm not quite clear what you want to do here. Is there anything you actually want to display on the form, or is it just that you want to set the course they are applying for along with the applicant details? – Daniel Roseman Dec 22 '14 at 10:24
  • And since your form is for the applicant model, how are you managing the same user signing up for multiple courses? It seems that they'd have to enter all their details for each course, and you'll have multiple Applicant objects for them in your db. – Daniel Roseman Dec 22 '14 at 10:25
  • Regarding your edit, no that is absolutely not the point of many-to-many relationships. You should have *one* instance of each applicant, and *one* instance of each course, but each applicant can have many courses and each course can have many applicants. Your models are correctly configured for that but your view and form are not. – Daniel Roseman Dec 22 '14 at 10:47
  • Thanks @DanielRoseman. I'll look into saving the instance of the applicant in the session and expanding it into a user login / signup form. – Oliver Burdekin Dec 22 '14 at 12:59

3 Answers3

3

It sounds like you have two things going on:

  • creating Applicant records, which I presume you would only want to do once per user - if a user wants to apply for more than one course, you don't want to prompt for all their details again.

  • associating an Applicant with a Course.

So I think you need a more complex workflow - one that stores the created Applicant data or id in the session, and re-uses as needed, or prompts for more data.

Whether or not you do this, the way to fix your view is to stop thinking about trying to use a ModelForm for everything. The view_course view is not a view that allows a person to edit everything about their Applicant record - it doesn't allow them to set/unset their entire list of courses. So using a ModelForm to set courses is not appropriate. Instead, you need to use the Applicant API that has been created for you by the ORM.

In your view code, I presume you have something like:

course = Course.objects.get(slug=slug)

You will need to add this course to the applicant:

applicant = form.save()
applicant.courses.add(course)

The applicant object will either be retrieved from the return value of form.save(), or from the session as I mentioned above.

spookylukey
  • 6,380
  • 1
  • 31
  • 34
  • Using `applicant.courses.add(course)` throws the error "Global name applicant is not defined". I'm confused as to how I would add the applicant information to the database if I'm not using a ModelForm. And thanks for the more holistic concept based answer. – Oliver Burdekin Dec 22 '14 at 12:03
  • I tried to clarify my code - as I said, `applicant` should be the return value of `form.save()`. It's a much clearer name than `model_instance`. – spookylukey Dec 22 '14 at 12:22
  • Apologies. This runs, but how would I view the associated courses in admin as 'course' is an unknown field in admin.py and adding 'courses' to the admin view shows a list of all available courses rather than just the one associated with the user. Do I need to add a 'course' field to my applicant model? Ideally I'd also like to be able to view the applicants associated with each course in admin. – Oliver Burdekin Dec 22 '14 at 12:47
  • @OliverBurdekin - for the admin, there is not just one course associated with the user, there are many. You specified that explicitly: `courses = models.ManyToManyField(Course)`. Also you said above that you want to see all the courses associated with a user in the admin. I'm confused! – spookylukey Dec 22 '14 at 22:33
  • I hope the edits make it clearer and thanks for the help – Oliver Burdekin Dec 22 '14 at 22:47
1

I would add the M2M field to the Course model, where it will be linked to Applicants. And within your view, get the selected course, and add applicant after a POST.

course = Course.objects.get(slug=slug) ... course.applicants.add(model_instance)

Blackeagle52
  • 1,956
  • 17
  • 16
0

When passing commit=False to a ModelForm.save() for a model that has a m2m relationship, you need to explicitely save the m2ms too, cf https://docs.djangoproject.com/en/1.7/topics/forms/modelforms/#the-save-method

IOW, your code should be:

if request.method == "POST":
    form = ApplicantForm(request.POST)
    if form.is_valid():
        model_instance = form.save(commit=False)
        model_instance.save()
        form.save_m2m()

Note that if you have to reason to pass commit=False, just calling form.save() will save the m2m.

bruno desthuilliers
  • 75,974
  • 6
  • 88
  • 118