#3871 closed New feature (fixed)
Use custom managers in reverse relations
| Reported by: | EspenG | Owned by: | v1v3kn |
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | dev |
| Severity: | Normal | Keywords: | |
| Cc: | paolo@…, espen@…, v1v3kn, Sebastian Goll, r.dav.lc@…, daevaorn@…, Stephane "Twidi" Angel, jdunck@…, loic@… | Triage Stage: | Ready for checkin |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
This is my case:
I got a Article model with two managers, the default "objects" and a custom one "published_objects". My Article model also got a foreign key to a Category model.
From the Category model I'm then able to write a_category.article_set.do_something(). The problem is then that this only allows me to use the default manager "objects", and not "published_objects".
More general:
It's not possible to override witch manager to use using reverse relations like foo.xxx_set
Possible syntax's for doing this:
On IRC someone came up with foo.xxx_set_manager1.all(), foo.xxx_set_manager2.all(), that is foo.xxx.set_<manager_name>.all().
One other way I came up with is using foo.xxx_set for the default manager, and then if you want to access a custom manager you could do something like this: foo.xxx_set.managers.my_custom_manager.all().
Others?
Hopefully someone got a great ide on how this should be done.
Attachments (6)
Change History (46)
comment:1 by , 19 years ago
| Cc: | added |
|---|
comment:2 by , 19 years ago
| Cc: | added |
|---|
comment:3 by , 19 years ago
| Cc: | added |
|---|
comment:4 by , 19 years ago
| Has patch: | set |
|---|---|
| Needs documentation: | set |
| Needs tests: | set |
I have made a patch witch fixes this ticket and it got 100% backwards compatibility.
Let's say you got two managers in the related model (remote), objects and test_objects, you can access them remotely like this: foo.remote_setobjects and foo.remote_settest_objects, while foo.remote_set stil uses the default manager.
Please test the code, and if this is the way you guys like it I will also write documentation and tests.
Espen
by , 19 years ago
comment:6 by , 19 years ago
| Has patch: | unset |
|---|---|
| Patch needs improvement: | set |
comment:7 by , 19 years ago
Ok, now I have been working on this ticket for a while and I have found a big problem. The patch itself works fine _some_ times, and I think I have figured out why it only works some times and not always.
At first I could not understand why everything worked when I tried to to what the patch does manually in the python shell but only sometimes when I used the patch on my system. Then I noticed that it worked if I rearranged the order the different models where in it worked. This then led me to the theory that in order for me to parse the different models (to find managers) i first have to wait until all the other initializing of the models are done, though I'm not a 100% sure about this. It would be nice if someone could confirm this.
Espen
comment:8 by , 19 years ago
| Triage Stage: | Unreviewed → Design decision needed |
|---|
Marked as Design Dec. Needed.
Espen - please raise this issue on Django-developers, you're more likely to get a response there :)
--Simon
comment:9 by , 18 years ago
| Component: | Core framework → Database wrapper |
|---|
comment:10 by , 15 years ago
| Triage Stage: | Design decision needed → Accepted |
|---|
Accepted on principle, but needs a cleaner API. EspenG's proposal is close; however, rather than seeing managers acquire an attribute for each of the model's manager, it would be better for a manager to have a method that allows it to return a different manager on the model - e.g., Author.article_set.manager('published') would give you the 'published' manager for Articles.
comment:11 by , 15 years ago
| Severity: | → Normal |
|---|---|
| Type: | → New feature |
by , 14 years ago
| Attachment: | 3871-patch.diff added |
|---|
Patch that fixes this by letting the user choose the manager by a .managers() function.
comment:12 by , 14 years ago
| Easy pickings: | unset |
|---|---|
| Has patch: | set |
| Needs tests: | unset |
| Owner: | changed from to |
| Status: | new → assigned |
| UI/UX: | unset |
Added a patch that provides this functionality, based on Russell's suggestion,
It allows you to do
#author is an Author object instance
author.article_set.managers('published_objects') # returns the published_objects manager instead of the default manager
comment:13 by , 14 years ago
| Cc: | added |
|---|---|
| Needs documentation: | unset |
| Patch needs improvement: | unset |
comment:14 by , 14 years ago
| Cc: | added |
|---|
I just ran into this issue described here myself (very closely related to the given introductory example, though in my case it's public events versus all events, possibly including private ones) which I want to access in the reverse …_set relation.
So I'd love to see these additions go to trunk. How are the chances of this happening in time for Django 1.4?
comment:15 by , 14 years ago
| Cc: | removed |
|---|
comment:16 by , 14 years ago
| Cc: | added |
|---|
comment:17 by , 14 years ago
What is the current status on this ticket? The deadline for non-trivial new features for 1.4 is the upcoming weekend (Dec 17–18). To me, the latest patch in this ticket seems essentially complete – it has documentation and tests, plus the actual changes to the code are not terribly large nor convoluted.
What is the next step in getting a developer involved into reviewing this and check-in to trunk?
comment:18 by , 14 years ago
As explained in the contributing guide, at this point, this ticket needs to be reviewed (by anyone) and marked as ready for checkin.
comment:19 by , 14 years ago
Thanks for the hint on the contributing guide. I wasn't aware that everybody could (and should) mark appropriate patches as ready for check-in. During my review I noticed that the latest patch in this ticket (3871-patch-with-docs.diff) doesn't apply cleanly to trunk. Also, I think the implementation, by monkey-patching the managers() method to the related managers, is not ideal.
Therefore, I wrote an alternative patch which I am attaching. The key differences to the last patch are:
- Method name
manager()instead ofmanagers(). Seems more appropriate for selecting a single manager (also, suggested by russellm). - Checks in place for disallowing the
remove()andclear()methods where they might show unexpected—and potentially dangerous—behavior, i.e. data loss. Namely, we do not check (and cannot do so without performance penalty database-wise) if the selected objects for removal are actually within the domain of the related manager. The only exception to this isclear()on a simple reverse foreign-key manager, which works as expected. - Structure of the implementation.
manager()is defined in the [Many]RelatedManagerclass alongside all other methods such asadd(),remove()etc. No monkey-patching necessary. - More elaborate tests. All three kinds of relevant related managers are tested: reverse foreign-key, reverse many-to-many, and forward many-to-many. Also additional tests for checking for unknown managers, and availability and behavior of the
remove()andclear()methods.
Since I wrote the new patch, I cannot myself review it. Thus, I ask for somebody else to take a look at it. The test suite passes, so there should be no regressions. All changes are backwards-compatible: the only addition is the manager() method.
comment:20 by , 14 years ago
| Cc: | added |
|---|
follow-up: 22 comment:21 by , 14 years ago
| Patch needs improvement: | set |
|---|
This patch no longer applies
follow-up: 23 comment:22 by , 14 years ago
| Patch needs improvement: | unset |
|---|
Replying to myusuf3:
This patch no longer applies
What exactly do you mean by that? The latest patch r17204-custom-reverse-managers.diff still applies perfectly cleanly to trunk, as of r17575:
$ svn up At revision 17575. $ patch -p0 <r17204-custom-reverse-managers.diff patching file docs/ref/models/relations.txt patching file django/db/models/fields/related.py patching file tests/modeltests/custom_managers/tests.py patching file tests/modeltests/custom_managers/models.py
comment:23 by , 14 years ago
Replying to sebastian:
Replying to myusuf3:
This patch no longer applies
What exactly do you mean by that? The latest patch
r17204-custom-reverse-managers.diffstill applies perfectly cleanly to trunk, as of r17575:
$ svn up At revision 17575. $ patch -p0 <r17204-custom-reverse-managers.diff patching file docs/ref/models/relations.txt patching file django/db/models/fields/related.py patching file tests/modeltests/custom_managers/tests.py patching file tests/modeltests/custom_managers/models.py
sorry I was using the wrong -p level.
comment:24 by , 14 years ago
| Cc: | added |
|---|
comment:25 by , 13 years ago
| Cc: | added |
|---|
comment:26 by , 13 years ago
What's the scoop here? It obviously didn't make it into 1.4 but it seems like a clear target for 1.5. This is a pretty basic thing to want to do with Django relations; I was surprised when I discovered it didn't exist.
comment:28 by , 13 years ago
I would go ahead and update the patch to mention the feature was added in 1.5 but I think it's feature frozen.
comment:31 by , 12 years ago
The patch in r17204-custom-reverse-managers.diff uses MyModel.m2mfield.manager('somemanager') while the call based syntax is MyModel.m2mfield('somemanager'). I am not sure which one is better. Opinions?
I think either the __call__ or .manager() based syntax should be adopted. Some solution would be welcome to this long standing issue.
comment:33 by , 12 years ago
| Triage Stage: | Accepted → Ready for checkin |
|---|
comment:34 by , 12 years ago
So, latest & ready for checkin idea is to go with MyModel.somerelation(manager='somemanager').filter... syntax. Last call for API vetoes. I'll try to get this into master tonight.
comment:35 by , 12 years ago
| Resolution: | → fixed |
|---|---|
| Status: | assigned → closed |
Seems like trac didn't pick up the commit. I merged this in 04a2a6b0f9cb6bb98edfe84bf4361216d60a4e38.
comment:36 by , 12 years ago
Since this was committed, I get "KeyError: 'manager'" at django/db/models/fields/related.py:379 when accessing a reverse relation from a template.
I have bisected the issue to 04a2a6b0f9cb6bb98edfe84bf4361216d60a4e38.
Full traceback:
Traceback (most recent call last):
File "/home/.../virtualenv/lib/python3.3/site-packages/waitress/channel.py", line 332, in service
task.service()
File "/home/.../virtualenv/lib/python3.3/site-packages/waitress/task.py", line 173, in service
self.execute()
File "/home/.../virtualenv/lib/python3.3/site-packages/waitress/task.py", line 388, in execute
app_iter = self.channel.server.application(env, start_response)
File "/home/.../django/django/core/handlers/wsgi.py", line 206, in __call__
response = self.get_response(request)
File "/home/.../django/django/core/handlers/base.py", line 196, in get_response
response = self.handle_uncaught_exception(request, resolver, sys.exc_info())
File "/home/.../django/django/core/handlers/base.py", line 114, in get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/home/.../django/django/db/transaction.py", line 360, in inner
return func(*args, **kwargs)
File "/home/.../...py", line 61, in ...
return render(request, '....html', {'...': ...})
File "/home/.../django/django/shortcuts/__init__.py", line 45, in render
return HttpResponse(loader.render_to_string(*args, **kwargs),
File "/home/.../django/django/template/loader.py", line 169, in render_to_string
return t.render(context_instance)
File "/home/.../django/django/template/base.py", line 141, in render
return self._render(context)
File "/home/.../django/django/template/base.py", line 135, in _render
return self.nodelist.render(context)
File "/home/.../django/django/template/base.py", line 830, in render
bit = self.render_node(node, context)
File "/home/.../django/django/template/base.py", line 844, in render_node
return node.render(context)
File "/home/.../django/django/template/loader_tags.py", line 122, in render
return compiled_parent._render(context)
File "/home/.../django/django/template/base.py", line 135, in _render
return self.nodelist.render(context)
File "/home/.../django/django/template/base.py", line 830, in render
bit = self.render_node(node, context)
File "/home/.../django/django/template/base.py", line 844, in render_node
return node.render(context)
File "/home/.../django/django/template/loader_tags.py", line 62, in render
result = block.nodelist.render(context)
File "/home/.../django/django/template/base.py", line 830, in render
bit = self.render_node(node, context)
File "/home/.../django/django/template/base.py", line 844, in render_node
return node.render(context)
File "/home/.../django/django/template/defaulttags.py", line 507, in render
six.iteritems(self.extra_context))
File "/home/.../django/django/template/defaulttags.py", line 506, in <genexpr>
values = dict((key, val.resolve(context)) for key, val in
File "/home/.../django/django/template/base.py", line 586, in resolve
obj = self.var.resolve(context)
File "/home/.../django/django/template/base.py", line 722, in resolve
value = self._resolve_lookup(context)
File "/home/.../django/django/template/base.py", line 776, in _resolve_lookup
current = current()
File "/home/.../django/django/db/models/fields/related.py", line 379, in __call__
manager = getattr(self.model, kwargs.pop('manager'))
comment:37 by , 12 years ago
| Resolution: | fixed |
|---|---|
| Status: | closed → new |
comment:39 by , 12 years ago
| Resolution: | → fixed |
|---|---|
| Status: | new → closed |
I believe someone also came up with a suggestion of selecting the Manager for a given ForeignKey relationship via the FK definition itself - e.g.
foo = ForeignKey(Foo,manager=Foo.manager2). I'm thinking something "clever" likeForeignKey(Foo.manager2)would be nicely succinct and could preserve backwards compatibility by assuming the default Manager if you just pass in theFooclass.However, that sort of syntax/implementation then locks you into a single manager; while that's really no worse than how things currently stand, the
foo.xxx_set_<manager_name>version would be more flexible, if more verbose. Ideally there'd be a way to get the best of both worlds.