I am working on a Django project where any time an admin does something in the admin console (CRUD), a set of people gets notified. I was pointed to three methods on ModelAdmin called log_addition, log_created and log_deleted which save all the necessary info into a special database called "django_admin_log".
I placed the following code into my admin.py:
class ModelAdmin(admin.ModelAdmin):
def log_addition(self, request, object):
subject = 'admin test of creation'
message = 'admin creation detected'
from_addr = 'no_reply#example.com'
recipient_list = ('luka#example.com',)
send_mail(subject, message, from_addr, recipient_list)
return super(ModelAdmin, self).log_addition( *args, **kwargs )
This code however, gets ignored when I create new users. Many posts actually recommend to create a different class name (MyModelAdmin) and I am not entirely sure why - the point is to override the existing model. I tried it, but with the same result. Can anyone point me in the right direction please? How exactly do you override a method of an existing class and give it some extra functionality?
Thank you!
Luka
EDIT: I figured it out, it seems that I had to unregister and re-register User for my change to work.
remove the return at the end.
If that doesn't work, you can instead put the code in a function called add_view:
class ModelAdmin(admin.ModelAdmin):
add_view(self, request):
...
super(ModelAdmin, self).add_view( *args, **kwargs)
This function can be overwritten to add functionality to the admin's view. If you look at the admin code:
https://code.djangoproject.com/browser/django/trunk/django/contrib/admin/options.py#L923
you will see that the function you have tried to overwrite is called from within the add_view function so you could equally put the code here
Related
What I am trying to do:
I am trying to access request object in my django models so that I can get the currently logged in user with request.user.
What I have tried:
I found a hack on this site. But someone in the comments pointed out not to do it when in production.
I also tried to override model's __init__ method just like mentioned in this post. But I got an AttributeError: 'RelatedManager' object has no attribute 'request'
Models.py:
class TestManager(models.Manager):
def user_test(self):
return self.filter(user=self.request.user, viewed=False)
class Test(models.Model):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(Test, self).__init__(*args, **kwargs)
user = models.ForeignKey(User, related_name='test')
viewed = models.BooleanField(default=False)
objects = TestManager()
I trying to access request object in my Django models so that I can get the currently logged in user with request.user.
Well a problem is that models are not per se used in the context of a request. One for example frequently defines custom commands to do bookkeeping, or one can define an API where for example the user is not present. The idea of the Django approach is that models should not be request-aware. Models define the "business logic" layer: the models define entities and how they interact. By not respecting these layers, one makes the application vulnerable for a lot of problems.
The blog you refer to aims to create what they call a global state (which is a severe anti-patten): you save the request in the middleware when the view makes a call, such that you can then fetch that object in the model layer. There are some problems with this approach: first of all, like already said, not all use cases are views, and thus not all use cases pass through the middleware. It is thus possible that the attribute does not exist when fetching it.
Furthermore it is not guaranteed that the request object is indeed the request object of the view. It is for example possible that we use the model layer with a command that thus does not pass through the middleware, in which case we should use the previous view request (so potentially with a different user). If the server processes multiple requests concurrently, it is also possible that a view will see a request that arrived a few nanoseconds later, and thus again take the wrong user. It is also possible that the authentication middleware is conditional, and thus that not all requests have a user attribute. In short there are more than enough scenario's where this can fail, and the results can be severe: people seeing, editing, or deleting data that they do not "own" (have no permission to view, edit, or delete).
You thus will need to pass the request, or user object to the user_test method. For example with:
from django.http import HttpRequest
class TestManager(models.Manager):
def user_test(self, request_or_user):
if isinstance(request_or_user, HttpRequest):
return self.filter(user=request_or_user.user, viewed=False)
else:
return self.filter(user=request_or_user, viewed=False)
one thus has to pass the request object from the view to the function. Even this is not really pure. A real pure approach would only accept a user object:
class TestManager(models.Manager):
def user_test(self, user):
return self.filter(user=user, viewed=False)
So in a view one can use this as:
def some_view(request):
some_tests = Test.objects.user_test(request.user)
# ...
# return Http response
For example if we want to render a template with this queryset, we can pass it like:
def some_view(request):
some_tests = Test.objects.user_test(request.user)
# ...
return render(request, 'my_template.html', {'some_tests': some_tests})
When trying to log in with a social account while there already being an account with that email, the following message shows:
An account already exists with this e-mail address. Please sign in to that account first, then connect your Google account.
Now I would like to change that message. At first I tried to override ACCOUNT_SIGNUP_FORM_CLASS = 'mymodule.forms.MySignupForm' and gave it my own raise_duplicate_email_error method, but that method is never called.
The form looks like this:
class SignupForm(forms.Form):
first_name = forms.CharField()
last_name = forms.CharField()
boolflag = forms.BooleanField()
def raise_duplicate_email_error(self):
# here I tried to override the method, but it is not called
raise forms.ValidationError(
_("An account already exists with this e-mail address."
" Please sign in to that account."))
def signup(self, request, user):
# do stuff to the user and save it
So the question is: How can I change that message?
If you want the raise_duplicate_email_error method to be called you must inherit from a form class that actually calls self.raise_duplicate_email_error()! However you are just inheriting from forms.Form!
Let's take a look at the code of forms.py at https://github.com/pennersr/django-allauth/blob/master/allauth/account/forms.py. We can see that raise_duplicate_email_error is a method of BaseSignupForm (and is called from its clean_email method).
So you need to inherit from either allauth.account.forms.BaseSignupForm or allauth.account.forms.SignupForm (which also inherits from BaseSignupForm and adds some more fieldds to it).
Update after OP's comment (that the BaseSignupForm is derived from the _base_signup_form_class() function, that itself imports the form class defined in the SIGNUP_FORM_CLASS setting ): Hmm you are right. The problem is that the raise_duplicate_email_error and clean_email methods of BaseSignupForm don't call the same-named methods of their ancestors through super (so your raise_duplicate_email_error is never called).
Let's see what the view does: If you have added the line url(r'^accounts/', include('allauth.urls')), to your urls.py (which is the usual thing to do for django-allauth), you'll see the line url(r"^signup/$", views.signup, name="account_signup"), in the file https://github.com/pennersr/django-allauth/blob/13edcfef0d7e8f0de0003d6bcce7ef58119a5945/allauth/account/urls.py and then in the file
https://github.com/pennersr/django-allauth/blob/13edcfef0d7e8f0de0003d6bcce7ef58119a5945/allauth/account/views.py you'll see the definition of signup as signup = SignupView.as_view(). So let's override SignupView to use our own form and then use our class view for the account_sigunp !
Here's how to do it:
a. Create your custom view that inherits SignupView and overrides the form class
class CustomFormSignupView(allauth.accounts.views.SignupView):
form_class = CustomSignupForm
b. Create a custom form that inherits from SignupForm and overrides the email validation message
class CustomSignupForm(allauth.accounts.forms.SignupForm ):
def raise_duplicate_email_error(self):
# here I tried to override the method, but it is not called
raise forms.ValidationError(
_("An account already exists with this e-mail address."
" Please sign in to that account."))
c. In your own urls.py add the following after include('allauth.urls') to override the account_signup url
url(r'^accounts/', include('allauth.urls')),
url(r"^accounts/signup/$", CustomFormSignupView.as_view(), name="account_signup"),``
After creating your CustomSignupForm(allauth.accounts.forms.SignupForm ) that overrides the raise_duplicate_email_error method, you can, with django-allauth version 0.18, add the following to your settings file:
SOCIALACCOUNT_FORMS = {
'signup': 'path.to.your.custom.social.signup.form.CustomSignupForm'
}
The new raise_duplicate_email_error method should be called now.
Hope this helps you.
I am currently utilizing the has_change_permission hook in my custom django admin class to implement a simple form of row-level permissions and determine whether a non-superuser can edit a particular object, like so:
def has_change_permission(self, request, obj=None):
if obj is None or request.user.is_superuser or (obj and not obj.superuser_only): # (my model has a 'superuser_only' flag that gets set via fixtures, but its beyond the scope of this question)
return True
return False
This works well enough: all objects are shown to a user, but if they click on an object that they don't have permission to edit then they are taken to my 403 page, presumably because PermissionDenied is raised. However, giving them a hyperlink to a permission denied page doesn't seem ideal for this case; I would like to show the objects but not provide any hyperlinks to the edit page on the list page (in addition to raising PermissionDenied if they tried to manually use the URL for the object). Is there a straightforward hook for removing these hyperlinks without a horrendous hack? I'm an experienced django developer so if you can point me in the right direction (if any exists), I'll be able to implement the details or determine that its not worth it for now.
I was able to accomplish this in a fairly straightforward way by overwriting the default get_list_display_links function in the ModelAdmin class (in my admin.py file):
def get_list_display_links(self, request, list_display):
if request.user.is_superuser:
if self.list_display_links or not list_display:
return self.list_display_links
else:
# Use only the first item in list_display as link
return list(list_display)[:1]
else:
# Ensures that no hyperlinks appear in the change list for non-superusers
self.list_display_links = (None, )
return self.list_display_links
Note that my code maintains the hyperlink for superusers. You can easily just use the latter part of the function if you don't want to make this distinction.
I'm using django-registration for a project of mine.
I'd like to add some extra contextual data to the template used for email activation.
Looking into the register view source, I cannot figure out how to do it.
Any idea ?
From what I remember, you need to write your own registration backend object (easier then is sounds) as well as your own profile model that inherits from RegistrationProfile and make the backend use your custom RegistrationProfile instead (This model is where the email templates are rendered and there is no way to extend the context, so they need to be overwritten)
A simple solution is to rewrite the send_activation_email
So instead of
registration_profile.send_activation_email(site)
I wrote this in my Users model
def send_activation_email(self, registration_profile):
ctx_dict = {
'activation_key': registration_profile.activation_key,
'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS,
'OTHER_CONTEXT': 'your own context'
}
subject = render_to_string('registration/activation_email_subject.txt',
ctx_dict)
subject = ''.join(subject.splitlines())
message = render_to_string('registration/activation_email.txt',
ctx_dict)
self.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
And I call it like this
user.send_activation_email(registration_profile)
I don't get what it is your problem but the parameter is just in the code you link (the last one):
def register(request, backend, success_url=None, form_class=None,
disallowed_url='registration_disallowed',
template_name='registration/registration_form.html',
extra_context=None)
That means you can do it from wherever you are calling the method. Let's say your urls.py:
from registration.views import register
(...)
url(r'/registration/^$', register(extra_context={'value-1':'foo', 'value-2':'boo'})), name='registration_access')
That's in urls.py, where usually people ask more, but, of course, it could be from any other file you are calling the method.
I am writing code to open a new window inside the django admin to add a model instance and to close on save. This is very similar to the behaviour of ForeignKey field add (green plus sign) does, but without selecting the newly created model instance (because its not a foreign key field).
The code I add to make the pop-up link is:
link = '<a id="add_id_event" class="add-another" onclick="return showAddAnotherPopup(this);" href="%s?date=%s">add</a>' % ( addurl,currentdate)
where my model is called Event. I correctly add RelatedObjectLookups.js
When I try to save this model, django applies the same code it would use on a ForeignKey field and tries to activate a SelectBox which I don't have. This causes the javascript to fail before it gets to the window.close()
I've tried overriding the save_model function with
def save_model(self, request, obj, form, change):
if request.GET.get('_popup') == '1':
obj.save()
return HttpResponse('<script type="text/javascript">window.close()</script>')
This code is used but the HttpResponse call is ignored and django renders the default. e.g.
<script type="text/javascript">opener.dismissAddAnotherPopup(window, "14382", "TMC 2012\u002D02\u002D02 10:00:00 DDT2010B\u002D028");</script>
which fails because there is no destination SelectBox object.
Thanks for your help.
You'll need to override ModelAdmin.response_add. That's where the redirect is happening.
In my case, I needed to override the dismissAddAnotherPopup method, so I created a new one called dismissAddAnotherPopupWithUpdate to handle my fancy M2M widgets. Here's the code i used:
def response_add(self, request, obj, post_url_continue='../%s/'):
"""
Overriding to force the widget to update
"""
resp = super(ModelAdmin, self).response_add(request, obj, post_url_continue)
if request.POST.has_key("_popup"):
return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopupWithUpdate(window, "%s", "%s");</script>' % \
# escape() calls force_unicode.
(escape(obj._get_pk_val()), escape(obj)))
return resp
While Cari's solution certainly works, a simple solution to this would be to specify a valid id in your <a> tag. The id is used so dismissAddAnotherPopup() can select the appropriate field after closing the window. It doesn't matter which id you specify for window.close() to work, as long as it exists.