Django - Saving ModelForm with foreign key from other model - python

I'm creating a Poll app with these models
class Poll(BaseModel):
title = models.CharField(max_length=255)
end_date = models.DateField()
class Choice(BaseModel):
poll = models.ForeignKey('Poll')
choice = models.CharField(max_length=255)
index = models.IntegerField()
A poll can have many choices -- the amount will vary for each poll. I'm struggling to figure out how to save a Poll through a modelform and at the same time save its related Choices.
I know I have to override the Save and Clean methods in my PollForm, but after that it kind of gets convoluted. I know there's a more pythonic/djangoesque way of doing this. My main confusion is the relationship between Choice and Poll, because it's only defined in one direction.
Additionally, I can't figure out how this would work when updating a Poll with a set of choices where some exist and some are new. The below code does not work, of course, but it's how I'm approaching thinking about this. I would appreciate a nudge in the right direction!
class PollForm:
def save(self, choices, commit=True, *args, **kwargs):
poll = super(PollForm, self).save(commit=False, *args, **kwargs)
if commit:
p = poll.save()
for choice in choices:
choice['poll_id'] = p.id
if choice['id']:
c = ChoiceForm(choice, instance=Choice.objects.get(id=choice['id']))
else:
c = ChoiceForm(choice)
if c.is_valid():
c.save()
return poll

What you need is Django Formsets (https://docs.djangoproject.com/en/1.8/topics/forms/formsets/), specifically: Model Formsets (https://docs.djangoproject.com/en/1.8/topics/forms/modelforms/#model-formsets).
For your model, you must use an inline formset (for the Choice model).
You can find all the information about them in: https://docs.djangoproject.com/en/1.8/topics/forms/modelforms/#inline-formsets.
Hope this helps.

Related

Incorporating "Like Post" feature

Hey so I am making a color scheme posting site where people can register and post color schemes they come up with. So far everything is working great, the only thing I have left to do is add a "Like Post" feature. I'm wondering what the best way to implement this would be.
I have two ideas on how this could be done, the first is add an additional field to both the ColorSet (posts) and the User models (for the user model I would set up a new model with a OneToOne relationship to add onto the User model) which would record users that have each single post, and which posts each user has liked to keep track of everything.
So this could look something like this:
from django.db import models
from django.core.urlresolvers import reverse
from django.conf import settings
from django.contrib.auth import get_user_model
User = get_user_model()
# Create your models here.
class ColorSet(models.Model):
user = models.ForeignKey(User,related_name='colorset')
published_date = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=50,blank=False)
color_one = models.CharField(max_length=6,blank=False,default='cccccc')
color_two = models.CharField(max_length=6,blank=False,default='ffffff')
color_three = models.CharField(max_length=6,blank=False,default='e5e5e5')
color_four = models.CharField(max_length=6,blank=False,default='f0f0f0')
color_five = models.CharField(max_length=6,blank=False,default='bababa')
liked_by = models.IntegerField(blank=True)
def publish(self):
self.save()
def get_absolute_url(self):
return reverse('index')
def __str__(self):
return self.name
user model:
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class UserStats(models.Model):
user = models.OneToOneField(User)
liked_sets = models.IntegerField(blank=True)
def __str__(self):
return self.user.username
In this first option I would have the new model fields (liked_sets and liked_by) be equal to lists containing the ok's of all the Color Sets each user has liked and all the users who have liked each post respectively.
The other way that I'm thinking about would be to just create an entirely new model that tracks the likes for each color set post (not totally sure how this model would look yet exactly).
Aside from which is easier, I wondering which makes more sense from a technical standpoint? Will one of these two options take up more space or create heavier server load?
Thanks for the help.
As far as I understand your problem, it can be broken in two parts.
Maintaining the total number of likes on a model ColorSet.
Keeping the track of all those users who liked a single instance of ColorSet.
Now if I understand your problem correctly(correct me if I'm wrong), when you say:
new model fields (liked_sets and liked_by) be equal to lists containing the ok's of all the Color Sets each user has liked and all the users who have liked each post respectively.
you intend to create a field in your database which would simply store a list of pks of all the people who've liked a ColorSet model instance. Even if you don't intend to do that, still an IntegerField to store such information is(in my humble opinion) somewhat wrong.
Now why you wouldn't want to do that? It's because relational databases are made to recognize the relations between tuples of information and enhance the processing by creating relations. That is why we use relations like OneToOneField and ForeignKey. They make the processing way faster. If we were to simply store the pk values in a Field, further search them in our database to retrieve information, that would be something really slow.
Now I suppose what you are looking for is ManyToManyField.
In your problem, you will simply map the ManyToManyField it to the User model.
It would look something like:
class ColorSet(models.Model):
user = models.ForeignKey(User,related_name='colorset')
published_date = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=50,blank=False)
color_one = models.CharField(max_length=6,blank=False,default='cccccc')
color_two = models.CharField(max_length=6,blank=False,default='ffffff')
color_three = models.CharField(max_length=6,blank=False,default='e5e5e5')
color_four = models.CharField(max_length=6,blank=False,default='f0f0f0')
color_five = models.CharField(max_length=6,blank=False,default='bababa')
liked_by = models.IntegerField(blank=True)
#add a simple ManyToManyField which will hold all the users who liked this colorset
likers = models.ManyToManyField(User , related_name = 'liked_colorsets')
def publish(self):
self.save()
def get_absolute_url(self):
return reverse('index')
def __str__(self):
return self.name
and remove your UserStats model to
Now use the following code outline structure to access the information from the database.
1) To get the ColorSets liked by a User:
#obtain any user model object; for example: user_object = User.objects.get(...)
user_object.liked_colorsets.all()
#a queryset with all the liked colorsets is returned.
2) To get the Users who liked a ColorSet:
#obtain any colorset model object; for example: colorset_object = ColorSet.objects.get(...)
colorset_object.likers.all()
#a queryset with all the Users who liked this colorset is returned.
One more thing that I would like to add here. After a User likes a ColorSet, you would obviously want to add this User to the likers field in your ColorSet model(and increment the liked_by field; I assume you'll manage that). To add a User in the likers field:
#obtain any colorset model object; for example: colorset_object = ColorSet.objects.get(...)
#obtain the user model object of the user who liked this colorset in user_object
#and do
colorset_object.likers.add(user_object)
Read more about adding the models in ManyToManyField here in docs.
Hope this helps. Thanks.

Django: Overriding the Save (Either Model or Admin Section)

I've been playing around with Django recently, however I'm stuck on how to approach this problem. I have a 'Person' model which has a one to many relationship with a 'Voucher' Model. Now the person has a quota and each time a Voucher is generated, the quota will decrease by one. What I'm stuck on is doing this through the save method. So how does one do this?
Below are my models:
class Person(AbstractDetail):
# Note: Model to maintain information about Person
vc = models.IntegerField(default = 3, verbose_name = 'Vouchers',
null = False, validators = [ validate_voucher ])
class Voucher(models.Model):
# Note: Model to maintain information about Voucher
vc = models.CharField (max_length = 25, verbose_name = 'Voucher ID',
help_text = 'Voucher Identifier')
ps = models.ForeignKey(Person, on_delete = models.CASCADE,
verbose_name = 'Person')
Don't do it in save(), it's easily messed up with django. Try to use django signal post_save():
from django.db.models.signals import post_save
#receiver(post_save, sender=Voucher)
def decrease_quota(sender, instance, created, *args, **kwargs):
if created:
instance.vc -= 1
instance.save()
Check django doc for django signals.
I'd take a slight performance hit in favour of avoiding the duplication of the voucher count information (call me old-fashioned).
In stead of saving the voucher count in Person.vc, I'd evaluate the voucher count when needed by creating a function for that in the Voucher object manager:
def get_voucher_count(self, person):
return self.filter(ps=person).count()
OK, I manage to work it out (with the help of Shang Wang & Ytsen de Boer answer as well, which I'm grateful for). The way which I did it was to create an instance of the object Person and take away from it there.
Fortunately enough, I quick time check the doc's which Shang just showed me (and look way deeper as well). Below is the way to do it:
#receiver(post_save, sender = Voucher, dispatch_uid = "voucher_identifier")
def decrease_quota(sender, instance, **kwargs):
obj = Person.objects.get(pk = instance.ps.id)
if obj.vc < 4 and obj.vc > 0:
obj.vc = obj.vc - 1
obj.save()
Of course I have to do abit more to it (simple validations and stuff like that), but this how I needed it.

Django: combine two ForeignKeys into one field

I need to implement the following:
The user shall be presented with a form that will have a drop down choice menu consisting of property names. There are two types of properties: general properties, i.e. properties common for all users and custom properties, i.e. properties that each user has defined prior to that. The models would look something like that:
class GeneralPropertyName(models.Model):
name = models.CharField(max_length=20)
class CustomPropertyName(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=20)
The drop down menu should have all general properties and only those custom properties that pertain to the user.
First question: how to define such a model?
I need to: 1. somehow unify both properties, 2. take only those items from CustomPropertyName that pertain to the user
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = ForeignKey(GeneralPropertyName) ??UNIFY??? ForeignKey(CustomPropertyName)
Second, is there anything special that needs to be done with ModelForm?
class SpecDataForm(ModelForm):
class Meta:
model = SpecData
And the 3rd question is what needs to be done in the view? I will need to use inline formsets since I will have a few dynamic forms like that.
def index(request):
user = User.objects.get(username=request.user.username)
specdataFormSet = inlineformset_factory(User, SpecData, form=SpecDataForm, extra=30)
...
specdata_formset = specdataFormSet(instance=user, prefix='specdata_set')
...
Thanks.
EDIT: Adjusted juliocesar's suggestion to include formsets. Somehow I am getting the following error message: Cannot resolve keyword 'property' into field. Choices are: id, name, selection_title, user
def index(request):
user = User.objects.get(username=request.user.username)
user_specdata_form = UserSpecDataForm(user=user)
SpecdataFormSet = inlineformset_factory(User, SpecData, form=user_specdata_form, extra=30)
You can use a GenericForeignKey to handle it, but you still need more to solve your further questions about forms and view.
I have made an example of how you solve your problem (logged user can select from General properties and his Custom properties, non-logged user only can select General properties). I used model inheritance for the properties (In your sample code it seems that a CustomPropertyName is a PropertyName with other fields). I think inheritance is an easier and a more basic concept than ContentTypes and it fits to your needs.
NOTE: I remove some code like imports to simplify the code.
1) models.py file:
class PropertyName(models.Model):
name = models.CharField(max_length=20)
def __unicode__(self):
return self.name
class CustomPropertyName(PropertyName): # <-- Inheritance!!
user = models.ForeignKey(User)
def __unicode__(self):
return self.name
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = models.ForeignKey(PropertyName)
NOTES: The field SpecData.property points to PropertyName since all properties are saved in the PropertyName's database table.
2) forms.py file:
from django import forms
from django.db.models import Q
from models import SpecData, PropertyName
def UserSpecDataForm(user=None):
UserPropertiesQueryset = PropertyName.objects.filter(Q(custompropertyname__user=None) | Q(custompropertyname__user__id=user.id))
class SpecDataForm(forms.ModelForm):
property = forms.ModelChoiceField(queryset=UserPropertiesQueryset)
class Meta:
model = SpecData
exclude = ('user',)
return SpecDataForm
NOTES: The trick here is to generate the form SpecDataForm dynamically, by filtering properties according the user specified in the parameter.
3) views.py file:
from forms import UserSpecDataForm
def index(request):
if request.POST:
form = UserSpecDataForm(request.user)(request.POST) # instance=user
if form.is_valid():
spec_data = form.save(commit=False)
spec_data.user = request.user
spec_data.save()
else:
form = UserSpecDataForm(request.user)()
return render_to_response('properties.html', {'form': form}, context_instance=RequestContext(request))
NOTES: Nothing special here, just a call to form.UserSpecDataForm(request.user) that returns the form class and then instantiate. Also setted the logged-in user to the object returned on save since It was excluded in the form to not show in front-end.
Following this basic example you can do the same with formsets if you need it.
UPDATE:
Formset can be used by adding following code to the view:
user_specdata_form = UserSpecDataForm(user=request.user)
SpecdataFormSet = inlineformset_factory(User, SpecData, form=user_specdata_form, extra=30)
The complete project sample can be downloaded from http://ge.tt/904Wg7O1/v/0
Hope this helps
1a) have you looked into django's ContentType framework this will allow you to have generic foreign keys and you can put restrictions on what types of models are acceptable to store in.
1b) I think that the validation for accepting what type of foreign key is acceptable shouldn't be in your model but should be part of your form validation before saving.
2) If you do use a model form you're going to have to define your own custom widget for the propery field. This means you're probably going to have to write you're own render function to render the html from the field. You should also define your own validation function on the form to make sure that only the appropriate data is acceptable to save.
3) I don't think you'll have to do anything you aren't already doing in the views
Use GenericForeignKey:
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
property = GenericForeignKey('content_type', 'object_id')
You can use this to combine the two fields(type & id) into a single choice field.
One way is that you have only one model, make user nullable:
class PropertyName(models.Model):
user = models.ForeignKey(User, null=True, blank=True)
name = models.CharField(max_length=20)
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = ForeignKey(PropertyName)
So, if user is not set, it is a general property. If it is set, it is related to this user.
However, please note that if you need unique property names, that NULL != NULL.
Of course, the suggested GenericForeignKey solution is better for some cases.
Also, you can easily make the normal (non-model) form with that you describe and separate form logic from model logic.

Modifying a Django ModelForms fields

I have a Django model, which has a foreign key owner, referring to the user who owns this object.
To let other users edit these objects, I currently use a forms.ModelForm, which works fine so far.
But I know want to let the owner and only the owner change the owner of the object he owns (what an ownage! :). Thus I tried the following:
class FolderForm(forms.ModelForm):
def __init__(self, user, *args, **kwargs):
if kwargs.get("instance", False):
if user == kwargs["instance"].owner:
self._meta.fields += ("owner",)
super(FolderForm, self).__init__(*args, **kwargs)
class Meta:
model = Folder
fields = (
"name",
"description",
)
But this doesn't work, since Django uses some metaclass-magic to set the fields on the model, which seems to be done before my subclassed __init__ is called.
Anyone ever did something like this?
I like to use closure normally
def make_form(exclude_user=True):
class Form(forms.ModelForm):
class Meta:
model = Folder
exclude = ['user'] if exclude_user else None
return Form
form_cls = make_form(request.user != folder.owner)
Why don't you create two Forms:
One that excludes owner for users that don't own the data (mouthful) and do a simple if statement in your view:
if request.user == Model.owner:
form = OwnerForm
else:
form = OthersForm
Keep it as simple as possible has HUGE wins down the line.

Django autocategorize in m2m field

I have done a pre_save signal in my django/satchmo inherited model Product called JPiece and I have another model inheritance from satchmo Category called JewelCategory. The pre_save signal makes the JPiece objects get the category list and add those categories that fit the Jpiece description to the relation, that is done in the model, meaning if I manually do
p = Jpiece.objects.get(pk=3)
p.save()
The categories are saved and added to the p.category m2m relation but If i save from the admin it does not do this...
How can I achieve this... to save from the admin a JPiece and to get the categories it belongs too...
Here are the models remember that they both have model inheritance from satchmo product and category classes.
class Pieza(Product):
codacod = models.CharField(_("CODACOD"), max_length=20,
help_text=_("Unique code of the piece. J prefix indicates silver piece, otherwise gold"))
tipocod = models.ForeignKey(Tipo_Pieza, verbose_name=_("Piece Type"),
help_text=_("TIPOCOD"))
tipoenga = models.ForeignKey(Engaste, verbose_name=_("Setting"),
help_text=_("TIPOENGA"))
tipojoya = models.ForeignKey(Estilos, verbose_name=_("Styles"),
help_text=_("TIPOJOYA"))
modelo = models.CharField(_("Model"),max_length=8,
help_text=_("Model No. of casting piece."),
blank=True, null=True)
def autofill(self):
#self.site = Site.objects.get(pk=1)
self.precio = self.unit_price
self.peso_de_piedra = self.stone_weigth
self.cantidades_de_piedra = self.stones_amount
self.for_eda = self.for_eda_pieza
if not self.id:
self.date_added = datetime.date.today()
self.name = str(self.codacod)
self.slug = slugify(self.codacod, instance=self)
cats = []
self.category.clear()
for c in JewelCategory.objects.all():
if not c.parent:
if self.tipocod in c.tipocod_pieza.all():
cats.append(c)
else:
if self.tipocod in c.tipocod_pieza.all() and self.tipojoya in c.estilo.all():
cats.append(c)
self.category.add(*cats)
def pieza_pre_save(sender, **kwargs):
instance = kwargs['instance']
instance.autofill()
# import ipdb;ipdb.set_trace()
pre_save.connect(pieza_pre_save, sender=Pieza)
I know I can be vague with explanations sometimes of what I need so please feel free to ask anything Ill be sure to clarify ASAP since this is a client that needs this urgently.
Thank you all as always...
If you use pre_save, it's called before save(), meaning you can't define m2m relationships since the model doesn't have an ID.
Use post_save.
# this works because the ID does exist
p = Jpiece.objects.get(pk=3)
p.save()
Update, check out the comment here: Django - How to save m2m data via post_save signal?
It looks like the culprit now is that with an admin form, there is a save_m2m() happening AFTER the post_save signal, which could be overwriting your data. Can you exclude the field from the form in your ModelAdmin?
# django.forms.models.py
if commit:
# If we are committing, save the instance and the m2m data immediately.
instance.save()
save_m2m()

Categories

Resources