2

Working with Django 1.2 I am making a wine review site. A user should only be able to review each wine once, but should be able to go back and re-review a wine without raising an error.

Using the get_or_create method seems the most rational solution but I have been running into various problems implementing it. Searching I found this article which looked promising: Correct way to use get_or_create?

and of course the django documentation on it: http://docs.djangoproject.com/en/1.2/ref/models/querysets/#get-or-create

But didn't seem to answer my question. Here is my code:

Views.py

@login_required
def wine_review_page(request, wine_id):
wine = get_object_or_404(Wine, pk=wine_id)

if request.method == 'POST':
form = WineReviewForm(request.POST)
if form.is_valid():
  review, created = Review.objects.get_or_create(
    user = request.user,
    wine = wine,
    like_dislike = form.cleaned_data['like_dislike'],
    ...
    )
variables = RequestContext(request, {
 'wine': wine
  })   
  review.save()
  return HttpResponseRedirect(
    '/detail/%s/' % wine_id
  )
else:
  form = WineReviewForm()
  variables = RequestContext(request, {
  'form': form,
  'wine': wine
 })
return render_to_response('wine_review_page.html', variables)

Models.py

class Review(models.Model):
  wine = models.ForeignKey(Wine, unique=True)
  user = models.ForeignKey(User, unique=True)
  like_dislike = models.CharField(max_length=7, unique=True)
  ...

If I understand how to use get_or_create correctly, since I am not matching on all the values like_dislike, etc... then django perceives it to be unique. I tried removing the other form parameters, but then they are not submitted with the post request.

Suggestions would be greatly appreciated.

Community
  • 1
  • 1
bmartinek
  • 568
  • 2
  • 6
  • 17
  • Wouldn't you have problems with a unique like_dislike field? and shouldn't that just be a BooleanField that isn't unique? – dting Apr 07 '11 at 02:44

1 Answers1

0

I came across this too when making a CRUD based app. I'm not sure if there's a better way but the way I ended up getting doing was using a exists() to check if an entry ... exists.

You can use get_or_create within the is_valid() scope, however, you need to check if the review exists before displaying your form in order to load instance data into the form in the case that the review already exists.

Your models.py might look like this:

from django.db import models
from django.contrib.auth.models import User

class Wine(models.Model):
    name = models.CharField()

class Review(models.Model):
    wine = models.ForeignKey(Wine)
    user = models.ForeignKey(User)
    like = models.BooleanField(null=True, blank=True) # if null, unrated

Your forms.py might look like this:

from django import forms

class WineReviewForm(forms.ModelForm):
    class Meta:
        model = Review
        fields = ['like',] # excludes the user and wine field from the form

Using get_or_create will let you do this if used like so:

@login_required
def wine_review_page(request, wine_id):
    wine = get_object_or_404(Wine, pk=wine_id)

    review, created = Review.objects.get_or_create(user=request.user, wine=wine)

    if request.method == 'POST':
        form = WineReviewForm(request.POST, instance=review)
        if form.is_valid():
            form.save()   
            return HttpResponseRedirect('/detail/%s/' % wine_id )
    else:
        form = WineReviewForm(instance=review)

    variables = RequestContext(request, {'form': form, 'wine': wine })
    return render_to_response('wine_review_page.html', variables) 

Doing creates a review just by visiting the page and requires that the other information either have a default or are allowed to be blank at the model level.

With exists(), you get two db hits if the review exists, however you don't create an object unless the user submits a valid form:

@login_required
def wine_review_page(request, wine_id):
    wine = get_object_or_404(Wine, pk=wine_id)

    review = None
    if Review.objects.filter(user=request.user, wine=wine).exists():
        review = Review.objects.get(user=request.user, wine=wine)

    if request.method == 'POST':
        form = WineReviewForm(request.POST, instance=review)
        if form.is_valid():
            form.save()   
            return HttpResponseRedirect('/detail/%s/' % wine_id )
    else:
        form = WineReviewForm(instance=review)

    variables = RequestContext(request, {'form': form, 'wine': wine })
    return render_to_response('wine_review_page.html', variables)

I used exists() but I think that this might be better?

try:
    review = Review.objects.get(user=request.user, wine=wine)
except Review.DoesNotExist:
    review = None

Hopefully someone with more experience will chime in.


Edit:

Here is a fairly old post from Daniel Roseman's Blog. I don't know if it is still applicable, but might be relevant to your question.

dting
  • 38,604
  • 10
  • 95
  • 114
  • Thanks for the reply kriegar! I tried the exists code, but a get an error about the instance is equal to review: form = WineReviewForm(instance=review), Thoughts? – bmartinek Apr 07 '11 at 03:47
  • what's the error? That should work, WineReviewForm is a ModelForm for Review? – dting Apr 07 '11 at 04:01
  • Error: __init__() got an unexpected keyword argument 'instance' – bmartinek Apr 07 '11 at 04:50
  • 1
    It looks like you are using a Form not a ModelForm. http://docs.djangoproject.com/en/dev/topics/forms/modelforms/ – dting Apr 07 '11 at 04:57
  • Hmm never have used a ModelForm before. Will have to check it out. – bmartinek Apr 07 '11 at 05:06
  • Per the documentation and code sample I tried: class WineReviewForm(ModelForm): class Meta: model = Review> At which point I got: 'QuerySet' object has no attribute '_meta' – bmartinek Apr 07 '11 at 05:50
  • @bmartinek looks about right. Unless there's a reason i can't use a modelform, I usually do. Hope it works out for you. I'm still hoping some one posts the way they tackle your initial question. I have a feeling there might be a different approach. Can you post your forms.py on your initial question as an edit? – dting Apr 07 '11 at 05:53
  • Thanks for all your efforts, its certainly a bit frustrating. – bmartinek Apr 07 '11 at 05:59
  • @kriegar Thanks! that did the job. Reviews are overwritten or new! – bmartinek Apr 07 '11 at 15:56
  • Question though, How can I use hidden fields in the ModelForm? I tried the `editable=False` for some of the fields, but some of the fields are edited via jQuery Sliders, I was using `widget=forms.HiddenInput()` for those. I didn't see documentation for that. – bmartinek Apr 07 '11 at 16:00
  • you can override field widgets for fields using the `__init__`. I'm pretty sure i've answered a question or two about overriding a field widget on SO so make a search and if you can't find the answer you are looking for post a new question =) – dting Apr 07 '11 at 18:12