testing admin.ModelAdmin in django - django

I am trying to find out the best way for testing admin.ModelAdmin in admin.py. Specifically I am overriding the save_model() function which I want to test. From the research I have done, the only solution I have found was writing a request/response test and then query the database.

As suggested in Udi's answer, we can study Django's own ModelAdmin tests, to determine the basic ingredients for a ModelAdmin test. Here's a summary:
Basic ingredients
In addition to the Django TestCase stuff, the basic ingredients are:
An instance of AdminSite:
from django.contrib.admin.sites import AdminSite
Your model class and corresponding ModelAdmin (sub)class:
from my_app.models import MyModel
from my_app.admin import MyModelAdmin
Optionally, depending on your needs, a (mock) request and/or form.
Recipe
The first two ingredients are required to create an instance of your (custom) ModelAdmin:
my_model_admin = MyModelAdmin(model=MyModel, admin_site=AdminSite())
Based on the ModelAdmin source, the default save_model implementation only requires an instance of your model, so it can be called, for example, as follows:
my_model_admin.save_model(obj=MyModel(), request=None, form=None, change=None)
# some test assertions here
It all depends on what your save_model does, and what you want to test.
Suppose your save_model checks user permissions, then you would need to provide a request (i.e. the third ingredient) with a valid user, in addition to the model instance:
from unittest.mock import Mock
...
my_user = User.objects.create(...)
my_model_admin.save_model(
obj=MyModel(), request=Mock(user=my_user), form=None, change=None
)
# some test assertions here
Here we use unittest.mock.Mock to create a mock-request. Based on the Django test source, a minimal request consists of a Python object with a user attribute.
The user attribute may refer to a mock user, or an actual instance of your AUTH_USER_MODEL, depending on your needs. An alternative would be to use django.test.RequestFactory.
This basic approach applies to the other ModelAdmin methods as well.

Check out Django's ModelAdminTests for examples.

You can specify custom modelform for modeladmin then simply test this modelform ;)
https://docs.djangoproject.com/en/1.8/ref/contrib/admin/#django.contrib.admin.ModelAdmin.form
forms
class SomeModelForm(forms.ModelForm):
class Meta:
model = SomeModel
admin
class SomeModelAdmin(admin.ModelAdmin):
form = SomeModelForm
admin.site.register(SomeModel, SomeModelAdmin)
tests
class TestSomeModel(TestCase):
def test_form(self):
form = SomeModelForm(**kwargs)
self.assertTrue(form.is_valid())
# ...

Related

Create custom page view counter in Django

I want to create my own view counter. I got inspired from django-hitcount. I read all models of this app.
In these lines:
class HitCountMixin(object):
"""
HitCountMixin provides an easy way to add a `hit_count` property to your
model that will return the related HitCount object.
"""
#property
def hit_count(self):
ctype = ContentType.objects.get_for_model(self.__class__)
hit_count, created = HitCount.objects.get_or_create(
content_type=ctype, object_pk=self.pk)
return hit_count
I couldn't understand the meaning and usage of ContentType and get_for_model(self.__class__). Can anyone help me?
Source of this mixin is here.
Since HitCountMixin can be inherited by different models in your app, HitCount model must be in some way connected to these models with a relation.
Here you can think of ContentType as a way of creating dynamic relation unlike it is with e.g. ForeignKey where you are bound to use the relation only with one model (table).
get_for_model is just Django's helper method for getting ContentType instance for given model because each model (table) would have its corresponding ContentType instance.
With example model using this mixin:
class Example(models.Model, HitCountMixin):
pass
ContentType.objects.get_for_model(self.__class__) would return ContentType instace for model Example
You can read more about ContentTypes in Django documentation

Django model validator not working on create

I have model with a field validator
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
class MyModel(model.Model):
name = models.CharField()
size = models.IntegerField(validators=[MinValueValidator(1),MaxValueValidator(10)])
The validator is working well in the django admin panel ,while I try to enter the value more than 10, it's showing me the error message 'Ensure this value is less than or equal to 10' and does not allow to save.
But, when I try in the django shell, the validator is not working, it allows to save the record, I don't know why is the validator not throwing error message here.
>>>form app.models import MyModel
>>>MyModel.objects.create(name="Some Name", size=15)
<MyModel: Some Name>
Can you please suggest me if anything I missed or any mistake i did here. Kindly help me to solve this problem, it will be very greatfull for me, Thanks in advance.
Django validation is mostly application level validation and not validation at DB level. Also Model validation is not run automatically on save/create of the model. If you want to validate your values at certain time in your code then you need to do it manually.
For example:
from django.core.exceptions import ValidationError
form app.models import MyModel
instance = MyModel(name="Some Name", size=15)
try:
instance.full_clean()
except ValidationError:
# Do something when validation is not passing
else:
# Validation is ok we will save the instance
instance.save()
More info you can see at django's documentation https://docs.djangoproject.com/en/1.10/ref/models/instances/#validating-objects
In administration it works automatically because all model forms (ModelForm) will run model validation process alongside form validation.
If you need to validate data because it is coming from untrusted source (user input) you need to use ModelForms and save the model only when the form is valid.
The validator only works when you are using models in a ModelForm.
https://docs.djangoproject.com/en/dev/ref/validators/#how-validators-are-run
You can perform model validation by overidding clean() and full_clean() methods
Validators work only with the Forms and model forms. Can't be used with the model definition because it runs at the app side not the DB side.
You can add this to your model and call it in save().
def save(self, *args, **kwargs):
self.run_validators()
super().save(*args, **kwargs)
def run_validators(self) -> None:
for field_name, field_value in model_to_dict(self).items():
model_field = getattr(UserSearchHistory, field_name)
field = getattr(model_field, 'field', object())
validators = getattr(field, 'validators', list())
for validator_func in validators:
if field_value is not None:
validator_func(field_value)
From django documentation:
Note that validators will not be run automatically when you save a
model, but if you are using a ModelForm, it will run your validators
on any fields that are included in your form.
https://docs.djangoproject.com/en/3.1/ref/validators/#how-validators-are-run
I ran into the same issue.
So the validators only work when you are using Forms and Model Form to fill it.
However, by creating in shell, you probably wanted to test the validators before going live.
So here is the additional piece of code to help in validating the validators.
>>>form app.models import MyModel
>>>MyModel.size.field.run_validators(value=<undesirable value>)
You can not run validator in creating you must run validation in instance if not exception occurred you must save it
It is worth mentioning that model field validators
like validate_color in here:
bg_color = models.CharField(
max_length=50, default="f0f2f5", validators=[validate_color]
)
work with restf_ramework (drf) Serializer class either.
https://github.com/encode/django-rest-framework/blob/master/rest_framework/serializers.py
so validators run when you call is_valid on ModelForm (from django) or is_valid on Serializer (from rest_framework).

Display a custom link in Django Admin edit item view?

Here's a piece of Django admin interface's instance edition form:
How should I change the underlying admin.ModelAdmin instance to make it contain an URL, like this?
Django makes this easy. Subclass ModelAdmin, add a custom method and then tell the Admin how to use it. Here's a sample admin.py:
from django.contrib import admin
from .models import Vendor
class VendorAdmin(admin.ModelAdmin):
readonly_fields = ['example_link']
def example_link(self, obj):
return 'link text'.format(obj.get_link()) # however you generate the link
example_link.allow_tags = True
admin.site.register(Vendor, VendorAdmin)
Here's the documentation that furthers explains readonly_fields, customizing the form label text with short_description, ordering, and how you can put this custom url method on the Model or ModelAdmin.

Change default widgets of Django to custom ones

My usecase: I want to use a different DateInput. But I want to reduce code duplication. I want all forms, which don't explicitly want a different DateInput widget, to use my custom widget.
Any change to solve this without monkey patching?
Example
models.py:
class MyModel(models.Model):
date=models.DateField()
forms.py:
class MyForm(forms.ModelForm):
class Meta:
model=MyModel
The above code should use my custom widget. I don't want to change the above models.py and forms.py, since there are many.
Unfortunately, I don't think you can get this working with your exact code listed above.
Without hacking django, essentially there are 2 parts to this. The first is creating a custom form field, and the second is defaulting your custom model field to your newly created form field.
To create your custom Form Field, you could override the existing django forms.DateField and update the widget.
# form_fields.py
from django.forms import DateField
from myapp.widgets import MyWidget
class MyDateFormField(DateField):
widget = MyWidget
And then after you have your form field created, you're going to have to override the django model field to default to your new form field
# fields.py
from django.db.models import DateField
from myapp.form_fields import MyDateFormField
class MyDateField(MyDateFormField):
def formfield(self, **kwargs):
defaults = {'form_class': MyDateFormField}
defaults.update(kwargs)
return super(DateField, self).formfield(**defaults)
You would then have your custom model field, which you would need to slightly change your code to use.
from myapp.fields import MyDateField
class MyModel(models.Model):
date=MyDateField()
It's not exactly what you were asking for (have to change the model field), but hopefully this gets you in the right direction.
Create your field
Create form that will use this field by default
import this form instead of default form, when you use it
If you're using it in admin:
create your own ModelAdmin that will use your form by default
use that instead of default ModelAdmin.

Best way to add convenience methods to a Django Auth User model?

I want to add a convenience/model method to the django.contrib.auth.models.User model. What is the best practice for doing this since, last time I checked, extending the User model was considered bad practice.
I have a separate custom UserProfile model. Should I be using that for all User-related convenience methods?
It depends what you are trying to add to the model. If you want to add more information about the user, then it is generally recommended that you use the UserProfile method: http://docs.djangoproject.com/en/dev/topics/auth/#storing-additional-information-about-users
However, if you just want to add custom methods or managers to the User model, I would say that it's more logical to use a proxy model, like so:
from django.contrib.auth.models import User
class UserMethods(User):
def custom_method(self):
pass
class Meta:
proxy=True
A proxy model will operate on the same database table as the original model, so is ideal for creating custom methods without physically extending the model. Just replace any references to User in your views to UserMethods. (And of course you can use this in the admin tool by unregistering the User model and registering your proxy model in its stead.)
Any instances of the original User model that are created will be instantly accessible via the UserMethods model, and vice-versa. More here: http://docs.djangoproject.com/en/dev/topics/db/models/#proxy-models
(NB. Proxy models require Django 1.1 and above)
if you want to add custom methods to the User model, I would recommend monkey_patching:
create a file monkey_patching.py in any of your apps::
#app/monkey_patching.py
from django.contrib.auth.models import User
def get_user_name(self):
if self.first_name or self.last_name:
return self.first_name + " " + self.last_name
return self.username
User.add_to_class("get_user_name",get_user_name)
and import it in app's __init__.py file. ie::
#app/__init__.py
import monkey_patching
Yes. No need to mess with the foundations when your user model has a .get_profile() function attached to it.
2013 update:
in 1.5 you can sustitute a custom User model and add whatever you want https://docs.djangoproject.com/en/dev/topics/auth/customizing/#auth-custom-user
I prefer to use the same UserProfile across various projects I develop and extend User for any project-specific needs. So, common functionality goes to UserProfile, and project-specific functionality goes to custom User. I have not had any adverse effects of having a subclassed User model yet, I wonder if there still exist any with Django 1.0+.