Django admin UserChangeForm with UUID field as primary key - python

I'm working on a Django app that will contain sensitive User data, so I'm taking a number of steps to anonymise User objects and their data.
I created custom 'User' object that subclassses the AbstractBaseUser model like so:
class User(AbstractBaseUser, PermissionsMixin):
(...)
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
(...)
It has the following linked ModelAdmin object:
from django.contrib.auth.forms import UserChangeForm
#admin.register(User)
class UserAdmin(admin.ModelAdmin):
form = UserChangeForm
I'm using UUID fields for primary keys to ensure maximum anonymity, yet I would like to be able to reset User passwords in the Django Admin (like you can do with the default User object)
However, when I open a User in the admin and press the link to change the password I get the following error message:
User with ID "21362aca-6918-47ea-9b29-275350a89c54/password" doesn't exist. Perhaps it was deleted?
The admin url is still expecting a url with the an integer as its pk value.
So it seems that I have to override the admin url configuration in the ModelAdmin definition, but I was wondering if there was a simpler way to achieve the desired result - as I imagine that replacing the User.pk with an UUID field is a fairly regular occurrence and I image many developers have ran into this problem. I tried to find some kind of settings / toggle to achieve this but to no avail, am I missing something?

Your 'UserAdmin' inherits from ModelAdmin, which provides the urls via the get_urls method for the add, change, delete, etc. views but also a 'fallback' url:
urlpatterns = [
#...
url(r'^(.+)/change/$', wrap(self.change_view), name='%s_%s_change' % info),
# For backwards compatibility (was the change url before 1.9)
path('<path:object_id>/', wrap(RedirectView.as_view(
pattern_name='%s:%s_%s_change' % ((self.admin_site.name,) + info)
))),
]
The url you are following looks like /user/<UUID>/password/ which only fits the regex of the fallback pattern - which redirects you to the change page in such a way that it uses <UUID>/password as the object_id.
Try inheriting from django.contrib.auth.admin.UserAdmin instead as its get_urls method provides the url pattern you need.
Some more poking around...
If your primary key field was an AutoField, the whole process would raise a ValueError('invalid literal for int') when trying to cast int('some_integer/password') in django.db.models.fields.AutoField.get_prep_value in order to prepare the value for a query. Understandable!
HOWEVER: UUIDField uses the get_prep_value method of the base class Field. Field.get_prep_value just simply returns the value without even checking (after all, validating the value should be the job of UUIDField). So you end up with a query that looks for the bogus uuid '<uuid>/password', which obviously doesn't exist.

Related

Django admin model query url string on NOT EQUALS

I have a simple Django ForeignKey relationship between two models in postgreSQL. The logic here is the Sample object can optionally have a foreign key into a type of sample control.
from django.contrib.postgres.fields import CICharField
from django.db import models
class Sample(models.Model):
controls_applied = models.ForeignKey(SampleControl, null=True,
blank=True,
on_delete=models.SET_NULL)
class SampleControl(models.Model):
id = CICharField(max_length=200, primary_key=True)
On the admin changelist for Sample, I am trying to create filter that queries all or none of Samples that have a specific control (in this case, we'll use a SampleControl with id='runcontrol'). I'm trying to craft the specific URI string to append to my changelist url as I have other filters I'm trying to run in conjunction.
To get all samples with controls_applied= 'runcontrol', I can simply append the following string to my URL (notice the reference to the id of the foreign key):
?controls_applied__id=runcontrol
But if I want to get all samples that are not run control, it's more complicated. Wasn't sure what to do, so I decided to use 'startswith', which has a convenient 'notstartswith' companion that will do the inverse. When I use this, I see that the following works:
?controls_applied__id__startswith=runcontrol
However, the inverse
?controls_applied__id__notstartswith=runcontrol
gives me an error: Unsupported lookup 'notstartswith' for CICharField or join on the field not permitted, perhaps you meant startswith or istartswith?
Which leads to me the simple question: is there a way to specify NOT EQUALS in the query string of the URL on Django's admin site? Thank you!
I don't think admin URLs are capable of representing an exclude queryset filter, unless you create your own SimpleListFilter.
Try this in your admin.py:
class WithoutControlListFilter(admin.SimpleListFilter):
title = ('Without Control')
parameter_name = 'without_control'
def lookups(self, request, model_admin):
return (
('runcontrol', ('Run Control')),
)
def queryset(self, request, queryset):
return queryset.exclude(controls_applied=self.value())
class SampleAdmin(admin.ModelAdmin):
list_filter = (WithoutControlListFilter,)
There is a bit of info about admin filters in the Django ModelAdmin docs.

Convert (and validate) file added in django admin

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

Custom permissions on Django group form

I've added some custom permissions to my Post model.
I've also created a form to add/edit groups with only this custom permissions:
class GroupFornm(forms.ModelForm):
permissions = forms.MultipleChoiceField(choices=Post._meta.permissions)
class Meta:
model = Group
fields = '__all__'
It works because I can see and select only my custom permissions but when I try to save the form I got the error:
invalid literal for int() with base 10: 'can_view'
What am I doing wrong? It seems that this form field waits for (int, str) pair but documentation says that as usually, (str, str) should work.
Edit
Post._meta.permissions:
(('can_view', 'Can see tickets'), ('can_edit', 'Can edit tickets'), ('can_delete', 'Can delete tickets'), ('can_create', 'Can add new tickets'))
The problem is not really related to the form itself, but the fact that you somehow need to translate those permissions into Permission objects that should be stored in the Group instance (the one that this ModelForm is managing).
I think displaying the options is not a problem. But if a user later for example performs a POST request, with the options (like can_write), then the question is how the Form should translate these into Permission objects (or the primary keys of Permission objects).
In that case you need to coerce the name of the permissions to Permission objects, or the ids of Permission objects. We can for example use a TypedMultipleChoiceField, and coerce with:
def get_permission_from_name(name):
return Permission.objects.get(name=name)
class GroupFornm(forms.ModelForm):
permissions = forms.TypedMultipleChoiceField(
choices=Post._meta.permissions,
coerce=get_permission_from_name,
)
class Meta:
model = Group
fields = '__all__'
Note that the above is not really a very efficient implementation, since it requires a query for every value send. Furthermore in case no permission with that name exists, then this will raise an error.
If you want to construct Permissions on the fly (in case these are not yet constructed), then you can change the function to:
def get_permission_from_name(name):
return Permission.objects.get_or_create(
name=name,
defaults={
'content_type': ContentType.objects.get_for_model(Post),
'codename': name
}
)

How to use current logged in user as PK for Django DetailView?

When defining URL patterns, I am supposed to use a regular expression to acquire a PK from the URL.
What if I want a URL that has no PK, and if it's not provided, it will use the currently logged in user? Examples:
visiting /user will get a DetailView of the currently logged in user
/user/edit will show an UpdateView for the currently logged in user
I tried hard-coding the pk= in the Detail.as_view() call but it reports invalid keyword.
How do I specify that in the URL conf?
My sample code that shows PK required error when visiting /user URL:
urlpatterns = patterns('',
url(r'user/$',
DetailView.as_view(
model=Account,
template_name='user/detail.html')),
)`
An alternative approach would be overriding the get_object method of the DetailView subclass, something along the line of:
class CurrentUserDetailView(UserDetailView):
def get_object(self):
return self.request.user
Much cleaner, simpler and more in the spirit of the class-based views than the mixin approach.
EDIT: To clarify, I believe that two different URL patterns (i.e. one with a pk and the other without) should be defined separately in the urlconf. Therefore they could be served by two different views as well, especially as this makes the code cleaner. In this case the urlconf might look something like:
urlpatterns = patterns('',
url(r"^users/(?P<pk>\d+)/$", UserDetailView.as_view(), name="user_detail"),
url(r"^users/current/$", CurrentUserDetailView.as_view(), name="current_user_detail"),
url(r"^users/$", UserListView.as_view(), name="user_list"),
)
And I've updated my example above to note that it inherits the UserDetailView, which makes it even cleaner, and makes it clear what it really is: a special case of the parent view.
As far as I know, you can't define that on the URL definition, since you don't have access to that information.
However, what you can do is create your own mixin and use it to build views that behave like you want.
Your mixin would look something like this:
class CurrentUserMixin(object):
model = Account
def get_object(self, *args, **kwargs):
try:
obj = super(CurrentUserMixin, self).get_object(*args, **kwargs)
except AttributeError:
# SingleObjectMixin throws an AttributeError when no pk or slug
# is present on the url. In those cases, we use the current user
obj = self.request.user.account
return obj
and then, make your custom views:
class UserDetailView(CurrentUserMixin, DetailView):
pass
class UserUpdateView(CurrentUserMixin, UpdateView):
pass
Generic views uses always RequestContext. And this paragraph in the Django Documentation says that when using RequestContext with auth app, the template gets passed an user variable that represents current user logged in. So, go ahead, and feel free to reference user in your templates.
You can get the details of the current user from the request object. If you'd like to see a different user's details, you can pass the url as parameter. The url would be encoded like:
url(r'user/(?P<user_id>.*)$', 'views.user_details', name='user-details'),
views.user_details 2nd parameter would be user_id which is a string (you can change the regex in the url to restrict integer values, but the parameter would still of type string). Here's a list of other examples for url patterns from the Django documentation.

Lookup admin change url for arbitrary Django model

How do you lookup the admin change url for an arbitrary model?
If I know the model, I can get the url by doing something like:
>>> print urlresolvers.reverse('admin:myapp_mymodel_change', args=(obj.id,))
/admin/myapp/mymodel/123/
I have a generic foreign key on a model, and I'd like to provide a link in admin to the object's corresponding change page. Since it can be any type of model, I can't easily use reverse(). Is there some way I could simply this to the following?
>>> get_admin_change_url(obj)
/admin/myapp/mymodel/123/
Once you have the object, you can access its app label and name on its _meta class, then construct the name of the admin change url dynamically.
app_label = obj._meta.app_label
model = obj._meta.module_name
reverse('admin:%s_%s_change' % (app_label, model), args=(obj.id,))

Categories

Resources