I have a wagtail site and have a problem with the ‘users’ section of the admin page
My users/admin.py is :
from django.contrib import admin
from django.contrib.auth import admin as auth_admin
from django.contrib.auth import get_user_model
from psymatik.users.forms import (
UserChangeForm,
UserCreationForm,
)
User = get_user_model()
admin.site.register(User)
class UserAdmin(auth_admin.UserAdmin):
form = UserChangeForm
add_form = UserCreationForm
fieldsets = (
("User", {"fields": ("username",)}),
) + auth_admin.UserAdmin.fieldsets
list_display = ["username", "is_superuser"]
search_fields = ["username"]
And my users/wagtail_hooks.py is:
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from .models import User
class UserAdmin(ModelAdmin):
model = User
menu_label = "Users"
menu_icon = "pick"
menu_order = 200
add_to_settings_menu = False
exclude_from_explorer = False
list_display = ( "name")
list_filter = ("name")
search_fields = ("name")
modeladmin_register(UserAdmin)
The issues I have is that when I am at admin/users and I click on the Users link in the sidebar I am taken to admin/users/user and get the error
“ValueError at /admin/users/user/
Field 'id' expected a number but got 'user’.”
Why is the sidebar link pointing to admin/users/user rather than just admin/users (which does work)? What is the best way to set this up?
When you register a model with ModelAdmin, the URLs will be formed from the app name ('users' here) and the model name ('user'), so /admin/users/user is expected. However, in this case Wagtail already provides a user management area (available from the Settings submenu) that exists under the /admin/users/ URL namespace - these URLs end up colliding with the ones you add through ModelAdmin.
It looks like your UserAdmin definition is more or less duplicating the functionality already provided by Wagtail, so you may not need this at all. If you do, one thing that might work is editing the INSTALLED_APPS setting in your project's settings to move your users app above wagtail.contrib.users - that way, the URL patterns for your own app will take precedence over Wagtail's built in /admin/users/ area, and it will correctly interpret anything under /admin/users/user/ as belonging to your own app (while letting all other URLs under /admin/users/ fall back to the Wagtail built-in area).
If that doesn't work, you'd need to either rename your users app (easier said than done for an established project...) or customise the ModelAdmin setup to use an alternative URL path. It doesn't look like ModelAdmin currently provides an official mechanism to do that, but overriding the AdminURLHelper object ought to work - within users/wagtail_hooks.py:
from wagtail.contrib.modeladmin.helpers import AdminURLHelper
class UsersAdminURLHelper(AdminURLHelper):
def _get_action_url_pattern(self, action):
# hard-code 'user-accounts' as the URL path instead of /users/user/
if action == "index":
return r"^user-accounts/$"
return r"^user-accounts/%s/$" % action
class UserAdmin(ModelAdmin):
model = User
url_helper_class = UsersAdminURLHelper
# all other settings as before
Incidentally, users/admin.py is unrelated here - it controls the Django admin backend, which is distinct from the Wagtail admin.
Related
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'm trying to get django-fmc set up with Django (v 1.97, Python v2.7.12, djangorestframework v3.3.3) to handle storing registration ids and sending notifications to devices. I am following the tutorial they provide but it doesn't seem to be working.
I am getting the following error when running my local server and python manage.py fcm_urls:
...
File "C:\Work\Dev\LiveTracking\Api\app\views.py", line 50, in DeviceViewSet
queryset = Device.objects.all()
File "C:\Work\Dev\LiveTracking\Api\env\lib\site-packages\django\db\models\manager.py", line 277, in __get__
self.model._meta.swapped,
AttributeError: Manager isn't available; 'fcm.Device' has been swapped for 'app.MyDevice'
I don't want to add additional fields to the MyDevice model for now. I've looked all over but can't fix this error. If anyone can shed some insight into this error it would be much appreciated.
Here are some of my code snippets:
settings.py
INSTALLED_APPS = (
'fcm',
)
# Firebase Cloud Messaging Key
FCM_APIKEY = 'AIzaSyCaqHZIcaGDOpfTZUmAHEowsqD-fCtow6A'
# Location of device model
FCM_DEVICE_MODEL = 'app.MyDevice'
serializers.py
from fcm.models import Device
class DeviceSerializer(serializers.ModelSerializer):
class Meta:
model = Device
fields = ('dev_id','reg_id','name','is_active')
views.py
from rest_framework import viewsets
from fcm.models import Device
from fcm.serializers import DeviceSerializer
class DeviceViewSet(viewsets.ModelViewSet):
queryset = Device.objects.all()
serializer_class = DeviceSerializer
urls.py
from rest_framework import routers
from fcm.views import DeviceViewSet
router = routers.DefaultRouter()
router.register(r'devices', DeviceViewSet)
urlpatterns = [
url(r'^v1/', include(router.urls)),
]
swappable is an undocumented feature, actually only supposed to be used for custom User models. The doc on custom user models clearly states that once you use a custom user model, directly referencing contrib.auth.models.User won't work:
If you reference User directly (for example, by referring to it in a foreign key), your code will not work in projects where the AUTH_USER_MODEL setting has been changed to a different user model.
You probably want to read the rest of this chapter FWIW.
To make a long story short: as Daniel Roseman mentions, you very probably want to use your own MyDevice model instead of the default Device one. And eventually contribute back a patch to django-fcm doc if it solves the issue.
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 developing a Django app that will have two administration backends. One for daily use by "normal" users and the default one for more advanced tasks and for the developers.
The application uses some custom permissions but none of the default ones. So I'm currently looking for a way to remove the default permissions, or at least a way to hide them from the "daily" admin backend without large modifications.
UPDATE: Django 1.7 supports the customization of default permissions
Original Answer
The following is valid for Django prior to version 1.7
This is standard functionality of the auth contrib application.
It handles the post_syncdb signal and creates the permissions (the standard 3: add, change, delete, plus any custom ones) for each model; they are stored in the auth_permission table in the database.
So, they will be created each time you run the syncdb management command
You have some choices. None is really elegant, but you can consider:
Dropping the auth contrib app and provide your own authentication backend.
Consequences -> you will lose the admin and other custom apps built on top of the auth User model, but if your application is highly customized that could be an option for you
Overriding the behaviour of the post_syncdb signal inside the auth app (inside \django\contrib\auth\management__init__.py file)
Consequences -> be aware that without the basic permissions the Django admin interface won't be able to work (and maybe other things as well).
Deleting the basic permissions (add, change, delete) for each model inside the auth_permission table (manually, with a script, or whatever).
Consequences -> you will lose the admin again, and you will need to delete them each time you run syncdb.
Building your own Permission application/system (with your own decorators, middlewares, etc..) or extending the existing one.
Consequences -> none, if you build it well - this is one of the cleanest solutions in my opinion.
A final consideration: changing the contrib applications or Django framework itself is never considered a good thing: you could break something and you will have hard times if you will need to upgrade to a newer version of Django.
So, if you want to be as clean as possibile, consider rolling your own permission system, or extending the standard one (django-guardian is a good example of an extension to django permissions). It won't take much effort, and you can build it the way it feels right for you, overcoming the limitations of the standard django permission system. And if you do a good work, you could also consider to open source it to enable other people using/improving your solution =)
I struggled with this same problem for a while and I think I've come up with a clean solution. Here's how you hide the permissions for Django's auth app:
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from django import forms
from django.contrib.auth.models import Permission
class MyGroupAdminForm(forms.ModelForm):
class Meta:
model = MyGroup
permissions = forms.ModelMultipleChoiceField(
Permission.objects.exclude(content_type__app_label='auth'),
widget=admin.widgets.FilteredSelectMultiple(_('permissions'), False))
class MyGroupAdmin(admin.ModelAdmin):
form = MyGroupAdminForm
search_fields = ('name',)
ordering = ('name',)
admin.site.unregister(Group)
admin.site.register(MyGroup, MyGroupAdmin)
Of course it can easily be modified to hide whatever permissions you want. Let me know if this works for you.
A new feature introduced in Django 1.7 is the ability to define the default permissions. As stated in the documentation if you set this to empty none of the default permissions will be created.
A working example would be:
class Blar1(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255, unique = True, blank = False, null = False, verbose_name= "Name")
class Meta:
default_permissions = ()
ShadowCloud gave a good rundown. Here's a simple way to accomplish your goal.
Add these line in your admin.py:
from django.contrib.auth.models import Permission
admin.site.register(Permission)
You can now add/change/delete permissions in the admin. Remove the unused ones and when you have what you want, go back and remove these two lines from admin.py.
As was mentioned by others, a subsequent syncdb will put everything back.
Built on top of the solution by #pmdarrow, I've come up with a relatively clean solution to patch the Django admin views.
See: https://gist.github.com/vdboor/6280390
It extends the User and Group admin to hide certain permissions.
You can't easily delete those permissions (so that syncdb won't put them back), but you can hide them from the admin interface. The idea is, as described by others, to override the admin forms but you have to do this for both users and groups.
Here is the admin.py with the solution:
from django import forms
from django.contrib import admin
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User, Group
from django.contrib.auth.admin import GroupAdmin, UserAdmin
from django.contrib.auth.forms import UserChangeForm
#
# In the models listed below standard permissions "add_model", "change_model"
# and "delete_model" will be created by syncdb, but hidden from admin interface.
# This is convenient in case you use your own set of permissions so the list
# in the admin interface wont be confusing.
# Feel free to add your models here. The first element is the app name (this is
# the directory your app is in) and the second element is the name of your model
# from models.py module of your app (Note: both names must be lowercased).
#
MODELS_TO_HIDE_STD_PERMISSIONS = (
("myapp", "mymodel"),
)
def _get_corrected_permissions():
perms = Permission.objects.all()
for app_name, model_name in MODELS_TO_HIDE_STD_PERMISSIONS:
perms = perms.exclude(content_type__app_label=app_name, codename='add_%s' % model_name)
perms = perms.exclude(content_type__app_label=app_name, codename='change_%s' % model_name)
perms = perms.exclude(content_type__app_label=app_name, codename='delete_%s' % model_name)
return perms
class MyGroupAdminForm(forms.ModelForm):
class Meta:
model = Group
permissions = forms.ModelMultipleChoiceField(
_get_corrected_permissions(),
widget=admin.widgets.FilteredSelectMultiple(('permissions'), False),
help_text = 'Hold down "Control", or "Command" on a Mac, to select more than one.'
)
class MyGroupAdmin(GroupAdmin):
form = MyGroupAdminForm
class MyUserChangeForm(UserChangeForm):
user_permissions = forms.ModelMultipleChoiceField(
_get_corrected_permissions(),
widget=admin.widgets.FilteredSelectMultiple(('user_permissions'), False),
help_text = 'Hold down "Control", or "Command" on a Mac, to select more than one.'
)
class MyUserAdmin(UserAdmin):
form = MyUserChangeForm
admin.site.unregister(Group)
admin.site.register(Group, MyGroupAdmin)
admin.site.unregister(User)
admin.site.register(User, MyUserAdmin)
If you are creating your own user management backend and only want to show your custom permissions you can filter out the default permissions by excluding permission with a name that starts with "Can".
WARNING:
You must remember not to name your permissions starting with "Can"!!!!
If they decide to change the naming convention this might not work.
With credit to pmdarrow this is how I did this in my project:
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.models import Permission
from django.contrib import admin
class UserEditForm(UserChangeForm):
class Meta:
model = User
exclude = (
'last_login',
'is_superuser',
'is_staff',
'date_joined',
)
user_permissions = forms.ModelMultipleChoiceField(
Permission.objects.exclude(name__startswith='Can'),
widget=admin.widgets.FilteredSelectMultiple(_('permissions'), False))
If you want to prevent Django from creating permissions, you can block the signals from being sent.
If you put this into a management/init.py in any app, it will bind to the signal handler before the auth framework has a chance (taking advantage of the dispatch_uid debouncing).
from django.db.models import signals
def do_nothing(*args, **kwargs):
pass
signals.post_syncdb.connect(do_nothing, dispatch_uid="django.contrib.auth.management.create_permissions")
signals.post_syncdb.connect(do_nothing, dispatch_uid="django.contrib.auth.management.create_superuser")
Alright, I have a fairly simple design.
class Update(models.Model):
pub_date = models.DateField()
title = models.CharField(max_length=512)
class Post(models.Model):
update = models.ForeignKey(Update)
body = models.TextField()
order = models.PositiveIntegerField(blank=True)
class Media(models.Model):
post = models.ForeignKey(Post)
thumb = models.ImageField(upload_to='frontpage')
fullImagePath = models.ImageField(upload_to='frontpage')
Is there an easy-ish way to allow a user to create an update all on one page?
What I want is for a user to be able to go to the admin interface, add a new Update, and then while editing an Update add one or more Posts, with each Post having one or more Media items. In addition, I want the user to be able to reorder Posts within an update.
My current attempt has the following in admin.py:
class MediaInline(admin.StackedInline):
model = Media
class PostAdmin(admin.ModelAdmin):
inlines = [MediaInline,]
This let's the user add a new Post item, select the relevant Update, add the Media items to it, and hit save - which is fine. But there's no way to see all the Posts that belong to a given Update in a single place, which in turn means you can't roderder Posts within an update. It's really quite confusing for the end user.
Help?
As of now there is no "built-in" way to have nested inlines (inline inside inline) in django.contrib.admin. Pulling something like this off is possible by having your own ModelAdmin and InlineModelAdmin subclasses that would enable this kind of functionality. See the patches on this ticket http://code.djangoproject.com/ticket/9025 for ideas on how to implement this. You'd also need to provide your own templates that would have nested iteration over both the top level inline and it's child inline.
There is now this egg available, which is a collation of the relevant patches mentioned in the other answer:
https://github.com/theatlantic/django-nested-admin
I have done this using https://github.com/theatlantic/django-nested-admin, for the following Data structure:
Contest
Judges
Contestants
Singers
Songs
My admin.pyfile:
from django.contrib import admin
import nested_admin
from .models import Contest, Contestant, Judge, Song, Singer
class SongInline(nested_admin.NestedTabularInline):
model = Song
extra = 0
class SingerInline(nested_admin.NestedTabularInline):
model = Singer
extra = 0
class ContestantInline(nested_admin.NestedTabularInline):
model = Contestant
inlines = [SongInline, SingerInline]
extra = 0
class JudgeInline(nested_admin.NestedTabularInline):
model = Judge
extra = 0
class ContestAdmin(nested_admin.NestedModelAdmin):
model = Contest
inlines = [ContestantInline, JudgeInline]
extra = 0
admin.site.register(Contest, ContestAdmin)
https://github.com/theatlantic/django-nested-admin appears to be much more actively maintained than the other apps already mentioned (https://github.com/BertrandBordage/django-super-inlines and https://github.com/Soaa-/django-nested-inlines)
I have just ran into this issue as well... Seems this thread which contains the request for the nested inlines feature (https://code.djangoproject.com/ticket/9025#no2) has been updated with further information.
A custom made app called "django-super-inline" has been released. More details here: https://github.com/BertrandBordage/django-super-inlines
Installation and usage instructions below.
Hope this is useful for whomever comes across this.
I ran into a similar issue to this. My approach was to make an UpdateAdmin that held inlines for both Media and Post... it basically just makes it so you have a list of all of the media entries followed by all of the posts in an update.
class MediaInline(admin.StackedInline):
model = Media
class PostInline(admin.StackedInline):
model = Post
class PostAdmin(admin.ModelAdmin):
inlines = [MediaInline,]
class UpdateAdmin(admin.ModelAdmin):
inlines = [MediaInline,PostInline]
It isn't an ideal solution but it works for a quick and dirty work around.
Use django-nested-admin which is the best package to do nested inlines.
First, install "django-nested-admin":
pip install django-nested-admin
Then, add "nested_admin" to "INSTALLED_APPS" in "settings.py":
# "settings.py"
INSTALLED_APPS = (
# ...
"nested_admin", # Here
)
Then, add "path('_nested_ad..." to "urlpatterns" in "urls.py":
# "urls.py"
from django.urls import include, path
urlpatterns = [
# ...
path('_nested_admin/', include('nested_admin.urls')), # Here
]
Finally, extend "NestedTabularInline" with "MediaInline()" and "PostInline()" classes and extend "NestedModelAdmin" with "UpdateAdmin()" class in "admin.py" as shown below:
# "admin.py"
from .models import Media, Post, Update
from nested_admin import NestedTabularInline, NestedModelAdmin
class MediaInline(NestedTabularInline):
model = Media
class PostInline(NestedTabularInline):
model = Post
inlines = [MediaInline]
#admin.register(Update)
class UpdateAdmin(NestedModelAdmin):
inlines = [PostInline]