I import
from django.contrib.sites.models import Site in models.py file.
In have this following in admin.py file:
class SitesAdmin(admin.ModelAdmin):
pass
admin.site.unregister(Site)
admin.site.register(Site, SitesAdmin)**
I want to attach validation to the site.domain field in admin.py, How i can accomplish this? please help.
First, specifying an empty ModelAdmin class is unnecessary, the following will work if you don't need to customize the admin:
admin.site.register(Site) # Notice that no ModelAdmin is passed
Now, to your question. You have to create a custom form. Then, you override the clean_domain method of the ModelForm. You can validate any field with the method(s) clean_FOO, where FOO is the field name.
from django import forms
class SiteAdminForm(forms.ModelForm):
def clean_domain(self):
domain = self.cleaned_data.get('domain')
# Custom validation here
return domain
class SiteAdmin(admin.ModelAdmin):
form = SiteAdminForm
admin.site.unregister(Site)
admin.site.register(Site, SiteAdmin)
Related
I have a Django app named site_settings in which SiteSettings model is defined. This model contains a foreign key to django.contrib.sites.models.Site model. Now I want to override default objects manager of Site model with new one I defined:
from django.contrib.sites.models import SiteManager as _OrigSiteManager
class SiteManager(_OrigSiteManager):
...
I tried:
Site.add_to_class("objects", SiteManager())
But it didn't work. The problem is I am adding to class with already existing objects name. When adding with another name, it works as expected:
Site.add_to_class("my_objects", SiteManager()) # now Site.my_objects points to my custom manager
But I want to override existing objects manager with my custom manager. How can I do that?
You can redefine and use your own Site class everywhere:
from django.contrib.sites.models import Site
class Site(Site):
...
objects = SiteManager()
or you can use contribute_to_class:
from django.contrib.sites.models import Site
SiteManager().contribute_to_class(Site, 'objects')
or you can made monkey patch:
from django.contrib.sites.models import Site
Site.objects = SiteManager(model=Site)
If you need it only to change queryset in foreignkey to Site, you can use limit_choices_to:
foreignkey(Site, limit_choices_to=Q(your query to limit sites queryset))
I'm working on a Django project that utilizes customized greetings (like in voicemail). The whole functionality is implemented, i have created a custom model:
class Greeting(models.Model):
audio_file = models.FileField(upload_to='greetings/')
description = models.CharField(max_length=128)
uploaded_at = models.DateTimeField(auto_now_add=True)
The next thing that i wanted to do is to make sure that the uploaded file has all the expected properties (is a WAV file, has one channel, has low bitrate etc). But i don't even know where to start. These files will be only added via django admin. In regular FormView i would utilize server-sided validation in View, and only then add it to model. How to do it in django admin?
To summarize what i expect my app to do:
1) Add file to a model in django admin
2) Server checks file properties, and if requirements are not met, tries to convert it to proper format 3) If the file is in proper format, only then it saves the object.
You need to register a ModelAdmin with a custom form.
ModelAdmin has a form property which is by default set to forms.ModelForm class, you can replace that by assigining that property to your Admin class.
# app_dir/admin.py
from django.contrib import admin
from .forms import GreetingAdminForm
from .models import Greeting
#admin.register(models.Greeting)
class GreetingAdmin(admin.ModelAdmin):
form = GreetingAdminForm
readonly_fields = ['uploaded_at']
Than you need to define your GreetingAdminForm in forms.py. with custom validation Logic.
The way I would do it is add a ModelForm with overridden audo_file field with added validators. You can check the django documentation for writing your validation logic here
Probaly you want to use file extension validation, and add a clean_{fieldname} method on the form.
The clean_{fieldname} method does not take any arguments but the return value of this method must replace the existing value in cleaned_data. You will need an external library that suits your needs, accepts audio formats that you intend to allow, and outputs processed file in desired format. Docs on cleaning specific attribiutes are here
# app_dir/forms.py
from django import forms
from django.core.exceptions import ValidationError
from .validators import validate_file_extension
from .models import Greeting
class GreetingAdminForm(forms.ModelForm):
audio_file = forms.FileField(validators=[validate_file_extension])
def clean_audio_file(self):
data = self.cleaned_data
processed_audio_file = None
# audio file processing logic goes here,
if not processed_audio_file:
raise ValidationError('error message')
data['audio_file'] = processed_audio_file
return data
class Meta:
model = Greeting
fields = [
'audio_file',
'description'
]
# app_dir/validators.py
def validate_file_extension(value):
# validate file extension logic here,
you can find a example of file extension validation
here
Another angle to approach this could be also
- writing a custom form field which subclasses the FileField,you can find documentation on writing your own field here, this class should override w methods validate() - which handles validation logic, and to python where you would prepare output to be available in your python code
I have a request from a client to have page admin fields that they can add/read numbers into with commas such as 1,000,000.
The Django model field to store the value would be a django.db.models.fields.DecimalField instance.
From looking at the Django docs, this is something that’s supported by the django.forms.fields.DecimalField localized property, but I can’t find a way of enforcing it in the Wagtail admin, even when subclassing the Wagtail BaseFieldPanel __init__ function with self.bound_field.field.localize = True.
You can override the fields that Wagtail FieldPanel uses by customising generated forms documented here:
http://docs.wagtail.io/en/v1.13/advanced_topics/customisation/page_editing_interface.html#wagtail.wagtailadmin.forms.WagtailAdminPageForm
Basic example below - myapp/models.py
from django import forms
from django.db import models
from wagtail.wagtailadmin.edit_handlers import FieldPanel
from wagtail.wagtailadmin.forms import WagtailAdminPageForm
from wagtail.wagtailcore.models import Page
class MyCustomPageForm(WagtailAdminPageForm):
# fields here are django.forms fields
# when set to localize, renders as a TextInput widget
total_amount = forms.DecimalField(localize=True)
# when left with defaults, renders as a NumberInput widget
# total_amount = forms.DecimalField()
# can also set any kind of widget here
# total_amount = forms.DecimalField(widget=MyCustomDecimalWidget)
class MyCustomPage(Page):
# fields here are django.db.models fields
total_amount = models.DecimalField()
content_panels = Page.content_panels + [
FieldPanel('total_amount'),
]
base_form_class = MyCustomPageForm # important: must set this
When you set the form field to localize=True it appears to follow the intended behaviour as per the Django Docs which is to render a TextInput widget.
You could also set your own widget for any field in your page's form class if you want to do some more complicated functionality.
Thanks #lb-ben-johnston - also to the Wagtail team who pointed me in a similar direction.
I have it working now in a loosely coupled fashion with this:
from django.forms.fields import DecimalField
from wagtail.wagtailadmin.forms import WagtailAdminPageForm
class ProductPageForm(WagtailAdminPageForm):
def __init__(self, *args, **kwargs):
super(ProductPageForm, self).__init__(*args, **kwargs)
for key, field in self.fields.items():
if isinstance(field, DecimalField):
field.localize = True
field.widget.is_localized = True
# The Page class to localize
class LocalizedPage(Page):
base_form_class = ProductPageForm
# Rest of page class
Make sure you explictly pass a TextInput widget to the field handler:
FieldPanel('localized_field', widget=TextInput)
Finally, make sure that the thousand separator flag is explicitly set in your settings.py or base.py:
USE_THOUSAND_SEPARATOR = True
I have 2 models - for example, Book and Page.
Page has a foreign key to Book.
Each page can be marked as "was_read" (boolean), and I want to prevent deleting pages that were read (in the admin).
In the admin - Page is an inline within Book (I don't want Page to be a standalone model in the admin).
My problem - how can I achieve the behavior that a page that was read won't be deleted?
I'm using Django 1.4 and I tried several options:
Override "delete" to throw a ValidationError - the problem is that the admin doesn't "catch" the ValidationError on delete and you get an error page, so this is not a good option.
Override in the PageAdminInline the method - has_delete_permission - the problem here -it's per type so either I allow to delete all pages or I don't.
Are there any other good options without overriding the html code?
Thanks,
Li
The solution is as follows (no HTML code is required):
In admin file, define the following:
from django.forms.models import BaseInlineFormSet
class PageFormSet(BaseInlineFormSet):
def clean(self):
super(PageFormSet, self).clean()
for form in self.forms:
if not hasattr(form, 'cleaned_data'):
continue
data = form.cleaned_data
curr_instance = form.instance
was_read = curr_instance.was_read
if (data.get('DELETE') and was_read):
raise ValidationError('Error')
class PageInline(admin.TabularInline):
model = Page
formset = PageFormSet
You could disable the delete checkbox UI-wise by creating your own custom
formset for the inline model, and set can_delete to False there. For
example:
from django.forms import models
from django.contrib import admin
class MyInline(models.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(MyInline, self).__init__(*args, **kwargs)
self.can_delete = False
class InlineOptions(admin.StackedInline):
model = InlineModel
formset = MyInline
class MainOptions(admin.ModelAdmin):
model = MainModel
inlines = [InlineOptions]
Another technique is to disable the DELETE checkbox.
This solution has the benefit of giving visual feedback to the user because she will see a grayed-out checkbox.
from django.forms.models import BaseInlineFormSet
class MyInlineFormSet(BaseInlineFormSet):
def add_fields(self, form, index):
super().add_fields(form, index)
if some_criteria_to_prevent_deletion:
form.fields['DELETE'].disabled = True
This code leverages the Field.disabled property added in Django 1.9. As the documentation says, "even if a user tampers with the field’s value submitted to the server, it will be ignored in favor of the value from the form’s initial data," so you don't need to add more code to prevent deletion.
In your inline, you can add the flag can_delete=False
EG:
class MyInline(admin.TabularInline):
model = models.mymodel
can_delete = False
I found a very easy solution to quietly avoid unwanted deletion of some inlines. You can just override delete_forms property method.
This works not just on admin, but on regular inlines too.
from django.forms.models import BaseInlineFormSet
class MyInlineFormSet(BaseInlineFormSet):
#property
def deleted_forms(self):
deleted_forms = super(MyInlineFormSet, self).deleted_forms
for i, form in enumerate(deleted_forms):
# Use form.instance to access object instance if needed
if some_criteria_to_prevent_deletion:
deleted_forms.pop(i)
return deleted_forms
I'm trying to send an email to a user when a new model instance is saved and I want the email to include a link to the admin page for that model instance. Is there a way to get the correct URL? I figure Django must have that information stored somewhere.
Not trying to rip off #JosvicZammit, but using ContentType is the wrong approach here. It's just a wasted DB query. You can get the require info from the _meta attribute:
from django.urls import reverse
info = (model_instance._meta.app_label, model_instance._meta.model_name)
admin_url = reverse('admin:%s_%s_change' % info, args=(model_instance.pk,))
This Django snippet should do:
from django.urls import reverse
from django.contrib.contenttypes.models import ContentType
from django.db import models
class MyModel(models.Model):
def get_admin_url(self):
content_type = ContentType.objects.get_for_model(self.__class__)
return reverse("admin:%s_%s_change" % (content_type.app_label, content_type.model), args=(self.id,))
The self refers to the parent model class, i.e. self.id refers to the object's instance id. You can also set it as a property on the model by sticking the #property decorator on top of the method signature.
EDIT: The answer by Chris Pratt below saves a DB query over the ContentType table. My answer still "works", and is less dependent on the Django model instance._meta internals. FYI.
This gives the same result as Josvic Zammit's snippet, but does not hit the database:
from django.urls import reverse
from django.db import models
class MyModel(models.Model):
def get_admin_url(self):
return reverse("admin:%s_%s_change" % (self._meta.app_label, self._meta.model_name), args=(self.id,))
Just use this one liner that is also python 3 ready:
from django.urls import reverse
reverse('admin:{0}_{1}_change'.format(self._meta.app_label, self._meta.model_name), args=(self.pk,))
More on this in the django admin site doc, reversing admin urls.
So, combining the answers by Chris, Josvic and Josh, here's a copy-paste method you can add into your model (tested on Django 1.8.3).
def get_admin_url(self):
"""the url to the Django admin interface for the model instance"""
from django.core.urlresolvers import reverse
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))