I'm developing a legacy Django 1.7 system for a client. The programmers before me overrode the Member model (basically the User model) "objects" property with a filter query that removes anything with "is_deleted" set to "True". I've listed the snippets below:
Member class snippet:
class Member(AbstractUser):
objects = MemberManager()
all_objects = models.Manager()
MemberManager class snippet:
class MemberManager(BaseUserManager):
def get_queryset(self):
return super(MemberManager, self).get_queryset().filter(is_deleted=False)
Now when I try to update a user that has the is_deleted flag set to "True" it fails. Below is an example code snippet. Notice how I use "all_objects" which is the default models.Manager() that returns all records.
user = Member.all_objects.get(pk=id) # id of an is_deleted = True record
user.is_deleted = False
user.save()
This code causes this Django query to run which unfortunately has "is_deleted = 0" included in the WHERE clause, which causes it to not find the record. Below is what shows up in the logs:
UPDATE Member [[snip...]] WHERE (Member.is_deleted = 0 AND Member.id = 6)
Is there any way to call "save()" that will not use the MemberManager.objects get_queryset filter?
I think the problem stems from having MemberManager listed first. As the documentation says:
Take note that the first Manager Django encounters (in the order in which they’re defined in the model) has a special status. Django interprets the first Manager defined in a class as the "default" Manager, and several parts of Django will use that Manager exclusively for that model. As a result, it’s a good idea to be careful in your choice of default manager in order to avoid a situation where overriding get_queryset() results in an inability to retrieve objects you’d like to work with.
Reversing the order of objects and all_objects should fix the problem.
I tried Kevin Christopher Henry's answer and unfortunately changing the default manager messed up the authentication code, which needed to inherit from the "BaseUserManager" class. The solution was to create an "undelete" function in the Member model class that uses the "all_objects" property to undelete the user before it's saved.
class Member(AbstractUser):
objects = MemberManager() # default manager
all_objects = models.Manager()
def undelete(self):
if self.is_deleted:
Member.all_objects.filter(id=self.id).update(is_deleted=False)
Then in my code I did this:
user = Member.all_objects.get(pk=id)
user.undelete()
Related
I have a custom model defined as following, with get() method override
class CustomQuerySetManager(models.QuerySet):
def get(self, *args, **kwargs):
print('Using custom manager')
# some other logics here...
return super(CustomQuerySetManager, self).get(*args, **kwargs)
class CustomModel(models.Model):
objects = CustomQuerySetManager.as_manager()
class Meta:
abstract = True
Then I have two models defined as
class Company(CustomModel):
name = models.CharField(max_length=40)
class People(CustomModel):
company = models.ForeignKey(Company, on_delete=models.CASCADE)
first_name = models.CharField(max_length=20)
last_name = models.CharField(max_length=20)
If I use get() directly like People.objects.get(pk=1) then it works, "Using custom manager" gets printed, but if I try to get the foreign key info, django still uses the get() method from the default manager, nothing gets printed and the rest of logic defined won't get executed, for example
someone = People.objects.get(id=1) # prints Using custom manager, custom logic executed
company_name = someone.company.name # nothing gets printed, custom logic does not execute
Is the foreign key field in a model using a different manager even though the foreign key model is also using my custom model class? Is there a way to make my custom get() method work for all fields?
As django doc says
By default, Django uses an instance of the Model._base_manager manager
class when accessing related objects (i.e. choice.question), not the
_default_manager on the related object
See more here.
So you have to tell django model which manager to use as base manager, like this:
class CustomModel(models.Model):
objects = CustomQuerySetManager.as_manager()
class Meta:
#django will use your custom "objects" manager as base_manager
#or you may have different managers for base and default managers
#if you define two managers with different names
base_manager_name = 'objects'
abstract = True
But, please, pay attention that you do not filter away any results from base manager. Django doc says:
This manager is used to access objects that are related to from some
other model. In those situations, Django has to be able to see all the
objects for the model it is fetching, so that anything which is
referred to can be retrieved.
Therefore, do not override get_queryset() for this kind of managers.
See more here.
I want to override related manager for a class.
I have a Company model. It has state column which can be in ACTIVE, INACTIVE, SUSPENDED. I am adding 2 new states called SALES, CLOSED.
Since its legacy model, just adding state can be devastating(there are very many places in code which doesn't filter by state).
So, to avoid inadvertent changes elsewhere, I decided to hide our new states for all other apps/elsewhere unless required otherwise (I'll whitelist only for our app/models)
I've overridden object manager in company.
class CompanyManager(models.Manager):
def get_queryset(self):
return super(CompanyRelatedManager, self).get_queryset().exclude(state__in=['SALES', 'CLOSED'])
class Company(models.Model):
_default_manager = models.Manager()
objects = CompanyManager()
allObjects = models.Manager()
name = models.TextField()
...
salesContact = models.ForeignKey(Contact)
The problem is that Company.objects.filter(blah=blah) filters out new states. But something like salesContact.companies.all() doesn't.
In [9]: salesContact.companies
Out[9]: <django.db.models.fields.related.RelatedManager at 0x12157a990>
My question is how to override the related manager of the salesContact = models.ForeignKey(Contact) and the likes so that I can modify the default queryset to exclude my new state.
And, I can't override the default manager, since overriding default manager also implies that I am overriding the db_manager which results in un-intended consequence (db tries to insert instead of update, whole other story).
I have a django 1.6 app with the following (trimmed for clarity)
classes defined. User is the standard django.contrib.auth User class.
class Event(models.Model):
user = models.ForeignKey(User, related_name='events')
name = models.CharField(max_length=64)
class Profile(models.Model):
user = models.ForeignKey(User, related_name='aprofile')
class MemberProfile(Profile):
pass
Here are my admin classes:
class ProfileAdmin(ModelAdmin):
model = Profile
fields = ('user', )
class MemberProfileAdmin(ModelAdmin):
model = MemberProfile
fields = ('user', )
readonly_fields = ('user', )
What I'd like to do is display a read-only list of all events for a given member, or at least profile. Of course joining across the User foreign key seems like the way to go, but I am drawing a blank as to how to accomplish this. Here's a summary of attempts so far.
Define an inline admin on the Event class directly referencing the user field, and add it to the ProfileAdmin:
class EventInlineAdmin(TabularInline):
model = Event
fk_name = 'user' # Fails - fk_name 'user' is not a ForeignKey to <class 'solo365.solo_profile.models.profile.Profile'>
...well, no, it sure isn't. But our User has an 'aprofile' field, so...
class EventInlineAdmin(TabularInline):
model = Event
fk_name = 'user__aprofile' # Fails - EventInlineAdmin.fk_name' refers to field 'user__aprofile' that is missing from model 'admin_fk_test.Event'.
Ok, those fields look like they should sync up, but perhaps we need to be a little more aggressive:
class EventInlineAdmin(TabularInline):
model = Event
fk_name = 'user__aprofile__pk' # Fails - 'EventInlineAdmin.fk_name' refers to field 'user__aprofile__pk' that is missing from model 'admin_fk_test.Event'.
I've also tried messing with formfield_for_foreignkey() and friends in both the inline and the regular model admins, but without that fk_name having a valid value, those methods never get called.
I then considered trying to access the events field directly from a Profile's user:
class ProfileAdmin(ModelAdmin):
model = Profile
fields = ('user', 'user__events') # Fails - Unknown field(s) (user__events) specified for Profile. Check fields/fieldsets/exclude attributes of class ProfileAdmin.
What about with a custom formfield_for_foreignkey() method? Sadly that never gets called for anything other than the 'user' field. I've also considered a custom get_formsets() method, but frankly I'm not sure how I could use that without a working EventInlineAdmin.
I could of course define a custom field that simply concatenates all of the events and returns that as a string, but ideally I would prefer something like a fully-featured inline (even read-only) than just a chunk o' text. IOW such a custom field would have a method that (ideally) would return an inline form without requiring any sort of custom template, setting of allow_tags, etc.
Am I doomed to have to create a completely custom Form for the Profile admin class? Or is there a simple way to accomplish what I'm trying to do, that I'm just missing?
Update:
Bonus points if a provided solution works for the MemberProfileAdmin class, not just the ProfileAdmin class.
The relation between User and Profile should be a 1:1 relation which would allow the referencing via user__aprofile. Otherwise, the reverse relation of a foreing key is a queryset because one foreign key can be assigned to multiple instances. This is might be the reason why your code failed.
Change it to:
class Profile(models.Model):
user = models.OneToOneKey(User, related_name='aprofile')
This is a bit like using ForeignKey(unique=True).
To know the attributes, it might help to call dir(model_instance) on the model instance in question, or try around in the Django shell (./manage.py shell).
Also, I've experienced that it might be more confusing to assign a custom related_name like in your case where you would expect one profile by looking at the related name but you would actually get back a queryset.
The generated name in that case would be profile_set, and you would have to call profile_set.all() or profile_set.values() to get some actual profiles.
Here is my models.py:
class Item(models.Model):
# ... some irrelevent fields ...
tags = models.ManyToManyField('Tag')
class Tag(models.Model):
name = models.CharField(max_lenght=30)
category_id = models.IntegerField()
Tag is actually a general-purpose name. Each item has many different type of tags - currently there are four types: team tags, subject tags, admin tags and special tags. eventually there will probably be a few more.
The idea is, they all have basically the same fields, so instead of having like 4 tables with manytomany relationship, and instead of adding a new column for Item whenever adding a new type, everything is called a 'tag' and it's very easy to add new types without any change to the schema.
Now to handle this in the admin.py I'm using dyamically created proxy models (based on this), as such:
def create_modeladmin(modeladmin, model, name = None):
class Meta:
proxy = True
app_label = model._meta.app_label
attrs = {'__module__': '', 'Meta': Meta}
newmodel = type(name, (model,), attrs)
admin.site.register(newmodel, modeladmin)
return modeladmin
class TagAdmin(models.Model):
def queryset(self):
return self.model.objects.filter(category_id = self.cid)
class TeamAdmin(TagAdmin):
cid = 1
class SubjectAdmin(TagAdmin):
cid = 2
# ... and so on ...
create_modeladmin(TeamAdmin, name='Teams', model=Tag)
create_modeladmin(SubjectAdmin, name='Subject', model=Tag)
#... and so on ...
This works great for me. However, different staff members need different editing permissions - one guy shouldn't access admin tags, while the other should only have access to edit subject-tags and team-tags. But as far as the admin site is concerned - the dynamic models do not exist in the permission list, and I can't give anyone permissions regarding them.
i.e. a user given all permissions on the list will still not have access to edit any of the dynamic models, and the only way to let anyone access them at all is to give him a superuser which obviously defies the point
I searched SO and the web and I can't anyone with a similar problem, and the docs don't say anything about this not in the dynamic models section or the proxy models section. so I'm guessing this is a different kind of problem. Any help would be greatly appreciated
UPDATE
So after some research into it, the answer was simple enough. Since permissions in django are objects that are saved to the database, what I needed to do was simple - add the relevent permissions (and create new ContentType objects as well) to the db, and then I could give people specific pemissions.
However, this raised a new question - is it a good convention to put the function that creates the permissions inside create_modeladmin as a find_or_create sort of function (that basically runs every time) or should it be used as an external script that I should run once every time I add a new dynamic model (sort of like how syncdb does it)?
And is there a way to also create the permissions dynamically (which seems to me like the ideal and most fitting solution)?
of course you can create permissions, django have django.contrib.auth.management.create_permissions to do this
There must be a problem with super(InviteManager, self).get_query_set() here but I don't know what to use. When I look through the RelatedManager of a user instance,
len(Invite.objects.by_email()) == len(user.invite_set.by_email())
Even if the user does not have any invites. However, user.invite_set.all() correctly returns all of the Invite objects that are keyed to the User object.
class InviteManager(models.Manager):
"""with this we can get the honed querysets like user.invite_set.rejected"""
use_for_related_fields = True
def by_email(self):
return super(InviteManager, self).get_query_set().exclude(email='')
class Invite(models.Model):
"""an invitation from a user to an email address"""
user = models.ForeignKey('auth.User', related_name='invite_set')
email = models.TextField(blank=True)
objects = InviteManager()
'''
u.invite_set.by_email() returns everything that Invite.objects.by_email() does
u.invite_set.all() properly filters Invites and returns only those where user=u
'''
You may want a custom QuerySet that implements a by_email filter. See examples on Subclassing Django QuerySets.
class InviteQuerySet(models.query.QuerySet):
def by_email(self):
return self.exclude(email='')
class InviteManager(models.Manager):
def get_query_set(self):
model = models.get_model('invite', 'Invite')
return InviteQuerySet(model)
Try:
def by_email(self):
return super(InviteManager, self).exclude(email='')
If nothing else, the .get_query_set() is redundant. In this case, it may be returning a whole new queryset rather than refining the current one.
The documentation specifies that you should not filter the queryset using get_query_set() when you replace the default manager for related sets.
Do not filter away any results in this type of manager subclass
One reason an automatic manager is used is to access objects that are related to from some other model. In those situations, Django has to be able to see all the objects for the model it is fetching, so that anything which is referred to can be retrieved.
If you override the get_query_set() method and filter out any rows, Django will return incorrect results. Don’t do that. A manager that filters results in get_query_set() is not appropriate for use as an automatic manager.
Try using .all() in place of .get_query_set(). That seemed to do the trick for a similar problem I was having.
def by_email(self):
return super(InviteManager, self).all().exclude(email='')