django - how to save modified form without overriding original entry - python

I have a logical "how to" or best practices problem.
A simplified example
I have a model class with an textbox. Users can add new entries but they will only be displayed for other users if the admin accepts them.
class MyClass(models.Model):
# Relation to a user
user = ForeignKey(User)
# Simple textbox as example attribute
text = TextArea()
# Admin has to accept the entry that other users can see it
accepted = BooleanField(default=False)
Problem
I would like to let users modify a listing, but the admin has to accept them first. As long as the admin hasn't accept the modification it should still show the old unmodified version of the entry.
My Approaches
a) create a new class
class MyEditClass(models.Model)
# ForeignKey to the original class
fk = ForeignKey(MyClass)
user = ForeignKey(User)
text = TextArea()
accepted = BooleanField(default=False)
The modifications gets saved in a new table/class. If the admin accepts this modified entry the original entry becomes this one.
Why I don't like it? My class has about 60 attributes with a lot of relations. As I haven't found a solution to duplicate a complete class this produces a lot of duplicated lines of code. If I add a new attr. in MyClass I also have to add it in MyEditClass ...
b) Add a new atrribute to MyClass if entry got edited
class MyClass(models.Model):
# new attribute with the primary key of the edited field
edited_pk = PositiveIntegerField(default=None, blank=True, none=True)
user = ForeignKey(User)
text = TextArea()
accepted = BooleanField(default=False)
In this case you don't create a new class, instead you save the edited entries in the same class and add the attribute edited_pk. If a entry is a new entry set edited_pk = None (default). If a User modifies an entry get the pk from the original entry. Then add the modified one as a new entry with edited_pk = original_entry.pk. If the admin accepts the modified version, the original entry gets overridden with the modified one.
Why I don't like this solution? As Admin I would like to have a single class in the backend to accept modified entries.
Do you have any other (maybe already bultin or 3rd party) approaches?
Thanks in advance
(Title is bad, but I can't find a better name. Please edit if you do so)
Solution
If you don't want to use a 3rd Party app check the marked answer.
My favorite solution, also by Obj3ctiv3_C_88 needs django-simple-history.
Therefore I created a method:
class MyClass(models.Model):
user = ForeignKey(User)
text = TextArea()
accepted = BooleanField(default=False)
history = HistoricalRecords() # rtd from django-simple-history
def get_accepted(self):
"""Return the first entry from the history which is accepted."""
return self.history.filter(accepted=True).first()
in your views:
# This code may be optimized, but for now it works
items = MyClass.objects.all()
items = list(items) # convert queryset to a list
i = 0
for item in items:
# Important to get the instance. Otherwise custom methods won't work
items[i] = item.get_accepted().instance
i += 1

Would something like this work?
class BlogComment(models.Model):
blog = models.ForeignKey(Blog)
user = models.ForeignKey(User)
unapproved = models.Charfield(max_length=1000, default=None, Blank=True)
approved = models.Charfield(max_length=1000, default=None, Blank=True)
# on submit
BlogComment.unapproved = request.POST['user_comment']
# on approve
BlogComment.approved = BlogComment.unapproved
BlogComment.unapproved = None
BlogComment.save()
This would allow you to keep 2 distinct states for the same comment. You only render the BlogComment.approved. For find the comments which need approval you just filter(~Q(unapproved = None))

Related

Trying to set user field in the nested form of a django nested inline formset - fails

I followed this: https://www.yergler.net/2009/09/27/nested-formsets-with-django/ and this: django inline formsets with a complex model for the nested form and overall my code works great.
class Account(models.Model):
user_username = models.ForeignKey(User, on_delete=models.CASCADE)
account_name = models.CharField(max_length=30)
class Classification(models.Model):
user_username=models.ForeignKey(User, on_delete=models.CASCADE)
data_id=models.ForeignKey(ImportData, on_delete=models.CASCADE)
class ImportData(models.Model):
user_username = models.ForeignKey(User, on_delete=models.CASCADE)
data_id = models.UUIDField(
primary_key=True, default=uuid.uuid4, editable=False)
ClassificationFormset = inlineformset_factory(ImportData, Classification, exclude=('user_username',), extra=1)
# below is just what came from the nested formset links above: pasted here for easy reference.
class BaseNestedTransactionFormset(BaseInlineFormSet):
def add_fields(self, form, index):
# allow the super class to create the fields as usual
super(BaseNestedTransactionFormset, self).add_fields(form, index)
try:
instance = self.get_queryset()[index]
pk_value = instance.pk
except IndexError:
instance=None
pk_value = hash(form.prefix)
transaction_data = None
if (self.data):
transaction_data = self.data;
# store the formset in the .nested property
form.nested = [
CategoryFormset(data=transaction_data,
instance = instance,
prefix = 'CAT_%s' % pk_value)]
def is_valid(self):
result = super(BaseNestedTransactionFormset, self).is_valid()
for form in self.forms:
if hasattr(form, 'nested'):
for n in form.nested:
# make sure each nested formset is valid as well
result = result and n.is_valid()
return result
def save_new(self, form, commit=True):
"""Saves and returns a new model instance for the given form."""
instance = super(BaseNestedTransactionFormset, self).save_new(form, commit=commit)
# update the form’s instance reference
form.instance = instance
# update the instance reference on nested forms
for nested in form.nested:
nested.instance = instance
# iterate over the cleaned_data of the nested formset and update the foreignkey reference
for cd in nested.cleaned_data:
cd[nested.fk.name] = instance
return instance
def save_all(self, commit=True):
"""Save all formsets and along with their nested formsets."""
# Save without committing (so self.saved_forms is populated)
# — We need self.saved_forms so we can go back and access
# the nested formsets
objects = self.save(commit=False)
# Save each instance if commit=True
if commit:
for o in objects:
o.save()
# save many to many fields if needed
if not commit:
self.save_m2m()
# save the nested formsets
for form in set(self.initial_forms + self.saved_forms):
# if self.should_delete(form): continue
for nested in form.nested:
nested.save(commit=commit)
ImportTransactionFormset = inlineformset_factory(Account, ImportData, exclude=('user_username',), formset=BaseNestedTransactionFormset, extra=0)
My template has a table that displays the import data formset... user selects the account and the table shows all the imported data from that account. For each of these row forms, there is a hidden row underneath... user clicks a button to show that hidden row. The hidden row displays the nested classification formset.
If include the user_username field in the template and allow for it to be part of the nested formset in the template, i can set is accordingly in the html form and the formsets save no problem.
However: I want to be able to exclude the user_username field from the template and have my view or some other method under the BaseNestedTransactionFormset class set the value of the user_username field to request.user value for whoever is logged in at that time.
I tried to override the clean method, but cleaned_data kicks back an error because the form doesnt validate; the field is required. I can't seem to figure out a good way to do this.
If this was a normal formset, not too hard to do. I would just set the field by modifying what comes back from POST. I have never worked with nested inline formsets, and the prefixes and indeces in the field names have got me. I've been at this for a couple of days and can't seem to be getting anywhere.
I am also contemplating just getting rid of that field from the classification model, since it is already tied to the ImportData model which is linked to the logged in user regardless. I'm just thinking i may run into this at some point again, so maybe good to solve.
Thanks in advance.

Why does Django models OneToOne query all objects for drop-down when ForeignKey does not?

models.py
class PumpLog(models.Model):
# work_order = models.OneToOneField('WorkOrder', on_delete = models.CASCADE)
work_order = models.ForeignKey('WorkOrder', on_delete = models.CASCADE)
class WorkOrder(models.Model):
completed_on = models.DateField('Completed On', null=True, blank=True)
template_.html
{{ form.work_order|as_crispy_field }}
forms.py
class TempForm(forms.ModelForm):
'''Initializes a mostly blank form with initial values for fields specified in default_values_list'''
initial = {}
page_type = self.page_type
if page_type == 'detail':
data = self.request.POST or None
for default_value in default_values_list:
if default_value in self.kwargs:
initial[default_value] = self.kwargs[default_value]
def clean(self):
...
def __init__(self, *args, **kwargs):
kwargs['initial'] = self.initial
super(TempForm, self).__init__(*args, **kwargs)
form = modelform_factory(model, TempForm,exclude = exclude_list,widgets = widgets)
As you can see there is a FK relationship between PumpLog and WorkOrder. When the PumpLog-Update page loads it only queries the selected/related WorkOrder. If the drop-down to select the work order is clicked - it queries additional WorkOrder as the user scrolls or searches.
I am trying to convert it to OneToOne. But when I switch it to OneToOne I notice that it retrieves all WorkOrders - which can take 1-4 minutes to load. I noticed this by placing print(self.index) in the WorkOrder __str__ method. In the console I see it list every single WorkOrder one-by-one. But if I switch it back to FK it only displays the index of the one already selected in the drop-down.
This doesn't seem to be a widget or crispy_form issue because I see the same behaviour when I remove or revert those.
I understand to some degree that it needs access to all of the work orders so that the user can select whichever one. BUT it doesn't seem to need to load all WorkOrders when the relationship is FK. Why is it behaving differently when I use OneToOne?
#markwalker_ pointed out
...django loads all related objects unless you override it.
This guided me to discovering that I had in fact overrode FK drop-downs by default via widgets. This would also explain why I experienced the same sluggishness when commenting out the widgets while using the OneToOne.
In fact if I had commented out the widgets while using the FK relationship I would have seen a similar sluggishness.
The way I override it is first check for any FK fields (which didn't check for OneToOne fields)
def get_foreign_keys(model):
foreign_keys = []
for field in model._meta.fields:
if field.get_internal_type() == 'ForeignKey':
foreign_keys.append(field)
return foreign_keys
...
foreign_keys = get_foreign_keys(model)
I then use the autocomplete ModelSelect2 widget which handles the lazy loading which I took for granted:
for fk in foreign_keys:
widgets[field_name] = autocomplete.ModelSelect2(url = url, forward=forward)
My quick-and-dirty solution is to check for OneToOne fields in get_foreign_keys():
def get_foreign_keys(model):
foreign_keys = []
for field in model._meta.fields:
if field.get_internal_type() == 'ForeignKey':
foreign_keys.append(field)
elif field.get_internal_type() == 'OneToOneField':
foreign_keys.append(field)
print(field.get_internal_type())
return foreign_keys
A more long term solution will simply be refactoring some naming to accommodate both FK and O2O for that function. And also some additional documentation - so I can better navigate old code.

Why is Django's `add()` method in my `many-to-many` Django models not taking?

Question / Problem:
I am building a Django app, with 2 models: User and Secret. Secrets can be made by Users, and other Users can "like" them. I've setup my likes field as a ManyToManyField, so that Users whom like a Secret can be stored there and later retrieved, etc. However, when I try to query for a User and a Secret and use my_secret.likes.add(my_User) nothing happens. I don't receive an error and when I print my Secret's many-to-many likes field, after the add, I see: secrets.User.None.
Why is my add() method running but I am not receiving any errors, and why is my User not properly being added to my Secret's likes?
Note: I've saved both the User and Secret objects upon initial creation. Outside this application I've been able to use the add() method just fine, but in those scenarios I was creating objects in the moment, and not retreiving already existing objects.
Is there a different way to handle add() when using data retreived from a Query? That's my only other line of reasoning right now, and I've followed the documentation here exactly: Django Many-to-Many Docs
I also apologize if this was answered elsewhere on the site. I did find one other post here, but there was no solution provided, granted they were experiencing the exact same issue.
My Models:
class User(models.Model):
"""
Creates instances of a `User`.
Parameters:
-`models.Model` - Django's `models.Model` method allows us to create new models.
"""
first_name = models.CharField(max_length=50) # CharField is field type for characters
last_name = models.CharField(max_length=50)
email = models.CharField(max_length=50)
password = models.CharField(max_length=22)
created_at = models.DateTimeField(auto_now_add=True) # DateTimeField is field type for date and time
updated_at = models.DateTimeField(auto_now=True) # note the `auto_now=True` parameter
objects = UserManager() # Attaches `UserManager` methods to our `User.objects` object.
class Secret(models.Model):
"""
Creates instances of a `Secret`.
Parameters:
-`models.Model` - Django's `models.Model` method allows us to create new models.
"""
description = models.CharField(max_length=100) # CharField is field type for characters
user = models.ForeignKey(User, related_name="secrets") # One-to-Many Relationship
likes = models.ManyToManyField(User) # Many to Many Relationship
created_at = models.DateTimeField(auto_now_add=True) # DateTimeField is field type for date and time
updated_at = models.DateTimeField(auto_now=True) # note the `auto_now=True` parameter
objects = SecretManager() # Attaches `SecretManager` methods to our `Secret.objects` object.
Problem Example:
The model migrates fine, everything seems to be in proper syntax. However, when I try and retrieve a User and a Secret, and add the User to the Secret.likes, the add() method gives no errors, runs, but no objects are saved.
Here's an example:
tim = User.objects.get(email="tim#tim.com") # Gets a user object
my_secret = Secret.objects.get(id=2) # Gets a secret object
# This is where nothing seems to happen / take:
my_secret.likes.add(tim) # add() method per Django many-to-many docs
print my_secret.likes # returns: `secrets.User.None` -- why?
Why when printing my_secret.likes above, is nothing printed?
Especially when:
tim.secret_set.all() shows the secret containing an id=2 as in the above example....so the User is recording the relationship with the Secret, but the Secret is not recording any relationship with the User. What am I doing wrong?
You need to call the all method of the many-to-many field to view all related objects:
print my_secret.likes.all()
# ^^^^^

Extending Django User model - form population errors

I'm extending Django's (v1.9) built-in User model with Player class, to add some extra properties.
class Player(models.Model):
TIMEZONES=()
user = models.OneToOneField(User, on_delete=models.CASCADE)
... (player-specific properties here)
time_zone = models.CharField(max_length=255, choices=PRETTY_TIMEZONE_CHOICES, blank=True, null=True,)
When creating users from Django admin panel, I don't always need to create players, so sometimes only User gets created. As a result, Player and User IDs don't exactly match. Turns out that this leads to a problem when populating ModelForms of models that are linked to Player, like this one:
class City(models.Model):
player = models.ForeignKey(Player, on_delete=models.CASCADE)
name = models.CharField(max_length = 100)
x_coord = models.SmallIntegerField()
y_coord = models.SmallIntegerField()
region = models.CharField(max_length = 100)
def __unicode__(self):
return str(self.player) + "-" + str(self.name)
class Meta:
db_table = 'cities'
class CityForm(ModelForm):
class Meta:
model = City
fields = (
'name',
'player',
'x_coord',
'y_coord',
'region')
This modelForm is used when creating a new city. When User ID and Player ID match, there is no problem, player ID gets populated in the form and city is successfully created. When User ID and Player ID are different, player ID is not populated in the form, the form fails to validate, and city creation fails.
I have no problem getting Player ID from request.user, and I could fix up the player ID before validating after getting POST data. I've also added a post-save hook so that Player always gets created, so the IDs will always match. But it seems that form should be populated with player ID in the first place, since user data is accessible and it's a one to one relationship.
What am I missing here?
What you are missing is that when you instantiate a ModelForm to create a new row that's related to some existing object, Django has no way of knowing the id of the related object. You need to tell it somehow.
One way to do that is, when you are displaying the form in response to a GET, use the initial argument to the form constructor:
myform = MyModelFormClass(None, initial={ 'myfkfield': myrelatedobject.pk })
Now the form class knows what value to pre-fill in when it renders the form, and when the form is posted, that field will be posted with it.
The other way to do it would be to omit the relation field from your form altogether, then fill it in later before you save, by using the commit argument to the form save method:
myform = MyModelFormClass(request.POST)
# this causes form values to be filled into the instance without actually
# writing to the database yet.
myinstance = myform.save(commit=False)
myinstance.myfkfield = myrelatedobject
# now really write to database
myinstance.save()
Note that this would be for an insert. For updates, you need to supply the existing object to your modelform constructor, like this:
myinstance = MyModel.objects.get(pk=self.kwargs.pk)
myform = MyModelFormClass(request.POST, instance=myinstance)
Without the instance, ModelForm doesn't know what row it's updating in the database. You would think that this is all present in the HTML so it shouldn't be necessary.. but that's not how Django works. You need to fetch the object existing from the database, and pass it to the ModelForm constructor along with the request.POST data. Then when you call myform.save() it will validate the form, merge its data with the existing object, and save the object. Using commit=False results in the last of those three steps being deferred, so that you can make any adjustments or checks to the updated instance before it is actually saved.

copy of a Django model instance along with a related One-to-One field

How do I create a copy of a Django model instance along with a related One-to-One field?
Copying the model instance works fine, but when I try to create a copy of the one-to-one model, all the fields of that model become blank. Here's what I did:
new_address = self.object.designer.address # type Address
new_address.pk = None
new_address.save()
new_contact = self.object.designer # type Contact
new_contact.pk = None
new_contact.address = new_address
new_contact.save()
self.object.shippinginfo.contact = new_contact
self.object.shippinginfo.save()
The Contact model has a one-to-one relationship with the Address model. I tried printing out the values, after creating the new address, the values were correct when I printed them out, but then when I save the address to the address field of the new contact, all of the fields of the address are blank except the pk...
To answer your direct question, you may want to use the save(force_insert=True) function and see if that fixes it. I would also check what you get if you call Contact.objects.all().count() and the same for Address, so you can ensure you are adding new records.
That said, I personally will recommend against what you are trying to do, which in my book, is a hack. Instead, just write the few extra lines of code and properly call the Adress.objects.create() and Contact.objects.create with the fields set from the other records. e.g.
old_address = self.object.designer.address
new_address = Address.objects.create(line1=old_adress.line1, line2=old_address.line2, etc)
Or even better, use an AddressManager:
class AddressManager(models.Manager):
def create_copy(self, obj):
address = self.create(line1=obj.line1, etc.)
return address
class ContactManager(models.Manager):
def create_copy(self, obj):
new_address = Address.objects.create_copy(obj.address)
contact = self.create(name=obj.name, address=new_address, etc.)
return contact
new_contact = Contact.objects.create_copy(old_contact)
Hope this helps.
I think you're not clear about how to define relationship. If Contact model has one to one relationship with Address model, then one object of Contact class can be related to one object of Address model. It'll be defined as:
class Contact(models.Model):
# field_one = ...
# field_two = ...
# and so on...
class Address(models.Model):
contact = OneToOneField(Contact)
# and more model fields...
This way you can associate one Contact object to one Address object. If you want to have more than one address for one contact then you should use ForeignKey.
For one Contact object having related to many Address instances, you can define relationship as:
class Contact(models.Model):
# field_one = ...
# field_two = ...
# and so on...
class Address(models.Model):
contact = ForeignKey(Contact)
# and more fields...
But here an Address object can be associated to a particular Contact object only.
You can read about Many To Many realtionship here.
And you don't have to initialize pk field as it is automatically updated/added.

Categories