Using the django-tagging app as an example, I would like to change the manager of the Tag model so that I can replace it with an extended one:
# mytagging/models.py
from django.db import models
from tagging.models import TagManager, Tag
class MyTagManager(TagManager):
def update_tags(self, obj, tag_names):
# My actions
return super(MyTagManager, self).update_tags(obj, tag_names)
def add_tag(self, obj, tag_name):
# My actions
return super(MyTagManager, self).add_tag(obj, tag_name)
Tag.objects = MyTagManager
Now, Tag.objects = MyTagManager doesn't work, nor did I expect it to, but it illustrates what I would like to accomplish. I could very well create class MyTag(Tag) and set the manager that way, but then it would seem that I would also have to extend every other class that uses Tag and change it to MyTag.
I tried Tag.objects = MyTagManager() to initialize the class, but I get the error 'NoneType' object has no attribute '_meta' from a query object, it appears.
The above code renders this error when calling Tag.objects.update_tags(kwargs['instance'], tags) from admin page:
unbound method update_tags() must be called with MyTagManager instance
as first argument (got LibraryFile instance instead)
The LibraryFile model is the one I'm attempting to tag, and should therefore be the second argument instead of first (self being first).
Use proxy model with the different manager:
class MyTag(Tag):
objects = MyTagManager()
class Meta:
proxy = True
Using Secator's suggestion as part of the solution, I ended up just monkey-patching the Tag model itself where needed instead of the Tag.objects.
This being the final code:
from tagging.models import TagManager, Tag
import tagging
class MyTagManager(TagManager):
def update_tags(self, obj, tag_names):
# My actions
return super(MyTagManager, self).update_tags(obj, tag_names)
def add_tag(self, obj, tag_name):
# My actions
return super(MyTagManager, self).add_tag(obj, tag_name)
class MyTag(Tag):
objects = MyTagManager()
class Meta:
proxy = True
tagging.models.Tag = MyTag
tagging.fields.Tag = MyTag
Related
More generally speaking I want add a custom admin Panel to list some related content. To lookup this related content I need to pass the current instance of the model or at least its ID to this panel. How can I do that within these lists in which these admin panels are noted?
Here is my specific example of an ArtistPage. In the editor I would like to add a panel to list WorkPages that are related to this ArtistPage:
from wagtail.models import Page
class ArtistPage(Page):
# ...
content_panels = [
# ...
ListWorksPanel(artist=self), # This doesn’t work
]
The panel itself is defined like that, mostly copied from the HelpPanel:
from wagtail.admin.panels import Panel
class ListWorksPanel(Panel):
def __init__(self, artist="", template="admin/list_works_panel.html", **kwargs,):
super().__init__(**kwargs)
self.artist = artist
self.template = template
def clone_kwargs(self):
kwargs = super().clone_kwargs()
del kwargs["help_text"]
kwargs.update(
artist=self.artist,
template=self.template,
)
return kwargs
class BoundPanel(Panel.BoundPanel):
def __init__(self, panel, instance, request, form):
super().__init__(panel, instance, request, form)
self.template_name = self.panel.template
self.artist = self.panel.artist
This is more a general Python question, I think. I know how to pass "self" in functions. But how does that work here with this class as element of a list? I reckon that the __init__() method of the ArtistPage is the way to go, but I cannot figure out how exactly.
What is the pythonic way of passing "self" to another class?
Update (Solution):
Following #gasman’s aswer, I just added the get_context_data method to the BoundPanel class. The works are accessible in the template of the panel now!
class ListWorksPanel(Panel):
def __init__(self, artist="", template="admin/list_works_panel.html", **kwargs,):
super().__init__(**kwargs)
self.artist = artist
self.template = template
def clone_kwargs(self):
kwargs = super().clone_kwargs()
del kwargs["help_text"]
kwargs.update(
artist=self.artist,
template=self.template,
)
return kwargs
class BoundPanel(Panel.BoundPanel):
def __init__(self, panel, instance, request, form):
super().__init__(panel, instance, request, form)
self.template_name = self.panel.template
self.artist = self.panel.artist
def get_context_data(self, parent_context):
context = super().get_context_data(parent_context)
context['works'] = self.instance.works.all() # exactly what I needed
return context
The ArtistPage instance is passed to BoundPanel.__init__ as the keyword argument instance. All code that deals with an individual ArtistPage needs to be written inside the BoundPanel class.
When you write ListWorksPanel() as part of a content_panels definition, you're creating a ListWorksPanel instance that then becomes part of the definition of the ArtistPage class. At this point in the code, no actual instance of ArtistPage exists, so there's no self to refer to. Effectively, there's a single ListWorksPanel object shared by all ArtistPage instances that will ever be created.
When the time comes to render the edit form for an individual page, Wagtail calls get_bound_panel on the ListWorksPanel object, passing the page instance along with the form and request objects. (The full process is explained here.) This returns an instance of BoundPanel, which is a template component that performs the final rendering. In this case, you probably want to define a get_context_data method on BoundPanel that does something like context['works'] = self.instance.works.all() - this will then make the variable works available on the template.
I want to separate some logic from model and group it in a property, just like Django does with model managers (object property). I fact, something like ForeignKey but without database representation.
I have something like this:
class Remote(object):
def __init(self, *args, **kwargs)
self.post = ... # how to get to post instance?
def synchronize(self):
# this function requires Post object access
print(self.post.name)
class Post(models.Model):
name = models.CharField(max_length=100)
remote = Remote()
...
for post in Post.objects.all():
post.remote.synchronize()
Question
How to modify above code to get access to Post object in Remote object?
Additional question
Is it possible to determine if Remote object has been called from Post instance (post.remote... – like above) or Post class (Post.remote...)?
What you want here can be achieved with descriptors.
In order for it to work, you need to define a __get__ method in your class that you want to be accessible as an attribute of another class.
A simple example for your case will look like this:
class Remote:
def __init__(self, post)
self.post = post
def synchronize(self):
print(self.post.name)
class RemoteDescriptor:
def __get__(self, obj):
if not obj:
return self
remote = getattr(obj, '_remote', None)
if not remote:
remote = Remote(obj)
obj._remote = remote
return remote
class Post(models.Model):
name = models.CharField(max_length=100)
remote = RemoteDescriptor()
Explanation:
In the above code, every time you call an attribute remote of your Post model, __get__ method of the RemoteDescriptor will be invoked. First check for obj is to make sure that descriptor is called from other object, not directly. Two classes Remote and RemoteDescriptor are needed here in order for you to be able to add custom methods inside your descriptor accessible using dot (e.g. post.remote.calculate())
Note also that I am placing the instance of Remote to the dict of Post on first invokation and on all subsequent calls, object will be returned from there.
You should also check a great article on descriptors on RealPython.
I'm writing a website generator with various classes that represent the content in the webpages such as Page, NewsPost, Tag, Category etc.
I'd like to be able to construct these objects plainly, and I don't have a problem with that.
However, I'd also like to construct these objects within a certain context - say, the context of a website with a particular root URL. Let's say I put this context into an instance of a class ContentManager. This is the code I ultimately hope to end up with:
page = Page(title='Test Page', content='hello world!')
assert page.cm == None
cm = ContentManager(root_url='//localhost')
page = cm.Page(title='Test Page', content='hello world!')
assert page.cm == cm
I can easily manage this if page.cm is a per-instance property set in __init__, but I need to call class methods on cm.Page which need access to the cm object, so it has to be a static property.
If I just set it as a static property on the Page class, it would end up affecting other ContentManagers pages as well, which is not desirable.
How would I achieve this? Metaclasses? Or some sort of class factory function?
One solution could be creating a subclass of Page for every ContentManage instance:
class Page:
cm = None
def __init__(self, title, content):
self.title = title
self.content = content
class ContentManager:
def __init__(self, root_url):
class PerContentManagerPage(Page):
cm = self
self.Page = PerContentManagerPage
page0 = Page(title='Test Page', content='hello world!')
cm = ContentManager(root_url='//localhost')
page = cm.Page(title='Test Page', content='hello world!')
cm2 = ContentManager(root_url='//localhost')
page2 = cm2.Page(title='Test Page 2', content='hello world!')
assert page0.cm is None
assert page.cm == cm
assert page2.cm == cm2
In python a class is also an object (an instance of its metaclass). This solution creates a new subclass of Page every time you instantiate ContentManager. This means that the cm.Page class isn't the same as the cm2.Page class but both are the subclasses of Page. This is why it's possible that cm.Page.cm and cm2.Page.cm have different values, because these are two separate classes (or class objects).
Note: Although in python this could be solved by creating subclass objects dynamically, problems usually have better solutions. Creating classes/subclasses dynamically is a warning sign (HACK).
I'm still convinced that you shouldn't create a page subclass for each content manager instance. Instead I would simply use instances of the global ContentManager and Page classes by connecting them with references to each other in a suitable way and putting the data and the code into instance attributes/methods.
Setting everything else aside, you'll just need to dynamically construct a class to tie to each instance of ContentManager; we can do this using the built-in type function, which can either, with one argument, give us the type of an object, or, with three arguments (class name, base classes, and class dictionary) construct a new class.
Here's a sample of how that might look in your situation:
class Page(object):
# This is just a default value if we construct a Page
# outside the context of a ContentManager
cm = None
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
#classmethod
def do_class_thing(cls):
return cls.cm
class ContentManager(object):
def __init__(self, root_url):
self.url = root_url
"""
This is where the magic happens. We're telling type() to
construct a class, with the class name ContentManagerPage,
have it inherit from the above explicitly-declared Page
class, and then overriding its __dict__ such that the class
cm variable is set to be the ContentManager we're
constructing it from.
"""
self.Page = type(str('ContentManagerPage'), (Page,), {'cm': self})
Once you've got all this set up, it's simple enough to do exactly what you're trying to do, with cm as a class variable.
In my Django app I have several different forms, which are similar in style. To not repeat myself over and over again, I try to rewrite the default form settings.
As a start I wanted to set some default settings for every form I use in my app and tried to subclass the django.forms.Form:
class DefaultForm(forms.Form):
error_css_class = 'alert'
error_class = DivErrorList
required_css_class = 'required'
label_suffix = ':'
auto_id = True
class TechnicalSurveyForm(DefaultForm):
location = forms.CharField(label='GPS Location')
satellite = forms.ModelChoiceField(queryset=get_satellites(), empty_label=None)
modem_sn = forms.CharField()
In my views.py I would call the Form simply with
tsurvey = TechnicalSurveyForm()
Unfortunately, the settings I set in DefaultForm are not in place (when I use TechnicalSurvey(auto_id = True, error_class = DivErrorList) they are). So, I guess my approach is totally wrong in some way. Can someone please help me out?
I guess the __init__ of forms.Form initializes attributes of a Form. You need to override the __init__ method and change attributes after Django has done its stuff.
EDIT: Indeed, after checking the django source code, you can see that attributes of a form object are initialized in the __init__ function. The method is visible on the github of django.
class DefaultForm(forms.Form):
def __init__(self, *args, **kwargs):
super(forms.Form, self ).__init__(*args, **kwargs)
self.error_css_class = 'alert'
self.error_class = DivErrorList
self.required_css_class = 'required'
self.label_suffix = ':'
self.auto_id = True
For Python beginners
This behavior is totally normal. Every attributes with the same name declared at the class declaration (as in the author example) will be override if it's also defined in the init function. There's a slightly difference between these two types of attributes declaration.
I have a few model inheritance levels in Django:
class WorkAttachment(models.Model):
""" Abstract class that holds all fields that are required in each attachment """
work = models.ForeignKey(Work)
added = models.DateTimeField(default=datetime.datetime.now)
views = models.IntegerField(default=0)
class Meta:
abstract = True
class WorkAttachmentFileBased(WorkAttachment):
""" Another base class, but for file based attachments """
description = models.CharField(max_length=500, blank=True)
size = models.IntegerField(verbose_name=_('size in bytes'))
class Meta:
abstract = True
class WorkAttachmentPicture(WorkAttachmentFileBased):
""" Picture attached to work """
image = models.ImageField(upload_to='works/images', width_field='width', height_field='height')
width = models.IntegerField()
height = models.IntegerField()
There are many different models inherited from WorkAttachmentFileBased and WorkAttachment. I want to create a signal, which would update an attachment_count field for parent work, when attachment is created. It would be logical, to think that signal made for parent sender (WorkAttachment) would run for all inherited models too, but it does not. Here is my code:
#receiver(post_save, sender=WorkAttachment, dispatch_uid="att_post_save")
def update_attachment_count_on_save(sender, instance, **kwargs):
""" Update file count for work when attachment was saved."""
instance.work.attachment_count += 1
instance.work.save()
Is there a way to make this signal work for all models inherited from WorkAttachment?
Python 2.7, Django 1.4 pre-alpha
P.S. I've tried one of the solutions I found on the net, but it did not work for me.
You could register the connection handler without sender specified. And filter the needed models inside it.
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_save)
def my_handler(sender, **kwargs):
# Returns false if 'sender' is NOT a subclass of AbstractModel
if not issubclass(sender, AbstractModel):
return
...
Ref: https://groups.google.com/d/msg/django-users/E_u9pHIkiI0/YgzA1p8XaSMJ
The simplest solution is to not restrict on the sender, but to check in the signal handler whether the respective instance is a subclass:
#receiver(post_save)
def update_attachment_count_on_save(sender, instance, **kwargs):
if isinstance(instance, WorkAttachment):
...
However, this may incur a significant performance overhead as every time any model is saved, the above function is called.
I think I've found the most Django-way of doing this: Recent versions of Django suggest to connect signal handlers in a file called signals.py. Here's the necessary wiring code:
your_app/__init__.py:
default_app_config = 'your_app.apps.YourAppConfig'
your_app/apps.py:
import django.apps
class YourAppConfig(django.apps.AppConfig):
name = 'your_app'
def ready(self):
import your_app.signals
your_app/signals.py:
def get_subclasses(cls):
result = [cls]
classes_to_inspect = [cls]
while classes_to_inspect:
class_to_inspect = classes_to_inspect.pop()
for subclass in class_to_inspect.__subclasses__():
if subclass not in result:
result.append(subclass)
classes_to_inspect.append(subclass)
return result
def update_attachment_count_on_save(sender, instance, **kwargs):
instance.work.attachment_count += 1
instance.work.save()
for subclass in get_subclasses(WorkAttachment):
post_save.connect(update_attachment_count_on_save, subclass)
I think this works for all subclasses, because they will all be loaded by the time YourAppConfig.ready is called (and thus signals is imported).
You could try something like:
model_classes = [WorkAttachment, WorkAttachmentFileBased, WorkAttachmentPicture, ...]
def update_attachment_count_on_save(sender, instance, **kwargs):
instance.work.attachment_count += 1
instance.work.save()
for model_class in model_classes:
post_save.connect(update_attachment_count_on_save,
sender=model_class,
dispatch_uid="att_post_save_"+model_class.__name__)
(Disclaimer: I have not tested the above)
I just did this using python's (relatively) new __init_subclass__ method:
from django.db import models
def perform_on_save(*args, **kw):
print("Doing something important after saving.")
class ParentClass(models.Model):
class Meta:
abstract = True
#classmethod
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
models.signals.post_save.connect(perform_on_save, sender=cls)
class MySubclass(ParentClass):
pass # signal automatically gets connected.
This requires django 2.1 and python 3.6 or better. Note that the #classmethod line seems to be required when working with the django model and associated metaclass even though it's not required according to the official python docs.
post_save.connect(my_handler, ParentClass)
# connect all subclasses of base content item too
for subclass in ParentClass.__subclasses__():
post_save.connect(my_handler, subclass)
have a nice day!
Michael Herrmann's solution is definitively the most Django-way of doing this.
And yes it works for all subclasses as they are loaded at the ready() call.
I would like to contribute with the documentation references :
In practice, signal handlers are usually defined in a signals submodule of the application they relate to. Signal receivers are connected in the ready() method of your application configuration class. If you’re using the receiver() decorator, simply import the signals submodule inside ready().
https://docs.djangoproject.com/en/dev/topics/signals/#connecting-receiver-functions
And add a warning :
The ready() method may be executed more than once during testing, so you may want to guard your signals from duplication, especially if you’re planning to send them within tests.
https://docs.djangoproject.com/en/dev/topics/signals/#connecting-receiver-functions
So you might want to prevent duplicate signals with a dispatch_uid parameter on the connect function.
post_save.connect(my_callback, dispatch_uid="my_unique_identifier")
In this context I'll do :
for subclass in get_subclasses(WorkAttachment):
post_save.connect(update_attachment_count_on_save, subclass, dispatch_uid=subclass.__name__)
https://docs.djangoproject.com/en/dev/topics/signals/#preventing-duplicate-signals
This solution resolves the problem when not all modules imported into memory.
def inherited_receiver(signal, sender, **kwargs):
"""
Decorator connect receivers and all receiver's subclasses to signals.
#inherited_receiver(post_save, sender=MyModel)
def signal_receiver(sender, **kwargs):
...
"""
parent_cls = sender
def wrapper(func):
def childs_receiver(sender, **kw):
"""
the receiver detect that func will execute for child
(and same parent) classes only.
"""
child_cls = sender
if issubclass(child_cls, parent_cls):
func(sender=child_cls, **kw)
signal.connect(childs_receiver, **kwargs)
return childs_receiver
return wrapper
It's also possible to use content types to discover subclasses - assuming you have the base class and subclasses packaged in the same app. Something like this would work:
from django.contrib.contenttypes.models import ContentType
content_types = ContentType.objects.filter(app_label="your_app")
for content_type in content_types:
model = content_type.model_class()
post_save.connect(update_attachment_count_on_save, sender=model)
In addition to #clwainwright answer, I configured his answer to instead work for the m2m_changed signal. I had to post it as an answer for the code formatting to make sense:
#classmethod
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
for m2m_field in cls._meta.many_to_many:
if hasattr(cls, m2m_field.attname) and hasattr(getattr(cls, m2m_field.attname), 'through'):
models.signals.m2m_changed.connect(m2m_changed_receiver, weak=False, sender=getattr(cls, m2m_field.attname).through)
It does a couple of checks to ensure it doesn't break if anything changes in future Django versions.