Access model instance inside model field - python

I have a model (Event) that has a ForeignKey to the User model (the owner of the Event).
This User can invite other Users, using the following ManyToManyField:
invites = models.ManyToManyField(
User, related_name="invited_users",
verbose_name=_("Invited Users"), blank=True
)
This invite field generates a simple table, containing the ID, event_id and user_id.
In case the Event owner deletes his profile, I don't want the Event to be deleted, but instead to pass the ownership to the first user that was invited.
So I came up with this function:
def get_new_owner():
try:
invited_users = Event.objects.get(id=id).invites.order_by("-id").filter(is_active=True)
if invited_users.exists():
return invited_users.first()
else:
Event.objects.get(id=id).delete()
except ObjectDoesNotExist:
pass
This finds the Event instance, and returns the active invited users ordered by the Invite table ID, so I can get the first item of this queryset, which corresponds to the first user invited.
In order to run the function when a User gets deleted, I used on_delete=models.SET:
owner = models.ForeignKey(User, related_name='evemt_owner', verbose_name=_("Owner"), on_delete=models.SET(get_new_owner()))
Then I ran into some problems:
It can't access the ID of the field I'm passing
I could'n find a way to use it as a classmethod or something, so I had to put the function above the model. Obviously this meant that it could no longer access the class below it, so I tried to pass the Event model as a parameter of the function, but could not make it work.
Any ideas?

First we can define a strategy for the Owner field that will call the function with the object that has been updated. We can define such deletion, for example in the <i.app_name/deletion.py file:
# app_name/deletion.py
def SET_WITH(value):
if callable(value):
def set_with_delete(collector, field, sub_objs, using):
for obj in sub_objs:
collector.add_field_update(field, value(obj), [obj])
else:
def set_with_delete(collector, field, sub_objs, using):
collector.add_field_update(field, value, sub_objs)
set_with_delete.deconstruct = lambda: ('app_name.SET_WITH', (value,), {})
return set_with_delete
You should pass a callable to SET, not call the function, so you implement this as:
from django.conf import settings
from django.db.models import Q
from app_name.deletion import SET_WITH
def get_new_owner(event):
invited_users = event.invites.order_by(
'eventinvites__id'
).filter(~Q(pk=event.owner_id), is_active=True).first()
if invited_users is not None:
return invited_users
else:
event.delete()
class Event(models.Model):
# …
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
related_name='owned_events',
verbose_name=_('Owner'),
on_delete=models.SET_WITH(get_new_owner)
)
Here we thus will look at the invites to find a user to transfer the object to. Perhaps you need to exclude the current .owner of the event in your get_new_owner from the collection of .inivites.
We can, as #AbdulAzizBarkat says, better work with a CASCADE than explicitly delete the Event object , since that will avoid infinite recursion where an User delete triggers an Event delete that might trigger a User delete: at the moment this is not possible, but later if extra logic is implemented one might end up in such case. In that case we can work with:
from django.db.models import CASCADE
def SET_WITH(value):
if callable(value):
def set_with_delete(collector, field, sub_objs, using):
for obj in sub_objs:
val = value(obj)
if val is None:
CASCADE(collector, field, [obj], using)
else:
collector.add_field_update(field, val, [obj])
else:
def set_with_delete(collector, field, sub_objs, using):
collector.add_field_update(field, value, sub_objs)
set_with_delete.deconstruct = lambda: ('app_name.SET_WITH', (value,), {})
return set_with_delete
and rewrite the get_new_owner to:
def get_new_owner(event):
invited_users = event.invites.order_by(
'eventinvites__id'
).filter(~Q(pk=event.owner_id), is_active=True).first()
if invited_users is not None:
return invited_users
else: # strictly speaking not necessary, but explicit over implicit
return None

Related

Cannot assign "'somedata'": "otherdatal" must be a "" instance.`

Is it possible to insert the session value to the foreign key?
I have 2 models
class candidate(models.Model):
fname=models.CharField("First name ",max_length=20,default="")
lname=models.CharField("Last name ",max_length=20,default="")
email=models.EmailField("Email ",max_length=254,primary_key=True)
password=models.CharField("Password ",max_length=100,default="")
def __str__(self):
return self.email #self.fname+" " +self.lname
class canDetails(models.Model):
candEmail=models.ForeignKey(candidate,on_delete=models.CASCADE)
location=models.CharField("location ",max_length=30)
role=models.CharField("role ",max_length=20)
cv=models.FileField(upload_to="media/canDetails/")
def __str__(self):
return self.candEmail
I am taking the email from above model as a session and trying to put this session value to the foreign key field of the other model, but here I am getting an error like:
Cannot assign "'cb#gmail.com'": "canDetails.candEmail" must be a "candidate" instance.
I am trying to get all the details from candidate model and candDetails model at once thats why i using pf and fk here,so is it the right way i am following...?
how can i deal with this ? any suggestions pls.?
The model canDetails needs to know to which candidate it belongs, so the Foreingkey field needs to be an candidate instance (in the database the PK value is stored). I strongly suggest, just like the Django docs also suggest, naming the field candidate and capitalizing the model names, for more clarity.
class Candidate(models.Model):
...
class CanDetails(models.Model):
candidate = models.ForeignKey(Candidate, on_delete=models.CASCADE)
...
If you only have the email, then you can use that to look up which Candidate it belongs to, fetch that instance and assign it to the CanDetails. For example:
try:
the_candidate = Candidate.objects.get(email='cb#gmail.com')
except (Candidate.DoesNotExist, Candidate.MultipleObjectsReturned):
# raise error or exit
return
det = CanDetails()
det.candidate = the_candidate
...
det.save()

django related field exists after delete

I've an Order model and others models which related with it. An user can delete any of this items and I must perform a check if the order is empty after deletion and set as active False in case true. Some basic code to ilustrate it
class Order(models.Model):
paid = models.BooleanField(default=False)
active = models.BooleanField(default=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL)
def empty_order():
"""
I must implement it
"""
class HomeOrder(models.Model):
...
order = models.OneToOneField(Order, related_name='primary_home')
class TourOrder(models.Model):
...
order = models.ForeignKey(Order, related_name='tours')
I have a post_delete signals that are connected with every of this Models related to Order:
post_delete.connect(delete_order_if_empty, sender=HomeOrder)
post_delete.connect(delete_order_if_empty, sender=TourOrder)
def delete_order_if_empty(sender, instance, **kwargs):
if instance.order.empty_order():
instance.order.active = False
instance.order.save()
An Order can have one Home, so if the Home exists I can do order.primary_home, if Home does not exist it will raise an AttributeError because it is an OneToOne relationship.
An Order can have many Tours, so in the empty_order method I thought to do some checks as following.
def empty_order():
home = hasattr(self, 'primary_home') # Avoid AttributeError exception
tours = self.tours.exists()
this_order_has_something = primary_home or tours
return not this_order_has_something
Now, when I delete an HomeOrder the signal is raised but the empty_method never realized that this HomeOrder does not exists any more. Example:
>>>o=Order.objects.create(...)
>>>o.primary_home # raise AttributeError
>>>h=HomeOrder.objetcs.create(order=o, ...)
>>>o.primary_home # <HomeOrder: home-xx>
>>>h.delete()
>>>o.primary_home # still <HomeOrder: home-xx> Why?
>>>o.refresh_from_db()
>>>o.primary_home # again <HomeOrder: home-xx>

Django get() returned more than one

I have this error, how can I fix this?
get() returned more than one Event -- it returned 2!
Can you guys help me understand what that means and maybe tell me in advance how to avoid this error in future?
MODEL
class Event (models.Model):
name = models.CharField(max_length=100)
date = models.DateField(default='')
dicript = models.CharField(max_length=50, default='Описание отсутствует')
category = models.ForeignKey(Category,on_delete=models.CASCADE)
adress = models.TextField(max_length=300)
user = models.ForeignKey(User,related_name="creator",null=True)
subs = models.ManyToManyField(User, related_name='subs',blank=True)
#classmethod
def make_sub(cls, this_user, sub_event):
event, created = cls.objects.get_or_create(
user=this_user
)
sub_event.subs.add(this_user)
VIEWS
def cards_detail (request,pk=None):
# if pk:
event_detail = Event.objects.get(pk=pk)
subs = event_detail.subs.count()
# else:
# return CardsView()
args = {'event_detail':event_detail,'subs':subs}
return render(request,'events/cards_detail.html',args)
class CardsView (TemplateView):`
template_name = 'events/cards.html'
def get (self,request):
events = Event.objects.all()
return render(request,self.template_name,{'events':events })
def subs_to_event (request,pk=None):
event = Event.objects.filter(pk=pk)
Event.make_sub(request.user,event)
return redirect('events:cards')
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
try:
instance = Instance.objects.get(name=name)
except (ObjectDoesNotExist, MultipleObjectsReturned):
pass
get() raises MultipleObjectsReturned if more than one object was found,more info here.
the error is cause by event_detail = Event.objects.get(pk=pk), check your event pk is unique.
Basically, the cls object is getting more than one value on the get part of the 'get_or_create()'. get() returns only a single object whereas filter returns a dict(ish).
Put it in a try/except instead. So you'll have:
try:
event, created = cls.objects.get_or_create(
user=this_user
)
except cls.MultipleObjectsReturned:
event = cls.objects.filter(user=this_user).order_by('id').first()
This way if multiple objects are found, it handles the exception and changes the query to a filter to receive the multiple object queryset. No need to catch the Object.DoesNotExist as the create part creates a new object if no record is found.
I also face the same error:
get() returned more than one -- it returned 4!
The error was that I forgot to make a migration for the newly added fields in the model.

How to update DjangoItem in Scrapy

I've been working with Scrapy but run into a bit of a problem.
DjangoItem has a save method to persist items using the Django ORM. This is great, except that if I run a scraper multiple times, new items will be created in the database even though I may just want to update a previous value.
After looking at the documentation and source code, I don't see any means to update existing items.
I know that I could call out to the ORM to see if an item exists and update it, but it would mean calling out to the database for every single object and then again to save the item.
How can I update items if they already exist?
Unfortunately, the best way that I found to accomplish this is to do exactly what was stated: Check if the item exists in the database using django_model.objects.get, then update it if it does.
In my settings file, I added the new pipeline:
ITEM_PIPELINES = {
# ...
# Last pipeline, because further changes won't be saved.
'apps.scrapy.pipelines.ItemPersistencePipeline': 999
}
I created some helper methods to handle the work of creating the item model, and creating a new one if necessary:
def item_to_model(item):
model_class = getattr(item, 'django_model')
if not model_class:
raise TypeError("Item is not a `DjangoItem` or is misconfigured")
return item.instance
def get_or_create(model):
model_class = type(model)
created = False
# Normally, we would use `get_or_create`. However, `get_or_create` would
# match all properties of an object (i.e. create a new object
# anytime it changed) rather than update an existing object.
#
# Instead, we do the two steps separately
try:
# We have no unique identifier at the moment; use the name for now.
obj = model_class.objects.get(name=model.name)
except model_class.DoesNotExist:
created = True
obj = model # DjangoItem created a model for us.
return (obj, created)
def update_model(destination, source, commit=True):
pk = destination.pk
source_dict = model_to_dict(source)
for (key, value) in source_dict.items():
setattr(destination, key, value)
setattr(destination, 'pk', pk)
if commit:
destination.save()
return destination
Then, the final pipeline is fairly straightforward:
class ItemPersistencePipeline(object):
def process_item(self, item, spider):
try:
item_model = item_to_model(item)
except TypeError:
return item
model, created = get_or_create(item_model)
update_model(model, item_model)
return item
I think it could be done more simply with
class DjangoSavePipeline(object):
def process_item(self, item, spider):
try:
product = Product.objects.get(myunique_id=item['myunique_id'])
# Already exists, just update it
instance = item.save(commit=False)
instance.pk = product.pk
except Product.DoesNotExist:
pass
item.save()
return item
Assuming your django model has some unique id from the scraped data, such as a product id, and here assuming your Django model is called Product.
for related models with foreignkeys
def update_model(destination, source, commit=True):
pk = destination.pk
source_fields = fields_for_model(source)
for key in source_fields.keys():
setattr(destination, key, getattr(source, key))
setattr(destination, 'pk', pk)
if commit:
destination.save()
return destination

django: exclude certain form elements based on a condition

I have some form fields that I want to include/exclude based on whether or not a certain condition is met. I know how to include and exclude form elements, but I am having difficulty doing it when I want it elements to show based on the outcome of a function.
Here is my form:
class ProfileForm(ModelForm):
# this_team = get Team instance from team.id passed in
# how?
def draft_unlocked(self):
teams = Team.objects.order_by('total_points')
count = 0
for team in teams:
if team.pk == this_team.pk:
break
count += 1
now = datetime.datetime.now().weekday()
if now >= count:
# show driver_one, driver_two, driver_three
else:
# do not show driver_one, driver_two, driver_three
class Meta:
model = Team
What I am trying to accomplish is, based on the standings of total points, a team should not be able to change their driver until their specified day. As in, the last team in the standings can add/drop a driver on Monday, second to last team can add/drop on Tuesday, and so on...
So the first problem -- how do I get the Team instance inside the form itself from the id that was passed in. And, how do I include/exclude based on the result of draft_unlocked().
Or perhaps there is a better way to do all of this?
Thanks a lot everyone.
This is actually fairly straightforward (conditional field settings) - here's a quick example:
from django.forms import Modelform
from django.forms.widgets import HiddenInput
class SomeForm(ModelForm):
def __init__(self, *args, **kwargs):
# call constructor to set up the fields. If you don't do this
# first you can't modify fields.
super(SomeForm, self).__init__(*args, **kwargs)
try:
# make somefunc return something True
# if you can change the driver.
# might make sense in a model?
can_change_driver = self.instance.somefunc()
except AttributeError:
# unbound form, what do you want to do here?
can_change_driver = True # for example?
# if the driver can't be changed, use a input=hidden
# input field.
if not can_change_driver:
self.fields["Drivers"].widget = HiddenInput()
class Meta:
model = SomeModel
So, key points from this:
self.instance represents the bound object, if the form is bound. I believe it is passed in as a named argument, therefore in kwargs, which the parent constructor uses to create self.instance.
You can modify the field properties after you've called the parent constructor.
widgets are how forms are displayed. HiddenInput basically means <input type="hidden" .../>.
There is one limitation; I can tamper with the input to change a value if I modify the submitted POST/GET data. If you don't want this to happen, something to consider is overriding the form's validation (clean()) method. Remember, everything in Django is just objects, which means you can actually modify class objects and add data to them at random (it won't be persisted though). So in your __init__ you could:
self.instance.olddrivers = instance.drivers.all()
Then in your clean method for said form:
def clean(self):
# validate parent. Do this first because this method
# will transform field values into model field values.
# i.e. instance will reflect the form changes.
super(SomeForm, self).clean()
# can we modify drivers?
can_change_driver = self.instance.somefunc()
# either we can change the driver, or if not, we require
# that the two lists are, when sorted, equal (to allow for
# potential non equal ordering of identical elements).
# Wrapped code here for niceness
if (can_change_driver or
(sorted(self.instance.drivers.all()) ==
sorted(self.instance.olddrivers))):
return True
else:
raise ValidationError() # customise this to your liking.
You can do what you need by adding your own init where you can pass in the id when you instantiate the form class:
class ProfileForm(ModelForm):
def __init__(self, team_id, *args, **kwargs):
super(ProfileForm, self).__init__(*args, **kwargs)
this_team = Team.objects.get(pk=team_id)
teams = Team.objects.order_by('total_points')
count = 0
for team in teams:
if team.pk == this_team.pk:
break
count += 1
now = datetime.datetime.now().weekday()
if now >= count:
# show driver_one, driver_two, driver_three
else:
# do not show driver_one, driver_two, driver_three
class Meta:
model = Team
#views.py
def my_view(request, team_id):
profile_form = ProfileForm(team_id, request.POST or None)
#more code here
Hope that helps you out.

Categories

Resources