Configuring Many-to-many field in django admin with related_name - django

I have the following model and admin defined in djando 1.5. This is a many-to-many relationship between subnet and vlan. I use the related_name option in the ManyToMany field to be able to get the collection of vlan from the related subnet object. Adding subnet to vlans from the admin works well. However when I try to add an horizontal_filer to the subnet admin in order to add van to its vlan set I get an error saying that the vlans attribute doesn't exist. I'm using subnet object in some view and I can access the vlans attribute just right.
What am I doing wrong here ? I've seen similar post but I couldn't adapt any of the proposed solution with success.
Thanks for your help
model.py
from django.db import models
class Subnet(models.Model):
networkAddress = models.CharField(max_length=15)
size = models.IntegerField()
def __unicode__(self):
return "%s/%s" % (self.networkAddress, self.size)
class IpAddress(models.Model):
ipAddress = models.CharField(max_length=15)
subnet = models.ForeignKey(Subnet)
def __unicode__(self):
return "%s" % (self.ipAddress)
class Vlan(models.Model):
number = models.IntegerField()
description = models.CharField(max_length=150)
subnets = models.ManyToManyField(Subnet, related_name='vlans', blank=True)
def __unicode__(self):
return "VLAN %s (%s)" % (self.number, self.description)
admin.py
from network.models import Subnet, IpAddress, Vlan
from django.contrib import admin
class SubnetAdmin(admin.ModelAdmin):
filter_horizontal = ('vlans',)
admin.site.register(Subnet, SubnetAdmin)
admin.site.register(IpAddress)
admin.site.register(Vlan)
and the error I get
Request Method: GET
Request URL: http://127.0.0.1:8000/admin/
Django Version: 1.5.2
Exception Type: ImproperlyConfigured
Exception Value:
'SubnetAdmin.filter_horizontal' refers to field 'vlans' that is missing from model 'network.Subnet'.

Apparently this is an 8 year old feature request. There is django-admin-extend. Or you could just throw something like this in there:
from django.contrib import admin as admin_module
class SiteForm(ModelForm):
user_profiles = forms.ModelMultipleChoiceField(
label='Users granted access',
queryset=UserProfile.objects.all(),
required=False,
help_text='Admin users (who can access everything) not listed separately',
widget=admin_module.widgets.FilteredSelectMultiple('user profiles', False))
class SiteAdmin(admin_module.ModelAdmin):
fields = ('user_profiles',)
def save_model(self, request, obj, form, change):
# save without m2m field (can't save them until obj has id)
super(SiteAdmin, self).save_model(request, obj, form, change)
# if that worked, deal with m2m field
obj.user_profiles.clear()
for user_profile in form.cleaned_data['user_profiles']:
obj.user_profiles.add(user_profile)
def get_form(self, request, obj=None, **kwargs):
if obj:
self.form.base_fields['user_profiles'].initial = [ o.pk for o in obj.userprofile_set.all() ]
else:
self.form.base_fields['user_profiles'].initial = []
return super(SiteAdmin, self).get_form(request, obj, **kwargs)
It should give you a filter_horizontal when you specify it in the fields tuple.

I have created a public gist that covers this specific issue.
https://gist.github.com/Wtower/0b181cc06f816e4feac14e7c0aa2e9d0
The general idea is to use that specific base form class in order to define a 'reverse' m2m field for the form that would otherwise not include it. Then easily override the form in the admin class.
Although the code is not quite complicated, the gist code is somehow long to include within the answer so apologies for that.

Related

django admin panel healthcheck

I want to create a healthcheck endpoint for my django admin panel.
I want to register it via admin.site.register_view (I am using the adminplus package) but I can't figure out how to made it accessible to the public, without the need to authenticate first.
Any ideas?
So I ended up overriding def has_permission(self, request) in subclass of AdminPlusMixin:
from adminplus.sites import AdminPlusMixin
class MyAdmin(AdminPlusMixin, AdminSite):
def has_permission(self, request):
if request.resolver_match.url_name == 'admin-healthcheck':
return True
return super().has_permission(request)
I believe that package is outdated, instead you could do something like the following:
Created a proxy model in models.py such as:
class Proxy(models.Model):
id = models.BigAutoField(db_column='id', primary_key=True)
def str(self):
return "<Label: id: %d>" % self.id
class Meta:
managed = False
verbose_name_plural = 'proxies'
db_table = 'proxy'
ordering = ('id',)
Which is just a mysql view that a created from am existing table
create view proxy
as select id
from samples
LIMIT 10;
And finally in admin.py
#admin.register(Proxy)
class LabelAdmin(admin.ModelAdmin):
change_list_template = 'label_view.html'
def changelist_view(self, request, extra_context=None):
...
return render(request, "label_view.html", context)
This way it will show in the admin panel, inside the app you're working on.
Probably what you have is a function in your views.py, in this case you should replace that function content where the "..." are in the LabelAdmin class.

Django: ValidationError message not showing in admin changeform

I am working on a webapp where user can be a member of one (and only one) organisation - this is done with a foreignkey in the Profile model, which in turn has a one-to-one link with the default django.auth.user model. We also want to make sure that each email address is only used once within each organisation. To do this we added the following function to the Profile model:
def clean(self):
if self.organisation and Profile.objects.filter(
user__email=self.user.email,
organisation_id=self.organisation.id
).exists():
raise ValidationError({'user': _('The email address from this user is already used within this organisation!')})
return super(Profile, self).clean()
However, when I add a user through the Django admin using an duplicate email address all that gets displayed is a generic please fix the errors below message at the top of the form. No text is displayed near the email field and the ValidationError text isn't displayed at all - thus giving the admins no information on what actually went wrong.
Does anyone know why the ValidationError message isnt being displayed in the admin, and what steps we can take to rectify this?
We are using a standard ModelAdmin class
class ProfileAdmin(ModelAdmin):
def username(self, profile, **kwargs):
return u'{} ({})'.format(
profile.user.profile.full_name(),
profile.user.username)
search_fields = ['user__username', 'user__first_name', 'user__last_name', 'user__email']
list_display = ('username', 'organisation')
list_filter = ('organisation')
I think form validation is a good idea in these type of situations.
forms.py
class YourForm(forms.ModelForm):
def clean(self):
super(YourForm, self).clean()
data1 = self.cleaned_data.get('data1')
data2 = self.cleaned_data.get('data2')
# Add validation condition here
# if validation error happened you can raise the error
# and attach the error message with the field you want.
self.add_error('field_name', 'error message')
In admin.py
class YourAdminClass(admin.ModelAdmin):
form = YourForm
Raise ValidationError from ProfileAdmin class. For example, from clean_<field name> method.

A Category model which creates proxy models for related model admin

So I'm having a bit of trouble with trying to create a model that will define dynamic proxy models that manage a related model in the admin site. I know that sentence was confusing, so I'll just share my code instead.
models.py
class Cateogry(models.Model):
name = models.CharField(...)
class Tag(models.Model):
name = models.CharField(...)
category = models.ForeignKey(Cateogry)
What I want to achieve is that in the admin site, instead of having one ModelAdmin for the Tag model, for each category I will have a modeladmin for all related tags. I have achieved this using this answer. Say I have a category named A:
def create_modeladmin(modeladmin, model, name = None):
class Meta:
proxy = True
app_label = model._meta.app_label
attrs = {'__module__': '', 'Meta': Meta}
newmodel = type(name, (model,), attrs)
admin.site.register(newmodel, modeladmin)
return modeladmin
class CatA(TagAdmin):
def queryset(self, request):
qs = super(CatA, self).queryset(request)
return qs.filter(cateogry = Cateogry.objects.filter(name='A'))
create_modeladmin(CatA, name='CategoryAtags', model=Tag)
But this is not good enough, because obviously I still need to manually subclass the TagAdmin model and then run create_modeladmin. What I need to do, is loop over all Category objects, for each one create a dynamic subclass for Tagadmin (named after the category), then create a dynamic proxy model from that, and this is where my head starts spinning.
for cat in Category.objects.all():
NewSubClass = #somehow create subclass of TagAdmin, the name should be '<cat.name>Admin' instead of NewSubClass
create_modeladmin(NewSubClass, name=cat.name, model=Tag)
Any guidance or help would be much appreciated
Dynamic ModelAdmins don't work well together with the way admin registeres models.
I suggest to create subviews in the CategoryAdmin.
from django.conf.urls import patterns, url
from django.contrib import admin
from django.contrib.admin.options import csrf_protect_m
from django.contrib.admin.util import unquote
from django.core.urlresolvers import reverse
from demo_project.demo.models import Category, Tag
class TagAdmin(admin.ModelAdmin):
# as long as the CategoryTagAdmin class has no custom change_list template
# there needs to be a default admin for Tags
pass
admin.site.register(Tag, TagAdmin)
class CategoryTagAdmin(admin.ModelAdmin):
""" A ModelAdmin invoked by a CategoryAdmin"""
read_only_fields = ('category',)
def __init__(self, model, admin_site, category_admin, category_id):
self.model = model
self.admin_site = admin_site
self.category_admin = category_admin
self.category_id = category_id
super(CategoryTagAdmin, self).__init__(model, admin_site)
def queryset(self, request):
return super(CategoryTagAdmin, self).queryset(request).filter(category=self.category_id)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'tag_changelist_link')
def tag_changelist_link(self, obj):
info = self.model._meta.app_label, self.model._meta.module_name
return '<a href="%s" >Tags</a>' % reverse('admin:%s_%s_taglist' % info, args=(obj.id,))
tag_changelist_link.allow_tags = True
tag_changelist_link.short_description = 'Tags'
#csrf_protect_m
def tag_changelist(self, request, *args, **kwargs):
obj_id = unquote(args[0])
info = self.model._meta.app_label, self.model._meta.module_name
category = self.get_object(request, obj_id)
tag_admin = CategoryTagAdmin(Tag, self.admin_site, self, category_id=obj_id )
extra_context = {
'parent': {
'has_change_permission': self.has_change_permission(request, obj_id),
'opts': self.model._meta,
'object': category,
},
}
return tag_admin.changelist_view(request, extra_context)
def get_urls(self):
info = self.model._meta.app_label, self.model._meta.module_name
urls= patterns('',
url(r'^(.+)/tags/$', self.admin_site.admin_view(self.tag_changelist), name='%s_%s_taglist' % info )
)
return urls + super(CategoryAdmin, self).get_urls()
admin.site.register(Category, CategoryAdmin)
The items in the categories changelist have an extra column with a link made by the tag_changelist_link pointing to the CategoryAdmin.tag_changelist. This method creates a CategoryTagAdmin instance with some extras and returns its changelist_view.
This way you have a filtered tag changelist on every category. To fix the breadcrumbs of the tag_changelist view you need to set the CategoryTagAdmin.change_list_template to a own template that {% extends 'admin/change_list.html' %} and overwrites the {% block breadcrumbs %}. That is where you will need the parent variable from the extra_context to create the correct urls.
If you plan to implement a tag_changeview and tag_addview method you need to make sure that the links rendered in variouse admin templates point to the right url (e.g. calling the change_view with a form_url as paramter).
A save_model method on the CategoryTagAdmin can set the default category when adding new tags.
def save_model(self, request, obj, form, change):
obj.category_id = self.category_id
super(CategoryTagAdmin, self).__init__(request, obj, form, change)
If you still want to stick to the apache restart aproach ... Yes you can restart Django. It depends on how you are deploying the instance.
On an apache you can touch the wsgi file that will reload the instance os.utime(path/to/wsgi.py.
When using uwsgi you can use uwsgi.reload().
You can check the source code of Rosetta how they are restarting the instance after the save translations (views.py).
So I found a half-solution.
def create_subclass(baseclass, name):
class Meta:
app_label = 'fun'
attrs = {'__module__': '', 'Meta': Meta, 'cat': name }
newsub = type(name, (baseclass,), attrs)
return newsub
class TagAdmin(admin.ModelAdmin):
list_display = ('name', 'category')
def get_queryset(self, request):
return Tag.objects.filter(category = Category.objects.filter(name=self.cat))
for cat in Category.objects.all():
newsub = create_subclass(TagAdmin, str(cat.name))
create_modeladmin(newsub, model=Tag, name=str(cat.name))
It's working. But every time you add a new category, you need to refresh the server before it shows up (because admin.py is evaluated at runtime). Does anyone know a decent solution to this?

Django CreateView validate field not in fields

So, I have a Django generic view:
class Foobaz(models.Model):
name = models.CharField(max_length=140)
organisation = models.ForeignKey(Organisation)
class FoobazForm(forms.ModelForm):
class Meta:
model = Foobaz
fields = ('name')
class FoobazCreate(CreateView):
form_class = FoobazForm
#login_required
def dispatch(self, *args, **kwargs):
return super(FoobazCreate, self).dispatch(*args, **kwargs)
What I'm trying to do is to take the organisation id from the URL:
/organisation/1/foobaz/create/
And add it back to the created object. I realise I can do this in CreateView.form_valid(), but from what I understand this is then completely unvalidated.
I've tried adding it to get_form_kwargs() but this does not expect the organisation kwarg as it is not in the included fields.
Ideally what I'd like to do is to add it to the instance of the form to validate it with the rest - ensuring it is a valid organisation, and that the user in question has the correct permissions to add a new foobaz to it.
I'm happy to just roll my own view if that is the best way of doing this, but I may just be simply missing a trick.
Thanks!
I thnik it would be better to include the organisation field and define it as hidden and readonly, this way django will validate it for you.
You can then override get_queryset method like this:
def get_queryset(self):
return Foobaz.objects.filter(
organisation__id=self.kwargs['organisation_id'])
where organisation_id is a keyword in url pattern.
You can do it overriding the View's get_kwargs() method and the Form's save() method. In the get_kwargs() I "inject" the organization_id into the initial data of the form, and in save() I retrieve the missing info with the supplied initial data:
In urls.py:
urlpatterns('',
#... Capture the organization_id
url(r'^/organisation/(?P<organization_id>\d+)/foobaz/create/',
FoobazCreate.as_view()),
#...
)
In views.py:
class FoobazCreate(CreateView):
# Override get_kwargs() so you can pass
# extra info to the form (via 'initial')
# ...(all your other code remains the same)
def get_form_kwargs(self):
# get CreateView kwargs
kw = super(CreateComment, self).get_form_kwargs()
# Add any kwargs you need:
kw['initial']['organiztion_id'] = self.kwargs['organization_id']
# Or, altenatively, pass any View kwarg to the Form:
# kw['initial'].update(self.kwargs)
return kw
In forms.py:
class FoobazForm(forms.ModelForm):
# Override save() so that you can add any
# missing field in the form to the model
# ...(Idem)
def save(self, commit=True):
org_id = self.initial['organization_id']
self.instance.organization = Organization.objects.get(pk=org_id)
return super(FoobazForm, self).save(commit=commit)

How to limit fields in django-admin depending on user?

I suppose similar problem would have been discussed here, but I couldn't find it.
Let's suppose I have an Editor and a Supervisor. I want the Editor to be able to add new content (eg. a news post) but before publication it has to be acknowledged by Supervisor.
When Editor lists all items, I want to set some fields on the models (like an 'ack' field) as read-only (so he could know what had been ack'ed and what's still waiting approval) but the Supervisor should be able to change everything (list_editable would be perfect)
What are the possible solutions to this problem?
I think there is a more easy way to do that:
Guest we have the same problem of Blog-Post
blog/models.py:
Class Blog(models.Model):
...
#fields like autor, title, stuff..
...
class Post(models.Model):
...
#fields like blog, title, stuff..
...
approved = models.BooleanField(default=False)
approved_by = models.ForeignKey(User)
class Meta:
permissions = (
("can_approve_post", "Can approve post"),
)
And the magic is in the admin:
blog/admin.py:
...
from django.views.decorators.csrf import csrf_protect
...
def has_approval_permission(request, obj=None):
if request.user.has_perm('blog.can_approve_post'):
return True
return False
Class PostAdmin(admin.ModelAdmin):
#csrf_protect
def changelist_view(self, request, extra_context=None):
if not has_approval_permission(request):
self.list_display = [...] # list of fields to show if user can't approve the post
self.editable = [...]
else:
self.list_display = [...] # list of fields to show if user can approve the post
return super(PostAdmin, self).changelist_view(request, extra_context)
def get_form(self, request, obj=None, **kwargs):
if not has_approval_permission(request, obj):
self.fields = [...] # same thing
else:
self.fields = ['approved']
return super(PostAdmin, self).get_form(request, obj, **kwargs)
In this way you can use the api of custom permission in django, and you can override the methods for save the model or get the queryset if you have to. In the methid has_approval_permission you can define the logic of when the user can or can't to do something.
Starting Django 1.7, you can now use the get_fields hook which makes it so much simpler to implement conditional fields.
class MyModelAdmin(admin.ModelAdmin):
...
def get_fields(self, request, obj=None):
fields = super(MyModelAdmin, self).get_fields(request, obj)
if request.user.is_superuser:
fields += ('approve',)
return fields
I have a system kind of like this on a project that I'm just finishing up. There will be a lot of work to put this together, but here are some of the components that I had to make my system work:
You need a way to define an Editor and a Supervisor. The three ways this could be done are 1.) by having an M2M field that defines the Supervisor [and assuming that everyone else with permission to read/write is an Editor], 2.) make 2 new User models that inherit from User [probably more work than necessary] or 3.) use the django.auth ability to have a UserProfile class. Method #1 is probably the most reasonable.
Once you can identify what type the user is, you need a way to generically enforce the authorization you're looking for. I think the best route here is probably a generic admin model.
Lastly you'll need some type of "parent" model that will hold the permissions for whatever needs to be moderated. For example, if you had a Blog model and BlogPost model (assuming multiple blogs within the same site), then Blog is the parent model (it can hold the permissions of who approves what). However, if you have a single blog and there is no parent model for BlogPost, we'll need some place to store the permissions. I've found the ContentType works out well here.
Here's some ideas in code (untested and more conceptual than actual).
Make a new app called 'moderated' which will hold our generic stuff.
moderated.models.py
class ModeratedModelParent(models.Model):
"""Class to govern rules for a given model"""
content_type = models.OneToOneField(ContentType)
can_approve = models.ManyToManyField(User)
class ModeratedModel(models.Model):
"""Class to implement a model that is moderated by a supervisor"""
is_approved = models.BooleanField(default=False)
def get_parent_instance(self):
"""
If the model already has a parent, override to return the parent's type
For example, for a BlogPost model it could return self.parent_blog
"""
# Get self's ContentType then return ModeratedModelParent for that type
self_content_type = ContentType.objects.get_for_model(self)
try:
return ModeratedModelParent.objects.get(content_type=self_content_type)
except:
# Create it if it doesn't already exist...
return ModeratedModelParent.objects.create(content_type=self_content_type).save()
class Meta:
abstract = True
So now we should have a generic, re-usable bit of code that we can identify the permission for a given model (which we'll identify the model by it's Content Type).
Next, we can implement our policies in the admin, again through a generic model:
moderated.admin.py
class ModeratedModelAdmin(admin.ModelAdmin):
# Save our request object for later
def __call__(self, request, url):
self.request = request
return super(ModeratedModelAdmin, self).__call__(request, url)
# Adjust our 'is_approved' widget based on the parent permissions
def formfield_for_dbfield(self, db_field, **kwargs):
if db_field.name == 'is_approved':
if not self.request.user in self.get_parent_instance().can_approve.all():
kwargs['widget'] = forms.CheckboxInput(attrs={ 'disabled':'disabled' })
# Enforce our "unapproved" policy on saves
def save_model(self, *args, **kwargs):
if not self.request.user in self.get_parent_instance().can_approve.all():
self.is_approved = False
return super(ModeratedModelAdmin, self).save_model(*args, **kwargs)
Once these are setup and working, we can re-use them across many models as I've found once you add structured permissions for something like this, you easily want it for many other things.
Say for instance you have a news model, you would simply need to make it inherit off of the model we just made and you're good.
# in your app's models.py
class NewsItem(ModeratedModel):
title = models.CharField(max_length=200)
text = models.TextField()
# in your app's admin.py
class NewsItemAdmin(ModeratedModelAdmin):
pass
admin.site.register(NewsItem, NewsItemAdmin)
I'm sure I made some code errors and mistakes in there, but hopefully this can give you some ideas to act as a launching pad for whatever you decide to implement.
The last thing you have to do, which I'll leave up to you, is to implement filtering for the is_approved items. (ie. you don't want un-approved items being listed on the news section, right?)
The problem using the approach outlined by #diegueus9 is that the ModelAdmin acts liked a singleton and is not instanced for each request. This means that each request is modifying the same ModelAdmin object that is being accessed by other requests, which isn't ideal. Below is the proposed solutions by #diegueus9:
# For example, get_form() modifies the single PostAdmin's fields on each request
...
class PostAdmin(ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if not has_approval_permission(request, obj):
self.fields = [...] # list of fields to show if user can't approve the post
else:
self.fields = ['approved', ...] # add 'approved' to the list of fields if the user can approve the post
...
An alternative approach would be to pass fields as a keyword arg to the parent's get_form() method like so:
...
from django.contrib.admin.util import flatten_fieldsets
class PostAdmin(ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if has_approval_permission(request, obj):
fields = ['approved']
if self.declared_fieldsets:
fields += flatten_fieldsets(self.declared_fieldsets)
# Update the keyword args as needed to allow the parent to build
# and return the ModelForm instance you require for the user given their perms
kwargs.update({'fields': fields})
return super(PostAdmin, self).get_form(request, obj=None, **kwargs)
...
This way, you are not modifying the PostAdmin singleton on every request; you are simply passing the appropriate keyword args needed to build and return the ModelForm from the parent.
It is probably worth looking at the get_form() method on the base ModelAdmin for more info: https://code.djangoproject.com/browser/django/trunk/django/contrib/admin/options.py#L431