Override method of a django builtin Class - django

Abstract of Question:
I am trying to extend (not replace) some method of any of django's builtin Class in my custom module which would allow to further extend the extended(overridden) method.
Lets say I have two custom modules mod1 and mod2. Want to override same method of some django's class say get_apps_list of AdminSite, In mod1 I want to add a line to say hello, in mod2 it should say hi.
Desired output:
There should be nothing if none of the modules installed,
it should say hi if mod1 installed and hello if mod2
installed.
And hi and hello if both installed
Question with real example:
Just for example, I need to modify the implementation of AdminSite.get_app_list like following
From
#app['models'].sort(key=lambda x: x['name'])
To
app['models'] = sort_with_name_length(app['models']) //my own method
Expected/desired approach: I supposed that it should be achievable by just writing following code in any of my custom module's models.py or sites.py file
class MyAdminSite(AdminSite):
def get_app_list(self, request):
app_list = super().get_app_list(request)
for app in app_list:
#app['models'].sort(key=lambda x: x['name'])
app['models'] = sort_with_name_length(app['models']) #an example change I need
return app_list
But what above code does is nothing, its never executed, until I use Monkey patching guided by this answer.
What I could achieve
from django.contrib.admin import AdminSite
class MyAdminSite(AdminSite):
def get_app_list(self, request):
# res = super(MyAdminSite, self).get_app_list(request) //gives following error
# super(type, obj): obj must be an instance or subtype of type
# So i have to rewrite complete method again in my module like following
app_dict = self._build_app_dict(request)
app_list = sorted(app_dict.values(), key=lambda x: x['name'].lower())
for app in app_list:
#app['models'].sort(key=lambda x: x['name'])
app['models'] = sort_with_name_length(app['models'])
return app_list
AdminSite.get_app_list = MyAdminSite.get_app_list
Problem being faced: Above does what I need in a totally undesired way. This solution has two problems
It will not allow me Multilevel Inheritance (I would not be able to have child and grand child)
Its actually even not an overriding, its just a replacement of implementation as it gives error using super
Just for elaboration, following is an example of similar behavior overriding in odoo, what I want to achieve with django
The exact expected/desired behavior is offered by odoo.
You can see get_auth_signup_qcontext method of auth_oauth's main controller
https://github.com/odoo/odoo/blob/14.0/addons/auth_oauth/controllers/main.py
https://github.com/odoo/odoo/blob/14.0/addons/auth_signup/controllers/main.py
What is does is if auth_oauth module is installed anywhere we call get_auth_signup_qcontext it would first go to child(auth_oauth)'s get_auth_signup_qcontext method which will call super in it. But if auth_oauth is not installed anywhere we call get_auth_signup_qcontext will directly hit auth_signup's method

Method 1. Use your own admin site's urls
One can simply use their own admin site's urls (reference):
from django.contrib.admin import AdminSite
class MyAdminSite(AdminSite):
def get_app_list(self, request):
app_list = super().get_app_list(request)
for app in app_list:
#app['models'].sort(key=lambda x: x['name'])
app['models'] = sort_with_name_length(app['models']) #an example change I need
return app_list
admin_site = MyAdminSite(name='myadmin')
admin_site.register(MyModel) # registering models
Then in your urls:
from django.urls import path
from myapp.admin import admin_site
urlpatterns = [
path('myadmin/', admin_site.urls),
]
Method 2. Overriding the default admin site
You can override the admin site:
In apps.py:
from django.contrib.admin.apps import AdminConfig
class MyAdminConfig(AdminConfig):
default_site = 'myproject.admin.MyAdminSite'
In settings.py:
INSTALLED_APPS = [
...
'myproject.apps.MyAdminConfig', # replaces 'django.contrib.admin'
...
]

Quite an awkward and hacky but completely independent/loosely coupled solution to really override/extend (not replace) a builtin class of django's method is
Caution read the complete answer before using code :)
Add following code to models.py of any of your custom (installed) app
from django.contrib.admin import AdminSite
original_get_app_list = AdminSite.get_app_list
class AdminSiteExtension1(AdminSite):
def get_app_list(self, request):
// do something here to manipulate earlier than calling parent
// Following is the solution line
app_list = original_get_app_list(self, request)
for app in app_list:
app['name'] = app['name'] + '_my_app'
return app_list
AdminSite.get_app_list = AdminSiteExtension1.get_app_list
AdminSite is inherited just to make the use of self
Its really loosely coupled as u need to do nothing anywhere else. Even you can extend it further in any other module and you will not need AdminSiteExtension1 in that module because the updates to get_app_list of original AdminSite reside in original method as long as the app having AdminSiteExtension1 is installed.
Disclosure: I am not really a well learned and visionary programmer so I cannot imagine a situation where this solution can cause any problem, so if someone guides, it would be welcome. otherwise the beneficiary has to take care him/herself.

Related

Wagtail: Add method to PageModel to get an inspect url

I am trying to add a dynamic instead of a hard coded get_inspect_url method to a PageModel:
class MyModel(Page):
# ...
# this is what I have now, which breaks in prod due to a different admin url
def get_inspect_url(self):
inspect_url = f"/admin/myapp/mymodel/inspect/{self.id}/"
return inspect_url
I know that you can use the ModelAdmin.url_helper_class in wagtail_hooks.py to get admin urls for an object, but I can't figure a way to do the same on the model level as a class method. Is there a way?
thanks to #Andrey Maslov tip I found a way:
from django.urls import reverse
def get_inspect_url(self):
return reverse(
f"{self._meta.app_label}_{self._meta.model_name}_modeladmin_inspect",
args=[self.id],
)
The basic form of a Wagtail admin url is:
[app_label]_[model_name]_modeladmin_[action]
Just change the [action] to one of the following to generated other instance level admin urls:
edit
delete
if you have in you urls.py line like this
path(MyModel/view/<id:int>/', YourViewName, name='mymodel-info'),
then you can add to your models.py this lines
from django.urls import reverse
...
class MyModel(Page):
def get_inspect_url(self):
inspect_url = reverse('mymodel-info', kwargs={'id': self.id})
return inspect_url

Testing Django Rest Framework: how to test hyperlink relations?

I'm trying to create a true unit test for a customized DjangoRestFramework Hyperlinked related field. But I cannot seem to get around this error:
django.core.exceptions.ImproperlyConfigured: Could not resolve URL for hyperlinked relationship using view name "relatedtestmodel-detail". You may have failed to include the related model in your API, or incorrectly configured the `lookup_field` attribute on this field.
And here is the unit test, stripped down to simplify the example:
from django.conf.urls import url
from django.test import TestCase, override_settings
from api_tests.models import APITestModel, RelatedTestModel
from api_tests.serializers import APITestModelSerializer
def dummy_view(request, pk):
pass
urlpatterns = [
url(r'/path/is/irrelevant/', dummy_view, name='relatedtestmodel-detail')
]
#override_settings(ROOT_URLCONF='tests.test_relations')
class HyperlinkedRelatedFieldTestCase(TestCase):
def setUp(self):
self.parent = APITestModel.objects.create()
self.child = RelatedTestModel.objects.create(parent=self.parent)
assert self.child.parent.id == self.parent.id
def test_to_internal_value_correct_error_message(self):
queryset = APITestModel.objects.all()
serializer = APITestModelSerializer(queryset, many=True, context={'request': None})
expected = [{'foo': 'bar'}]
self.assertEqual(serializer.data, expected)
I more or less lifted the test from https://github.com/encode/django-rest-framework/blob/master/tests/test_relations_hyperlink.py, because I figured who knows best how to unit test DRF than the makers of DRF? But as it stands, my test refuses to run. The error is raised during the test, when I attempt to access serializer.data for the assert.
Notice in particular that I override the settings with a custom urlpatterns (which is this same file, hence the urlpatterns at the top). So I don't understand why DRF thinks that url name doesn't exist - I can clearly see that my url conf in fact has ONLY that view name! I've even gone so far as to edit my actual URL conf and replace it with the single, named, dummy url pattern shown here, and removedthe settings override, just to be sure that it wasn't that the override_settings simply wasn't working, but even then I get the same error.
To my eye, the dummy url pattern is exactly the same as how DRF did it in their tests. Anyone have any ideas what is going on?
A bit more requested context:
api_tests.models:
from django.db import models
class APITestModel(models.Model):
pass
class RelatedTestModel(models.Model):
parent = models.ForeignKey(
APITestModel,
related_name='children',
related_query_name='child'
)
I do not have access to the traceback at this time, but I can confirm it did not pass through any of my code - it was all isolated to the DjangoRestFramework code, basically exclusively relations.py
Preamble
A few things this question is lacking
No definition of APITestModelSerializer
RelatedTestModel is not used in the test and therefore irrelevant for the example
No error stacktrace
No "useful" expected dict for the asserts
APITestModel has no fields so it can't be serialized (your test shouldn't even have run)
Minor things but still relevant
You are creating specific instances of APITestModel and RelatedTestModel in the setUp but in the test you serialize all instances of APITestModel
The line assert self.child.parent.id == self.parent.id should not be in the setUp. It should be in a separate test
My changes
I deleted all irrelevant information for this question mentioned above
I added an integer field to APITestModel
I changed the urlpatterns element from url(...) to path(...)
I added a regex to the relative path
The serializer is a subclass of HyperlinkedModelSerializer and includes fields "url" and "year"
My project and app urls.py files are the "stock" ones (not shown here) to emphasize that this test resolves the path in isolation.
Changed #override_settings(ROOT_URLCONF='tests.test_relations') to #override_settings(ROOT_URLCONF=__name__)
Code
models.py
from django.db import models
class APITestModel(models.Model):
year = models.IntegerField(null=False)
serializers.py
from rest_framework import serializers
from api.models import APITestModel
class APITestModelSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = APITestModel
fields = ["url", "year"]
test_serializers.py
from django.test import TestCase, override_settings
from django.urls import path
from api.models import APITestModel
from api.serializers import APITestModelSerializer
urlpatterns = [
path('whateveryouwant/<int:pk>/', lambda request: None, name='apitestmodel-detail'),
]
#override_settings(ROOT_URLCONF=__name__)
class HyperlinkedRelatedFieldTestCase(TestCase):
def setUp(self):
# Populate db with APITestModel instances
_ = APITestModel.objects.create(year=1960)
_ = APITestModel.objects.create(year=1961)
_ = APITestModel.objects.create(year=1962)
def test_to_internal_value_correct_error_message(self):
queryset = APITestModel.objects.all()
serializer = APITestModelSerializer(queryset, many=True, context={'request': None})
expected = [
{'url': '/whateveryouwant/1/', 'year': 1960},
{'url': '/whateveryouwant/2/', 'year': 1961},
{'url': '/whateveryouwant/3/', 'year': 1962},
]
self.assertEqual(serializer.data, expected)
The other files in the project are the default ones created automatically by django + djangorestframework.
For future readers, I created a github project with this working code and can be found here: https://github.com/Alechan/drf_test_hyperlink_relations

Avoiding circular imports in Django Models (Config class)

I've created a Configuration model in django so that the site admin can change some settings on the fly, however some of the models are reliant on these configurations. I'm using Django 2.0.2 and Python 3.6.4.
I created a config.py file in the same directory as models.py.
Let me paracode (paraphase the code? Real Enum has many more options):
# models.py
from .config import *
class Configuration(models.Model):
starting_money = models.IntegerField(default=1000)
class Person(models.Model):
funds = models.IntegarField(default=getConfig(ConfigData.STARTING_MONEY))
# config.py
from .models import Configuration
class ConfigData(Enum):
STARTING_MONEY = 1
def getConfig(data):
if not isinstance(data, ConfigData):
raise TypeError(f"{data} is not a valid configuration type")
try:
config = Configuration.objects.get_or_create()
except Configuration.MultipleObjectsReturned:
# Cleans database in case multiple configurations exist.
Configuration.objects.exclude(Configuration.objects.first()).delete()
return getConfig(data)
if data is ConfigData.MAXIMUM_STAKE:
return config.max_stake
How can I do this without an import error? I've tried absolute imports
You can postpone loading the models.py by loading it in the getConfig(data) function, as a result we no longer need models.py at the time we load config.py:
# config.py (no import in the head)
class ConfigData(Enum):
STARTING_MONEY = 1
def getConfig(data):
from .models import Configuration
if not isinstance(data, ConfigData):
raise TypeError(f"{data} is not a valid configuration type")
try:
config = Configuration.objects.get_or_create()
except Configuration.MultipleObjectsReturned:
# Cleans database in case multiple configurations exist.
Configuration.objects.exclude(Configuration.objects.first()).delete()
return getConfig(data)
if data is ConfigData.MAXIMUM_STAKE:
return config.max_stake
We thus do not load models.py in the config.py. We only check if it is loaded (and load it if not) when we actually execute the getConfig function, which is later in the process.
Willem Van Onsem's solution is a good one. I have a different approach which I have used for circular model dependencies using django's Applications registry. I post it here as an alternate solution, in part because I'd like feedback from more experienced python coders as to whether or not there are problems with this approach.
In a utility module, define the following method:
from django.apps import apps as django_apps
def model_by_name(app_name, model_name):
return django_apps.get_app_config(app_name).get_model(model_name)
Then in your getConfig, omit the import and replace the line
config = Configuration.objects.get_or_create()
with the following:
config_class = model_by_name(APP_NAME, 'Configuration')
config = config_class.objects.get_or_create()

Sphinx documentation on Django templatetags

I am using Sphinx with autodoc for documenting my Django project.
Design guys want to have documentation page(s) about all the template tags that are defined in the project. Of course, I can make such a page by enumerating all the template processing functions by hand, but I think it is not DRY, isn't it? In fact, template tag processing functions are all marked with #register.inclusion_tag decorator. So it seems to be possible and natural for some routine to collect them all and put into documentation.
The same about filter functions.
I've googled it, searched Django documentation but in vein. I can hardly believe that such a natural functionality hasn't been implemented by someone.
I didn't stop at this point and implemented a Sphinx autodoc extension.
Snippet 2. Sphinx autodoc extension
"""
Extension of Sphinx autodoc for Django template tag libraries.
Usage:
.. autotaglib:: some.module.templatetags.mod
(options)
Most of the `module` autodoc directive flags are supported by `autotaglib`.
Andrew "Hatter" Ponomarev, 2010
"""
from sphinx.ext.autodoc import ModuleDocumenter, members_option, members_set_option, bool_option, identity
from sphinx.util.inspect import safe_getattr
from django.template import get_library, InvalidTemplateLibrary
class TaglibDocumenter(ModuleDocumenter):
"""
Specialized Documenter subclass for Django taglibs.
"""
objtype = 'taglib'
directivetype = 'module'
content_indent = u''
option_spec = {
'members': members_option, 'undoc-members': bool_option,
'noindex': bool_option,
'synopsis': identity,
'platform': identity, 'deprecated': bool_option,
'member-order': identity, 'exclude-members': members_set_option,
}
#classmethod
def can_document_member(cls, member, membername, isattr, parent):
# don't document submodules automatically
return False
def import_object(self):
"""
Import the taglibrary.
Returns True if successful, False if an error occurred.
"""
# do an ordinary module import
if not super(ModuleDocumenter, self).import_object():
return False
try:
# ask Django if specified module is a template tags library
# and - if it is so - get and save Library instance
self.taglib = get_library(self.object.__name__)
return True
except InvalidTemplateLibrary, e:
self.taglib = None
self.directive.warn(unicode(e))
return False
def get_object_members(self, want_all):
"""
Decide what members of current object must be autodocumented.
Return `(members_check_module, members)` where `members` is a
list of `(membername, member)` pairs of the members of *self.object*.
If *want_all* is True, return all members. Else, only return those
members given by *self.options.members* (which may also be none).
"""
if want_all:
return True, self.taglib.tags.items()
else:
memberlist = self.options.members or []
ret = []
for mname in memberlist:
if mname in taglib.tags:
ret.append((mname, self.taglib.tags[mname]))
else:
self.directive.warn(
'missing templatetag mentioned in :members: '
'module %s, templatetag %s' % (
safe_getattr(self.object, '__name__', '???'), mname))
return False, ret
def setup(app):
app.add_autodocumenter(TaglibDocumenter)
This extension defines Sphinx directive autotaglib which behaves much like automodule, but enumerates only tag implementing functions.
Example:
.. autotaglib:: lib.templatetags.bfmarkup
:members:
:undoc-members:
:noindex:
For the record, Django has an automatic documentation system (add django.contrib.admindocs to your INSTALLED_APPS).
This will give you extra views in the admin (usually at /admin/docs/) that represent your models, views (based on the URL), template tags and template filters.
More documentation for this can be found in the admindocs section.
You can take a look at that code to include it in your documentation or at the extensions for the Django documentation.
I solved the problem and would like to share my snippets - in case they will be useful to someone.
Snippet 1. Simple documenter
import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'project.settings'
from django.template import get_library
def show_help(libname):
lib = get_library(libname)
print lib, ':'
for tag in lib.tags:
print tag
print lib.tags[tag].__doc__
if __name__ == '__main__':
show_help('lib.templatetags.bfmarkup')
Before running this script you should setup PYTHONPATH environment variable.

Problem using generic views in django

I'm currently working with django generic views and I have a problem I can't figure out.
When using delete_object I get a TypeError exception:
delete_object() takes at least 3 non-keyword arguments (2 given)
Here is the code (I have ommited docstrings and imports):
views.py
def delete_issue(request, issue_id):
return delete_object(request,
model = Issue,
object_id = issue_id,
template_name = 'issues/delete.html',
template_object_name = 'issue')
urls.py
urlpatterns = patterns('issues.views',
(r'(?P<issue_id>\d+)/delete/$', 'delete_issue'),
)
The other generic views (object_list, create_object, etc.) work fine with those parameters. Another problem I have is when using the create_object() function, it says something about a CSRF mechanism, what is that?
You need to provide post_delete_redirect, this means url, where user should be redirected after object is deleted. You can find this in view signature:
def delete_object(request, model, post_delete_redirect, object_id=None,
slug=None, slug_field='slug', template_name=None,
template_loader=loader, extra_context=None, login_required=False,
context_processors=None, template_object_name='object'):