How can I integrate Django Import Export and Wagtail? - django

I'm trying to add a model resource from django-import-export into the admin for Wagtail. The only documentation I can find says that you would do it through hooks. The problem is, I keep getting the error:
missing 2 required positional arguments: 'model' and 'admin_site'
The whole resource and ModelAdmin are:
class AccountResource(resources.ModelResource):
class Meta:
model = Account
fields = ('first_name', 'last_name', 'email', 'created', 'archived')
class AccountsAdmin(ImportExportModelAdmin, ModelAdmin):
resource_class = AccountResource
model = Account
menu_label = 'Accounts' # ditch this to use verbose_name_plural from model
menu_icon = 'group' # change as required
menu_order = 200 # will put in 3rd place (000 being 1st, 100 2nd)
add_to_settings_menu = False # or True to add your model to the Settings sub-menu
exclude_from_explorer = False # or True to exclude pages of this type from Wagtail's explorer view
list_display = ('first_name', 'last_name', 'email', 'created', 'archived')
search_fields = ('first_name', 'last_name', 'email', 'created')
# Now you just need to register your customised ModelAdmin class with Wagtail
modeladmin_register(AccountsAdmin)
Any suggestions?

The Wagtail ModelAdmin does not share the API of the Django ModelAdmin.
The mixins from django-import-export expect to be used with a Django ModelAdmin and won't work with Wagtail ModelAdmin as you have experienced yourself.
For the export functionality, I have solved the problem by hooking the export_action of a Django ModelAdmin with the ExportMixin into the urls of a Wagtail ModelAdmin.
This might not be too pretty, but allows reusing the view and the logic that is part of the ExportMixin.
I have published an example Project on GitHub that makes use of this design.
The actual glue code that can be found here is not all that much:
from django.conf.urls import url
from django.contrib.admin import ModelAdmin as DjangoModelAdmin
from django.core.exceptions import ImproperlyConfigured
from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _
from import_export.admin import ExportMixin
from wagtail.contrib.modeladmin.helpers import ButtonHelper
class ExporterDummySite:
name = None
def each_context(self, request):
return {}
class WMAExporter(ExportMixin, DjangoModelAdmin):
export_template_name = 'wma_export/export.html'
def __init__(self, wagtail_model_admin):
self.wagtail_model_admin = wagtail_model_admin
super().__init__(wagtail_model_admin.model, ExporterDummySite())
def get_export_queryset(self, request):
index_view = self.wagtail_model_admin.index_view_class(
model_admin=self.wagtail_model_admin
)
index_view.dispatch(request)
return index_view.get_queryset(request)
class ExportButtonHelper(ButtonHelper):
export_button_classnames = ['bicolor', 'icon', 'icon-download']
def export_button(self, classnames_add=None, classnames_exclude=None):
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
classnames_exclude = []
classnames = self.export_button_classnames + classnames_add
cn = self.finalise_classname(classnames, classnames_exclude)
return {
'url': self.url_helper.get_action_url("export") + '?' + urlencode(self.view.params),
'label': _('Export'),
'classname': cn,
'title': _('Export these %s') % self.verbose_name_plural,
}
class WMAExportMixin:
button_helper_class = ExportButtonHelper
exporter_class = None
def get_admin_urls_for_registration(self):
return super().get_admin_urls_for_registration() + (
url(self.url_helper._get_action_url_pattern("export"),
self.export_view,
name=self.url_helper.get_action_url_name("export")),
)
def export_view(self, request):
if self.exporter_class is None:
raise ImproperlyConfigured(f"{self.__class__.__name__}.exporter_class not set!")
exporter = self.exporter_class(self)
return exporter.export_action(request)
I would assume that something similar can be done to implement the import part.

Related

Combine two django models with shared ids into a single viewset

I have two django models in two independent apps, who use the same user ids from an external authentication service:
In app1/models.py:
class App1User(models.Model):
user_id = models.UUIDField(unique=True)
app1_field = models.BooleanField()
In app2/models.py:
class App2User(models.Model):
user_id = models.UUIDField(unique=True)
app2_field = models.BooleanField()
I would like to have a combined viewset that can make it seem like these two are a single model with a list response as follows:
[
{
'user_id': ...,
'app1_field': ...,
'app2_field': ...
},
...
]
If I create or update with this viewset, it should save the data to each of the two models.
To create a combined viewset for App1User and App2User models, you can
follow these steps:
Create a serializer for the combined model. In your main app, create a
serializers.py file and define the following serializer:
from rest_framework import serializers
from app1.models import App1User
from app2.models import App2User
class App1UserSerializer(serializers.ModelSerializer):
class Meta:
model = App1User
fields = ('user_id', 'app1_field')
class App2UserSerializer(serializers.ModelSerializer):
class Meta:
model = App2User
fields = ('user_id', 'app2_field')
class CombinedUserSerializer(serializers.Serializer):
user_id = serializers.UUIDField()
app1_field = serializers.BooleanField()
app2_field = serializers.BooleanField()
def create(self, validated_data):
app1_data = {'user_id': validated_data['user_id'], 'app1_field': validated_data['app1_field']}
app2_data = {'user_id': validated_data['user_id'], 'app2_field': validated_data['app2_field']}
app1_serializer = App1UserSerializer(data=app1_data)
app2_serializer = App2UserSerializer(data=app2_data)
if app1_serializer.is_valid() and app2_serializer.is_valid():
app1_serializer.save()
app2_serializer.save()
return validated_data
def update(self, instance, validated_data):
app1_instance = App1User.objects.get(user_id=validated_data['user_id'])
app2_instance = App2User.objects.get(user_id=validated_data['user_id'])
app1_serializer = App1UserSerializer(app1_instance, data={'app1_field': validated_data['app1_field']})
app2_serializer = App2UserSerializer(app2_instance, data={'app2_field': validated_data['app2_field']})
if app1_serializer.is_valid() and app2_serializer.is_valid():
app1_serializer.save()
app2_serializer.save()
return validated_data
This serializer defines the structure of the combined model and uses App1UserSerializer and App2UserSerializer to handle the fields of each model.
Create a viewset for the combined model. In your main app, create a viewsets.py file and define the following viewset:
from rest_framework import viewsets
from app1.models import App1User
from app2.models import App2User
from .serializers import CombinedUserSerializer
class CombinedUserViewSet(viewsets.ModelViewSet):
serializer_class = CombinedUserSerializer
def get_queryset(self):
app1_users = App1User.objects.all()
app2_users = App2User.objects.all()
combined_users = []
for app1_user in app1_users:
app2_user = app2_users.filter(user_id=app1_user.user_id).first()
if app2_user:
combined_user = {
'user_id': app1_user.user_id,
'app1_field': app1_user.app1_field,
'app2_field': app2_user.app2_field
}
combined_users.append(combined_user)
return combined_users
This viewset uses CombinedUserSerializer to handle the requests and retrieves the data from both App1User and App2User models. The get_queryset() method retrieves all the App1User and App2User instances, matches them based on their user_id, and creates a list of combined users.
Register the viewset in your main app's urls.py file:
# main_app/urls.py
from django.urls import include, path
from rest_framework import routers
from .views import CombinedUserViewSet
router = routers.DefaultRouter()
router.register(r'combined-users', CombinedUserViewSet)
urlpatterns = [
path('', include(router.urls)),
]
This code sets up a default router and registers the CombinedUserViewSet with the endpoint /combined-users/, which can be used to access the combined model data.
Above solution is the idea how it can be done, you can modify according to your needs. If this idea helps, plz mark this as accept solution.

Django Rest Auth - Custom registration logic for social views

I'm building a REST API with Django Rest Framework and Django Rest Auth.
My users have a consumer profile.
class UserConsumerProfile(
SoftDeletableModel,
TimeStampedModel,
UniversallyUniqueIdentifiable,
Userable,
models.Model
):
def __str__(self):
return f'{self.user.email} ({str(self.uuid)})'
As you can see it consists of some mixins that give it a UUID, a timestamp and an updated field and a OneToOne relationship to the user. I use this consumerprofile in relations to link data to the user.
This consumerprofile should get created as soon as a user signs up.
Here is the serializer that I wrote for the registration:
from profiles.models import UserConsumerProfile
from rest_auth.registration.serializers import RegisterSerializer
class CustomRegisterSerializer(RegisterSerializer):
def custom_signup(self, request, user):
profile = UserConsumerProfile.objects.create(user=user)
profile.save()
I connected this serializer in the settings:
REST_AUTH_REGISTER_SERIALIZERS = {
"REGISTER_SERIALIZER": "accounts.api.serializers.CustomRegisterSerializer"
}
It works flawlessly when the users signs up using his email. But when he signs up using facebook, no consumer profile gets created.
I thought the social view would also use the register serializer when creating users? How can I run custom logic after a social sign up?
EDIT for the bounty:
Here are the settings that I use for Django Rest Auth:
# django-allauth configuration
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_CONFIRM_EMAIL_ON_GET = True
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 1
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_ADAPTER = 'accounts.adapter.CustomAccountAdapter'
SOCIALACCOUNT_ADAPTER = 'accounts.adapter.CustomSocialAccountAdapter'
SOCIALACCOUNT_PROVIDERS = {
'facebook': {
'METHOD': 'oauth2',
'SCOPE': ['email', 'public_profile', 'user_friends'],
'AUTH_PARAMS': {'auth_type': 'reauthenticate'},
'INIT_PARAMS': {'cookie': True},
'FIELDS': [
'id',
'email',
'name',
'first_name',
'last_name',
'verified',
'locale',
'timezone',
'link',
'gender',
'updated_time',
],
'EXCHANGE_TOKEN': True,
'LOCALE_FUNC': 'path.to.callable',
'VERIFIED_EMAIL': True,
'VERSION': 'v2.12',
}
}
# django-rest-auth configuration
REST_SESSION_LOGIN = False
OLD_PASSWORD_FIELD_ENABLED = True
REST_AUTH_SERIALIZERS = {
"TOKEN_SERIALIZER": "accounts.api.serializers.TokenSerializer",
"USER_DETAILS_SERIALIZER": "accounts.api.serializers.UserDetailSerializer",
}
REST_AUTH_REGISTER_SERIALIZERS = {
"REGISTER_SERIALIZER": "accounts.api.serializers.CustomRegisterSerializer"
}
And here are the custom adapters (in case they matter):
from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth.utils import build_absolute_uri
from django.http import HttpResponseRedirect
from django.urls import reverse
class CustomAccountAdapter(DefaultAccountAdapter):
def get_email_confirmation_url(self, request, emailconfirmation):
"""Constructs the email confirmation (activation) url."""
url = reverse(
"accounts:account_confirm_email",
args=[emailconfirmation.key]
)
ret = build_absolute_uri(
request,
url
)
return ret
def get_email_confirmation_redirect_url(self, request):
"""
The URL to return to after successful e-mail confirmation.
"""
url = reverse(
"accounts:email_activation_done"
)
ret = build_absolute_uri(
request,
url
)
return ret
def respond_email_verification_sent(self, request, user):
return HttpResponseRedirect(
reverse('accounts:account_email_verification_sent')
)
class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
def get_connect_redirect_url(self, request, socialaccount):
"""
Returns the default URL to redirect to after successfully
connecting a social account.
"""
assert request.user.is_authenticated
url = reverse('accounts:socialaccount_connections')
return url
Lastly, here are the views:
from allauth.socialaccount.providers.facebook.views import \
FacebookOAuth2Adapter
from rest_auth.registration.views import SocialConnectView, SocialLoginView
class FacebookLogin(SocialLoginView):
adapter_class = FacebookOAuth2Adapter
class FacebookConnect(SocialConnectView):
adapter_class = FacebookOAuth2Adapter
I thought that if I connected the serializer like I did in the initial part of the question the register serializer logic would also get run when someone signs up using facebook.
What do I need to do to have that logic also run when someone signs up using facebook?
(If I can't fix it, I could make a second server request after each facebook sign up on the client side which creates the userconsumerprofile, but that would be kinda overkill and would introduce new code surface which leads to a higher likelihood of bugs.)
Looking briefly at the DefaultAccountAdapter and DefaultSocialAccountAdapter it may be an opportunity for you to override/implement the save_user(..) in your CustomAccountAdapter/CustomSocialAccountAdapter to setup the profile?
Looking just at code it seems that the DefaultSocialAccountAdapter.save_user will finally call the DefaultAccountAdapter.save_user.
Something like this maybe?
class CustomAccountAdapter(DefaultAccountAdapter):
def save_user(self, request, user, form, commit=True):
user = super(CustomAccountAdapter, self).save_user(request, user, form,
commit)
UserConsumerProfile.objects.get_or_create(user=user)
return user
There are a few other "hooks"/functions in the adapters that may we worth to investigate if the save_user doesn't work for your scenario.
The REGISTER_SERIALIZER that you created is only used by the RegisterView.
The social login & connect views use different serializers: SocialLoginSerializer and SocialConnectSerializer, that cannot be overwritten per settings.
I can think of two ways to achieve your desired behavior:
create serializers for the social login & connect views (inherriting the default serializers) and set them as serializer_class for the view,
use Django signals, especially the post_save signal for the User model and when an instance is created, create your UserConsumerProfile.

how to use django rest filtering with mongoengine

Hi I am starting django 1.8.3 with mongodb using mongo engine to create rest api.
I am using rest_framework_mongoengine to do so.
I wanted to use a feature of DjangoFilterBackend for same.
My code is:
models.py:
from mongoengine import *
from django.conf import settings
connect(settings.DBNAME)
class Client(Document):
name = StringField(max_length=50)
city = StringField(max_length=50)
country = StringField(max_length=200, verbose_name="Country")
address = StringField(default='')
Serializer.py
from client.models import Client
from rest_framework_mongoengine.serializers import DocumentSerializer
class ClientSerializer(DocumentSerializer):
class Meta:
model = Client
depth = 1
views.py
from rest_framework_mongoengine.generics import *
from rest_framework import filters
class ClientList(ListCreateAPIView):
serializer_class = ClientSerializer
queryset = Client.objects.all()
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ('name',)
I start getting error
QuerySet object has no attribute model
Don't know where its going wrong. If I remove filter_field It works but I can not use filter feature.
Any help would be of great use
You can also perform the filtering by overriding the get_queryset() method and creating a generic filtering function.
Here, we specify the filter fields tuple in the view as my_filter_fields on which we want to perform filtering. Then in our get_queryset(), we call a function get_kwargs_for_filtering().
get_kwargs_for_filtering() functions iterates over the fields defined in my_filter_fields and checks if this was passed in the query_params. If the field is found then a key with the field name and value as the retrieved value is set in a dictionary filtering_kwargs. After the iteration is over, this filtering_kwargs dictionary is returned to the get_queryset() method.
This filtering_kwargs dictionary is used to filter the queryset then.
from rest_framework_mongoengine.generics import *
from rest_framework import filters
class ClientList(ListCreateAPIView):
serializer_class = ClientSerializer
my_filter_fields = ('name', 'country') # specify the fields on which you want to filter
def get_kwargs_for_filtering(self):
filtering_kwargs = {}
for field in self.my_filter_fields: # iterate over the filter fields
field_value = self.request.query_params.get(field) # get the value of a field from request query parameter
if field_value:
# filtering_kwargs[field] = field_value
field = self.get_serializer().fields[field_name]
filtering_kwargs[field] = field.to_representation(field_value)
return filtering_kwargs
def get_queryset(self):
queryset = Client.objects.all()
filtering_kwargs = self.get_kwargs_for_filtering() # get the fields with values for filtering
if filtering_kwargs
queryset = Client.objects.filter(**filtering_kwargs) # filter the queryset based on 'filtering_kwargs'
return queryset
here is a good method to do this work.
firstly, you should install django_mongoengine_filter rest_framework_mongoengine
pip install django_mongoengine_filter
pip install rest_framework_mongoengine
secondly, you can write your owner Filter like this:
import django_mongoengine_filter as filters
from app.models import User
class UserFilter(filters.FilterSet):
class Meta:
model = User
fields = ['name']
finally, you can use the UserFilter like this in view class:
from app.models import User
from app.serializer import UserS # use rest_framework_mongoengine to write serializer
from app.filters import UserFilter
class UserVS(ModelViewSet):
queryset = User.objects.all()
serializer_class = UserS
# override filter_queryset function
def filter_queryset(self, queryset):
filter = UserFilter(self.request.query_params, queryset=queryset)
return filter.qs
Although the question is referring to Django 1.8. This is a question that is still important and valid in later versions of Django, DRF and MongoEngine.
At the time of this answer I am running
Django==4.1.3
djangorestframework==3.14
django-rest-framework-mongoengine==3.4.1
mongoengine==0.24.2
django-filter==22.1
# patched version of django-mongoengine-filter to support Django 4.0
# https://github.com/oussjarrousse/django-mongoengine-filter
# Pull request https://github.com/barseghyanartur/django-mongoengine-filter/pull/16
django-mongoengine-filter>=0.3.5
The idea in this answer is to add filtering support to django-rest-framework-mongoengine using django-mongoengine-filter that is an replacement or an extension to django-filter and should work the same way as django-filter.
First let's edit the project/settings.py file. Find the INSTALLED_APPS variable and make sure the following "Django apps" are added:
# in settings.py:
INSTALLED_APPS = [
# ...,
"rest_framework",
"rest_framework_mongoengine",
"django_filters",
# ...,
]
the app django_filters is required to add classes related to filtering infrastructure, and other things including html templates for DRF.
Then in the variable REST_FRAMEWORK we need to edit the values associated with the key: DEFAULT_FILTER_BACKENDS
# in settings.py:
REST_FRAMEWORK = {
# ...
"DEFAULT_FILTER_BACKENDS": [
"filters.DjangoMongoEngineFilterBackend",
# ...
],
# ...
}
DjangoMongoEngineFilterBackend is a custom built filter backend that we need to add to the folder (depending on how you structure your project) in the file filters
# in filters.py:
from django_filters.rest_framework.backends import DjangoFilterBackend
class DjangoMongoEngineFilterBackend(DjangoFilterBackend):
# filterset_base = django_mongoengine_filter.FilterSet
"""
Patching the DjangoFilterBackend to allow for MongoEngine support
"""
def get_filterset_class(self, view, queryset=None):
"""
Return the `FilterSet` class used to filter the queryset.
"""
filterset_class = getattr(view, "filterset_class", None)
filterset_fields = getattr(view, "filterset_fields", None)
if filterset_class:
filterset_model = filterset_class._meta.model
# FilterSets do not need to specify a Meta class
if filterset_model and queryset is not None:
element = queryset.first()
if element:
queryset_model = element.__class__
assert issubclass(
queryset_model, filterset_model
), "FilterSet model %s does not match queryset model %s" % (
filterset_model,
str(queryset_model),
)
return filterset_class
if filterset_fields and queryset is not None:
MetaBase = getattr(self.filterset_base, "Meta", object)
element = queryset.first()
if element:
queryset_model = element.__class__
class AutoFilterSet(self.filterset_base):
class Meta(MetaBase):
model = queryset_model
fields = filterset_fields
return AutoFilterSet
return None
This custom filter backend will not raise the exceptions that the original django-filter filter backend would raise. The django-filter DjangoFilterBackend access the key model in QuerySet as in queryset.model, however that key does not exist in MongoEngine.
Maybe making it available in MongoEngine should be considered:
https://github.com/MongoEngine/mongoengine/issues/2707
https://github.com/umutbozkurt/django-rest-framework-mongoengine/issues/294
Now we can add a custom filter to the ViewSet:
# in views.py
from rest_framework_mongoengine.viewsets import ModelViewSet
class MyModelViewSet(ModelViewSet):
serializer_class = MyModelSerializer
filter_fields = ["a_string_field", "a_boolean_field"]
filterset_class = MyModelFilter
def get_queryset(self):
queryset = MyModel.objects.all()
return queryset
Finally let's get back to filters.py and add the MyModelFilter
# in filters.py
from django_mongoengine_filter import FilterSet, StringField, BooleanField
class MyModelFilter(FilterSet):
"""
MyModelFilter is a FilterSet that is designed to work with the django-filter.
However the original django-mongoengine-filter is outdated and is causing some troubles
with Django>=4.0.
"""
class Meta:
model = MyModel
fields = [
"a_string_field",
"a_boolean_field",
]
a_string_field = StringFilter()
a_boolean_field = BooleanFilter()
That should do the trick.

Django Admin linking to related objects

My app has users who create pages. In the Page screen of the admin, I'd like to list the User who created the page, and in that list, I'd like the username to have a link that goes to the user page in admin (not the Page).
class PageAdmin(admin.ModelAdmin):
list_display = ('name', 'user', )
list_display_links = ('name','user',)
admin.site.register(Page, PageAdmin)
I was hoping that by making it a link in the list_display it would default to link to the actual user object, but it still goes to Page.
I'm sure I'm missing something simple here.
Modifying your model isn't necessary, and it's actually a bad practice (adding admin-specific view-logic into your models? Yuck!) It may not even be possible in some scenarios.
Luckily, it can all be achieved from the ModelAdmin class:
from django.urls import reverse
from django.utils.safestring import mark_safe
class PageAdmin(admin.ModelAdmin):
# Add it to the list view:
list_display = ('name', 'user_link', )
# Add it to the details view:
readonly_fields = ('user_link',)
def user_link(self, obj):
return mark_safe('{}'.format(
reverse("admin:auth_user_change", args=(obj.user.pk,)),
obj.user.email
))
user_link.short_description = 'user'
admin.site.register(Page, PageAdmin)
Edit 2016-01-17:
Updated answer to use make_safe, since allow_tags is now deprecated.
Edit 2019-06-14:
Updated answer to use django.urls, since as of Django 1.10 django.core.urls has been deprecated.
Add this to your model:
def user_link(self):
return '%s' % (reverse("admin:auth_user_change", args=(self.user.id,)) , escape(self.user))
user_link.allow_tags = True
user_link.short_description = "User"
You might also need to add the following to the top of models.py:
from django.template.defaultfilters import escape
from django.core.urls import reverse
In admin.py, in list_display, add user_link:
list_display = ('name', 'user_link', )
No need for list_display_links.
You need to use format_html for modern versions of django
#admin.register(models.Foo)
class FooAdmin(admin.ModelAdmin):
list_display = ('ts', 'bar_link',)
def bar_link(self, item):
from django.shortcuts import resolve_url
from django.contrib.admin.templatetags.admin_urls import admin_urlname
url = resolve_url(admin_urlname(models.Bar._meta, 'change'), item.bar.id)
return format_html(
'{name}'.format(url=url, name=str(item.bar))
)
I ended up with a simple helper:
from django.shortcuts import resolve_url
from django.utils.safestring import SafeText
from django.contrib.admin.templatetags.admin_urls import admin_urlname
from django.utils.html import format_html
def model_admin_url(obj: Model, name: str = None) -> str:
url = resolve_url(admin_urlname(obj._meta, SafeText("change")), obj.pk)
return format_html('{}', url, name or str(obj))
Then you can use the helper in your model-admin:
class MyAdmin(admin.ModelAdmin):
readonly_field = ["my_link"]
def my_link(self, obj):
return model_admin_url(obj.my_foreign_key)
I needed this for a lot of my admin pages, so I created a mixin for it that handles different use cases:
pip install django-admin-relation-links
Then:
from django.contrib import admin
from django_admin_relation_links import AdminChangeLinksMixin
#admin.register(Group)
class MyModelAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
# ...
change_links = ['field_name']
See the GitHub page for more info. Try it out and let me know how it works out!
https://github.com/gitaarik/django-admin-relation-links
I decided to make a simple admin mixin that looks like this (see docstring for usage):
from django.contrib.contenttypes.models import ContentType
from django.utils.html import format_html
from rest_framework.reverse import reverse
class RelatedObjectLinkMixin(object):
"""
Generate links to related links. Add this mixin to a Django admin model. Add a 'link_fields' attribute to the admin
containing a list of related model fields and then add the attribute name with a '_link' suffix to the
list_display attribute. For Example a Student model with a 'teacher' attribute would have an Admin class like this:
class StudentAdmin(RelatedObjectLinkMixin, ...):
link_fields = ['teacher']
list_display = [
...
'teacher_link'
...
]
"""
link_fields = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.link_fields:
for field_name in self.link_fields:
func_name = field_name + '_link'
setattr(self, func_name, self._generate_link_func(field_name))
def _generate_link_func(self, field_name):
def _func(obj, *args, **kwargs):
related_obj = getattr(obj, field_name)
if related_obj:
content_type = ContentType.objects.get_for_model(related_obj.__class__)
url_name = 'admin:%s_%s_change' % (content_type.app_label, content_type.model)
url = reverse(url_name, args=[related_obj.pk])
return format_html('{}', url, str(related_obj))
else:
return None
return _func
If anyone is trying to do this with inline admin, consider a property called show_change_link since Django 1.8.
Your code could then look like this:
class QuestionInline(admin.TabularInline):
model = Question
extra = 1
show_change_link = True
class TestAdmin(admin.ModelAdmin):
inlines = (QuestionInline,)
admin.site.register(Test, TestAdmin)
This will add a change/update link for each foreign key relationship in the admin's inline section.

django users and groups - show group membership in admin?

The Django Admin's User management gives us easy access to setting a User's group allegiances via the list for picking the Groups. This is the ManyToMany relationship default widget for assignment. And for the most part that's nice for User creation from the get go.
However, say I have a huge number of users and groups, I'd like the chance to view and manage group membership by clicking on the Group in the admin and then seeing all the members in a similar picker (or via the horizontal widget).
Is there a built-in Django method/trick to show ManyToMany relations in "reverse?"
Setting group assignment is fine from the User adminform on User creation. But this can be a big pain to find and manage once users and groups are well established.
I guess the workaround is to make my own view to show group members, I just wanted to see if there was some trick I could do with inlines or something in the admin.
Try to use this snippet. It's customisation example of contrib.auth app admin part.
# utils/admin_auth.py
# -*- coding: utf-8 -*-
from django.utils.safestring import mark_safe
from django.contrib.auth.models import User, Group
from django.contrib.auth.admin import UserAdmin, GroupAdmin
from django.contrib import admin
def roles(self):
#short_name = unicode # function to get group name
short_name = lambda x:unicode(x)[:1].upper() # first letter of a group
p = sorted([u"<a title='%s'>%s</a>" % (x, short_name(x)) for x in self.groups.all()])
if self.user_permissions.count(): p += ['+']
value = ', '.join(p)
return mark_safe("<nobr>%s</nobr>" % value)
roles.allow_tags = True
roles.short_description = u'Groups'
def last(self):
fmt = "%b %d, %H:%M"
#fmt = "%Y %b %d, %H:%M:%S"
value = self.last_login.strftime(fmt)
return mark_safe("<nobr>%s</nobr>" % value)
last.allow_tags = True
last.admin_order_field = 'last_login'
def adm(self):
return self.is_superuser
adm.boolean = True
adm.admin_order_field = 'is_superuser'
def staff(self):
return self.is_staff
staff.boolean = True
staff.admin_order_field = 'is_staff'
from django.core.urlresolvers import reverse
def persons(self):
return ', '.join(['%s' % (reverse('admin:auth_user_change', args=(x.id,)), x.username) for x in self.user_set.all().order_by('username')])
persons.allow_tags = True
class UserAdmin(UserAdmin):
list_display = ['username', 'email', 'first_name', 'last_name', 'is_active', staff, adm, roles, last]
list_filter = ['groups', 'is_staff', 'is_superuser', 'is_active']
class GroupAdmin(GroupAdmin):
list_display = ['name', persons]
list_display_links = ['name']
admin.site.unregister(User)
admin.site.unregister(Group)
admin.site.register(User, UserAdmin)
admin.site.register(Group, GroupAdmin)