Class-based views for M2M relationship with intermediate model - django

I have a M2M relationship between two Models which uses an intermediate model. For the sake of discussion, let's use the example from the manual:
class Person(models.Model):
name = models.CharField(max_length=128)
def __unicode__(self):
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
def __unicode__(self):
return self.name
class Membership(models.Model):
person = models.ForeignKey(Person)
group = models.ForeignKey(Group)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
I'd like to make use of Django's Class-based views, to avoid writing CRUD-handling views. However, if I try to use the default CreateView, it doesn't work:
class GroupCreate(CreateView):
model=Group
This renders a form with all of the fields on the Group object, and gives a multi-select box for the members field, which would be correct for a simple M2M relationship. However, there is no way to specify the date_joined or invite_reason, and submitting the form gives the following AttributeError:
"Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead."
Is there a neat way to override part of the generic CreateView, or compose my own custom view to do this with mixins? It feels like this should be part of the framework, as the Admin interface atomatically handles M2M relationships with intermediates using inlines.

You must extend CreateView:
from django.views.generic import CreateView
class GroupCreate(CreateView):
model=Group
and override the form_valid():
from django.views.generic.edit import ModelFormMixin
from django.views.generic import CreateView
class GroupCreate(CreateView):
model = Group
def form_valid(self, form):
self.object = form.save(commit=False)
for person in form.cleaned_data['members']:
membership = Membership()
membership.group = self.object
membership.person = person
membership.save()
return super(ModelFormMixin, self).form_valid(form)
As the documentation says, you must create new memberships for each relation between group and person.
I saw the form_valid override here:
Using class-based UpdateView on a m-t-m with an intermediary model

class GroupCreate(CreateView):
model = Group
def form_valid(self, form):
self.object = form.save(commit=False)
### delete current mappings
Membership.objects.filter(group=self.object).delete()
### find or create (find if using soft delete)
for member in form.cleaned_data['members']:
x, created = Membership.objects.get_or_create(group=self.object, person=member)
x.group = self.object
x.person = member
#x.alive = True # if using soft delete
x.save()
return super(ModelFormMixin, self).form_valid(form)

'For reference, I didn't end up using a class-based view, instead I did something like this:
def group_create(request):
group_form = GroupForm(request.POST or None)
if request.POST and group_form.is_valid():
group = group_form.save(commit=False)
membership_formset = MembershipFormSet(request.POST, instance=group)
if membership_formset.is_valid():
group.save()
membership_formset.save()
return redirect('success_page.html')
else:
# Instantiate formset with POST data if this was a POST with an invalid from,
# or with no bound data (use existing) if this is a GET request for the edit page.
membership_formset = MembershipFormSet(request.POST or None, instance=Group())
return render_to_response(
'group_create.html',
{
'group_form': recipe_form,
'membership_formset': membership_formset,
},
context_instance=RequestContext(request),
)
This may be a starting point for a Class-based implementation, but it's simple enough that it's not been worth my while to try to shoehorn this into the Class-based paradigm.

I was facing pretty the same problem just a few days ago. Django has problems to process intermediary m2m relationships.
This is the solutions what I have found useful:
1. Define new CreateView
class GroupCreateView(CreateView):
form_class = GroupCreateForm
model = Group
template_name = 'forms/group_add.html'
success_url = '/thanks'
Then alter the save method of defined form - GroupCreateForm. Save is responsible for making changes permanent to DB. I wasn't able to make this work just through ORM, so I've used raw SQL too:
1. Define new CreateView
class GroupCreateView(CreateView):
class GroupCreateForm(ModelForm):
def save(self):
# get data from the form
data = self.cleaned_data
cursor = connection.cursor()
# use raw SQL to insert the object (in your case Group)
cursor.execute("""INSERT INTO group(group_id, name)
VALUES (%s, %s);""" (data['group_id'],data['name'],))
#commit changes to DB
transaction.commit_unless_managed()
# create m2m relationships (using classical object approach)
new_group = get_object_or_404(Group, klient_id = data['group_id'])
#for each relationship create new object in m2m entity
for el in data['members']:
Membership.objects.create(group = new_group, membership = el)
# return an object Group, not boolean!
return new_group
Note:I've changed the model a little bit, as you can see (i have own unique IntegerField for primary key, not using serial. That's how it got into get_object_or_404

Just one comment, when using CBV you need to save the form with commit=True, so the group is created and an id is given that can be used to create the memberships.
Otherwise, with commit=False, the group object has no id yet and an error is risen.

Related

Django Rest Framework Update or Create if not exists

I want to use the perform an update or create in django-rest-framework, by passing or not the id field. I've got this model
class Etiqueta(models.Model):
name_tag = models.CharField(max_length=200, blank=False, null=False)
description_tag = models.TextField(max_length=500, blank=False, null=False)
def __unicode__(self):
return self.name_tag
And in django-rest-framework I've got this serializer
from myapp.modulos.estado_1.models import Etiqueta
from rest_framework import serializers, viewsets
# Serializers define the API representation.
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Etiqueta
fields = (
'id',
'name_tag',
'description_tag'
)
# ViewSets define the view behavior.
class TagViewSet(viewsets.ModelViewSet):
queryset = Etiqueta.objects.all()
serializer_class = TagSerializer
Normally when I create an object, I perform a POST to the URL without the /:id, but if I've got an object with a local id, I want him to be created in the REST with the same id (remote id), django overwrite my local id and creates a new one. Does anybody know how achieve this? Also it is important to mention that I'm working with google-app-engine, google-cloud-datastore and django-dbindexer.
This code should work for your case -
class TagViewSet(viewsets.ModelViewSet):
queryset = Etiqueta.objects.all()
serializer_class = TagSerializer
def get_object(self):
if self.request.method == 'PUT':
obj, created = Etiquetta.objects.get_or_create(pk=self.kwargs.get('pk'))
return obj
else:
return super(TagViewSet, self).get_object()
You should have a look at how Django REST framework does currently and adapts your create method to update whenever you have an id field.
The original ViewSet.create is here and the ViewSet.update is here.
Please note that you will probably end up with two different serializers for /tag/ and /tag/:id since the later should not allow the id field to be writable while the former should.
I've write a drf views mixin for updating an object by id, if no corresponding object, just create it then update.

CreateOrUpdateView for Django Models

In my models I have a ForeignKey relationship like this:
class Question(models.Model):
question = models.TextField(null=False)
class Answer(models.Model):
question = models.ForeignKey(Question, related_name='answer')
user = models.ForeignKey(User)
class Meta:
unique_together = (("question", "user"),)
the corresponding URL to submit an answer contains the id of the question, like this:
url(r'^a/(?P<pk>\d+)/$', AnswerQuestion.as_view(), name='answer-question'),
With user coming from self.request.user, I am trying to get something like a CreateOrUpdateView, to allow some convenient navigation for the user and URL scheme.
Until now I tried this with:
class AnswerQuestion(LoginRequiredMixin, CreateView):
and add initial value, but that isn't clean because of pk. With an UpdateView I run into problems because I have to set default values for the form.
Has anybody done something like this? I'd rather avoid having a Create and Update view for the same Answer.
The UpdateView and CreateView are really not that different, the only difference is that UpdateView sets self.object to self.get_object() and CreateView sets it to None.
The easiest way would be to subclass UpdateView and override get_object():
AnswerQuestionView(LoginRequiredMixin, UpdateView):
def get_object(queryset=None):
if queryset is None:
queryset = self.get_queryset()
# easy way to get the right question from the url parameters:
question = super(AnswerQuestionView, self).get_object(Question.objects.all())
try:
obj = queryset.get(user=self.request.user, question=question)
except ObjectDoesNotExist:
obj = None
return obj
Returns the right answer if it exists, None if it does not. Of course add any attributes you need to the class, like model, form_class etc.

Override default queryset in Django admin

One of my models has a deleted flag, which is used to hide objects globally:
class NondeletedManager(models.Manager):
"""Returns only objects which haven't been deleted"""
def get_query_set(self):
return super(NondeletedManager, self).get_query_set().exclude(deleted=True)
class Conversation(BaseModel):
...
deleted = models.BooleanField(default=False)
objects = NondeletedManager()
all_conversations = models.Manager() # includes deleted conversations
How can I override the default queryset used by Django admin module to include deleted conversations?
You can override get_queryset method in your model admin class.
class MyModelAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(author=request.user)
Note in Django<=1.5 the method was named just queryset.
Konrad is correct, but this is more difficult than the example given in the documentation.
Deleted conversations can't be included in a queryset that already excludes them. So I don't see an option other than re-implementing admin.ModelAdmin.queryset entirely.
class ConversationAdmin (admin.ModelAdmin):
def queryset (self, request):
qs = Conversation.all_conversations
ordering = self.get_ordering(request)
if ordering:
qs = qs.order_by(*ordering)
return qs
You can do this with a Django proxy model.
# models.py
class UnfilteredConversation(Conversation):
class Meta:
proxy = True
# this will be the 'default manager' used in the Admin, and elsewhere
objects = models.Manager()
# admin.py
#admin.register(UnfilteredConversation)
class UnfilteredConversationAdmin(Conversation):
# regular ModelAdmin stuff here
...
Or, if you have an existing ModelAdmin class you want to re-use:
admin.site.register(UnfilteredConversation, ConversationAdmin)
This approach avoids issues that can arise with overriding the default manager on the original Conversation model - because the default manager is also used in ManyToMany relationships and reverse ForeignKey relationships.
What would be so wrong with the following:
class Conversation(BaseModel):
...
deleted = models.BooleanField(default=False)
objects = models.Manager() # includes deleted conversations
nondeleted_conversations = NondeletedManager()
So in your own apps/projects, you use Conversation.nondeleted_conversations() and let the built-in admin app do it's thing.
Natan Yellin is correct, but you can change the managers order and the first will be the default, then it is the used by the admin:
class Conversation(BaseModel):
...
deleted = models.BooleanField(default=False)
all_conversations = models.Manager() # includes deleted conversations
objects = NondeletedManager()
The admin implementation of get_queryset() use ._default_manager instead .objects, as show next
qs = self.model._default_manager.get_queryset()
ref Django github BaseModelAdmin implementation
This only ensures that every time you use YourModel.objects, you will not include deleted objects, but the generic views and others uses ._default_manager too. Then if you don't override get_queryset is not a solution. I've just check on a ListView and admin.
The accepted solution works great for me but I needed a little bit more flexibility, so I ended up extending the changelist view to add in a custom queryset parameter. I can now configure my default queryset/filter as such and it can still be modified by using a different filter (get parameters):
def changelist_view(self, request, extra_context=None):
if len(request.GET) == 0 :
q = request.GET.copy()
q['status__gt'] = 4
request.GET = q
request.META['QUERY_STRING'] = request.GET.urlencode()
return super(WorksheetAdmin,self).changelist_view(request, extra_context=extra_context)
To extend on some of these answers with what I found most concise and useful.
I've made the assumption you have a field like "name" to show the entries.
# admin.py
from django.contrib import admin
#admin.register(Conversation)
class ConversationAdmin(admin.ModelAdmin):
list_display = ('name', '_is_deleted')
# Nice to have but indicates that an object is deleted
#admin.display(
boolean=True,
ordering='deleted'
)
def _is_deleted(self, obj):
return obj.deleted
def get_queryset(self, request):
return Conversation.all_conversations
Which will give you an interface like:
The problem I found with subclassing a model was that it caused issues with meta inheritance and reverse-path lookups.

Django Forms: Hidden model field?

I've got a Form. I want to include a hidden field that returns a model. I'll set it's value in the view; I just need it to be posted along to the next page.
What field am I supposed to use in the form class?
A hidden field that returns a model? So a model instance ID?
The forms.HiddenInput widget should do the trick, whether on a FK field or CharField you put a model instance ID in.
class MyForm(forms.Form):
hidden_2 = forms.CharField(widget=forms.HiddenInput())
hidden_css = forms.CharField(widget=forms.MostWidgets(attrs={'style': 'display:none;'}))
I suppose the fastest way to get this working is
class MyForm(forms.Form):
model_instance = forms.ModelChoiceField(queryset=MyModel.objects.all(), widget=forms.HiddenInput())
form = MyForm({'model_instance': '1'})
form.cleaned_data['model_instance']
But I don't like the idea of supplying MyModel.objects.all() if you're going to specify one item anyways.
It seems like to avoid that behavior, you'd have to override the form __init__ with a smaller QuerySet.
I think I prefer the old fashioned way:
class MyForm(forms.Form):
model_instance = forms.CharField(widget=forms.HiddenInput())
def clean_model_instance(self):
data = self.cleaned_data['model_instance']
if not data:
raise forms.ValidationError()
try:
instance = MyModel.objects.get(id=data)
except MyModel.DoesNotExist:
raise forms.ValidationError()
return instance
The approach in Yuji's answer uses a clean_model_instance method on the form which is fine if you're only ever doing this once in your code base. If you do it more often, then you might benefit from implementing a custom model field.
This is the code I have:
from django import forms
class ModelField(forms.Field):
Model = None
def prepare_value(self, value):
"""Inject entities' id value into the form's html data"""
if isinstance(value, self.Model):
return value.id
return value
def to_python(self, value):
"""More or less stolen from ModelChoiceField.to_python"""
if value in self.empty_values:
return None
try:
value = self.Model.objects.get(id=value)
except (ValueError, self.Model.DoesNotExist):
raise forms.ValidationError('%s does not exist'
% self.Model.__class__.__name__.capitalize())
return value
If you use that as a base class and then specialise it with your own models then it becomes a useful based. For example:
# In app/fields.py
from .models import CustomModel
class CustomModelField(ModelField):
Model = CustomModel
Then you can pair that with whatever widget you need at the time:
# in app/forms.py
class MyForm(forms.Form):
hidden_custom_model_field = CustomModelField(widget=forms.HiddenInput())
other_widget_custom_model_field = CustomModelField(widget=MyCustomWidget())

Free-form input for ForeignKey Field on a Django ModelForm

I have two models related by a foreign key:
# models.py
class TestSource(models.Model):
name = models.CharField(max_length=100)
class TestModel(models.Model):
name = models.CharField(max_length=100)
attribution = models.ForeignKey(TestSource, null=True)
By default, a django ModelForm will present this as a <select> with <option>s; however I would prefer that this function as a free form input, <input type="text"/>, and behind the scenes get or create the necessary TestSource object and then relate it to the TestModel object.
I have tried to define a custom ModelForm and Field to accomplish this:
# forms.py
class TestField(forms.TextInput):
def to_python(self, value):
return TestSource.objects.get_or_create(name=value)
class TestForm(ModelForm):
class Meta:
model=TestModel
widgets = {
'attribution' : TestField(attrs={'maxlength':'100'}),
}
Unfortunately, I am getting: invalid literal for int() with base 10: 'test3' when attempting to check is_valid on the submitted form. Where am I going wrong? Is their and easier way to accomplish this?
Something like this should work:
class TestForm(ModelForm):
attribution = forms.CharField(max_length=100)
def save(self, commit=True):
attribution_name = self.cleaned_data['attribution']
attribution = TestSource.objects.get_or_create(name=attribution_name)[0] # returns (instance, <created?-boolean>)
self.instance.attribution = attribution
return super(TestForm, self).save(commit)
class Meta:
model=TestModel
exclude = ('attribution')
There are a few problems here.
Firstly, you have defined a field, not a widget, so you can't use it in the widgets dictionary. You'll need to override the field declaration at the top level of the form.
Secondly get_or_create returns two values: the object retrieved or created, and a boolean to show whether or not it was created. You really just want to return the first of those values from your to_python method.
I'm not sure if either of those caused your actual error though. You need to post the actual traceback for us to be sure.
TestForm.attribution expects int value - key to TestSource model.
Maybe this version of the model will be more convenient for you:
class TestSource(models.Model):
name = models.CharField(max_length=100, primary_key=True)
Taken from:
How to make a modelform editable foreign key field in a django template?
class CompanyForm(forms.ModelForm):
s_address = forms.CharField(label='Address', max_length=500, required=False)
def __init__(self, *args, **kwargs):
super(CompanyForm, self).__init__(*args, **kwargs)
try:
self.fields['s_address'].initial = self.instance.address.address1
except ObjectDoesNotExist:
self.fields['s_address'].initial = 'looks like no instance was passed in'
def save(self, commit=True):
model = super(CompanyForm, self).save(commit=False)
saddr = self.cleaned_data['s_address']
if saddr:
if model.address:
model.address.address1 = saddr
model.address.save()
else:
model.address = Address.objects.create(address1=saddr)
# or you can try to look for appropriate address in Address table first
# try:
# model.address = Address.objects.get(address1=saddr)
# except Address.DoesNotExist:
# model.address = Address.objects.create(address1=saddr)
if commit:
model.save()
return model
class Meta:
exclude = ('address',) # exclude form own address field
This version sets the initial data of the s_address field as the FK from self, during init , that way, if you pass an instance to the form it will load the FK in your char-field - I added a try and except to avoid an ObjectDoesNotExist error so that it worked with or without data being passed to the form.
Although, I would love to know if there is a simpler built in Django override.