In my Django app, I have a Newsletter model. Now I'd like to be able to send the newsletter (and even resend it) from Django Admin.
I could do this with a hook on the Model.save() method but is there another way that is not tied to the Model?
Thanks.
Admin actions allow you to easily hook up custom actions which can be performed on selected items from the admin's list pages.
If you are doing it from the admin then you'll need to override the save() method, but it can be the AdminModel save... doesn't need to be the full Model save.
However, if you are emailing a lot of emails, a better approach would be to install django-mailer which puts emails into a queue for later processing and then provides you with a new management command: send_mail.
So once you're ready to send the newsletter out you can manually run python manage.py send_mail. Any emails with errors will be moved to a deferred queue where you can retry sending them later.
You can automate this by running manage.py send_mail from cron.
If you really want to get fancy and do it from admin site, install django-chronograph and set up your send_mail schedule from there.
2022 Update:
django.__version__ == 4.1 and maybe ealier.
You can add a custom action at /admin/.../change by overriding django.contrib.admin.options.ModelAdmin._changeform_view:
from django.contrib import admin
class MyModelAdmin(admin.ModelAdmin):
def _changeform_view(self, request, object_id, form_url, extra_context):
if '<your-action>' in request:
# 1. check permissions
# 2. do your thing
print(request)
return super()._changeform_view(request, object_id, form_url, extra_context)
You can now add simple sumbit button to your form with your custom action:
<input type="submit" name="<your-action>" value="<button-title>">
Advantage
Zero javascript.
Authentication (CSRF) protected.
All stock admin actions will work with your form (add, save and continue, ...)
You can try this YouTube tutorial. Just change:
def available(modeladmin, request, queryset):
queryset.update(status='ava')
def not_available(modeladmin, request, queryset):
queryset.update(status='not')
to something like this:
def send(modeladmin, request, queryset):
for data in queryset:
subject=data.title
message=data.mesage
for d in Users.objects.filter(newsletter=True):
email=d.email
sendemail = EmailMessage(
subject, message + unsubscribe,
'emailo#mdjangotutsme.com',
[email], [],
headers = {'Reply-To': 'emailo#mdjangotutsme.com'}
)
sendemail.content_subtype = "html"
sendemail.send()
You can create custom django admin actions.
For example, I have Person model below:
# "store/models.py"
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=30)
def __str__(self):
return self.name
Now, I created uppercase() and lowercase() admin actions in Person admin as shown below. *actions = ("uppercase", "lowercase") is necessary to display uppercase() and lowercase() admin actions:
# "store/admin.py"
from django.contrib import admin, messages
from .models import Person
#admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
actions = ("uppercase", "lowercase") # Necessary
#admin.action(description='Make selected persons uppercase')
def uppercase(modeladmin, request, queryset):
for obj in queryset:
obj.name = obj.name.upper()
obj.save()
messages.success(request, "Successfully made uppercase!")
#admin.action(description='Make selected persons lowercase')
def lowercase(modeladmin, request, queryset):
for obj in queryset:
obj.name = obj.name.lower()
obj.save()
messages.success(request, "Successfully made lowercase!")
Then, if I select 2 persons and Make selected persons uppercase then click on Go as shown below:
I can make 2 persons uppercase as shown below:
Then, if I select 2 persons and Make selected persons lowercase then click on Go as shown below:
I can make 2 persons lowercase as shown below:
In addition, if I remove #admin.action() as shown below
# "store/admin.py"
# ...
# #admin.action(description='Make selected persons uppercase')
def uppercase(modeladmin, request, queryset):
for obj in queryset:
obj.name = obj.name.upper()
obj.save()
messages.success(request, "Successfully made uppercase!")
# #admin.action(description='Make selected persons lowercase')
def lowercase(modeladmin, request, queryset):
for obj in queryset:
obj.name = obj.name.lower()
obj.save()
messages.success(request, "Successfully made lowercase!")
Actual function names are displayed as shown below:
Related
I have a data model in which some fields can only be set initially per each instance of the class, and once set, they should never change. The only way that can be allowed to change such an object should be by deleting it and creating a new one.
Pseudo code:
from django.db import models
from django.core.exceptions import ValidationError
class NetworkConnection(models.Model):
description = models.CharField(max_length=1000)
config = models.CharField(max_length=1000)
connection_info = models.CharField(max_length=5000)
def clean(self):
from .methods import establish_connection
self.connection_info = establish_connection(self.config)
if not self.connection_info:
raise ValidationError('Unable to connect')
def delete(self):
from .methods import close_connection
close_connection(self.config)
super(NetworkConnection, self).delete()
As in the above code, the user should initially input both the config and the description fields. Then Django verifies the config and establishes some sort of network connection based on such configurations and saves its information to another field called connection_info.
Now since each object of this class represents something that cannot be edited once created, I need to hind the config field from the admin page that edits the object, leaving only the description field; However, the config field still needs to be there when adding a new connection. How do I do this?
The following is an example of my last admin.py attempt:
from django.contrib import admin
from .models import NetworkConnection
class NetworkConnectionAdmin(admin.ModelAdmin):
exclude = ('connection_info')
def change_view(self, request, object_id, extra_context=None):
self.exclude = ('config')
return super(NetworkConnection, self).change_view(request, object_id, extra_context)
admin.site.register(NetworkConnection, NetworkConnectionAdmin)
But unfortunately, it seems to hide the config field from the add page too. Not only the change page
You can achieve that with a custom method directly in your NetworkConnectionAdmin class:
from django.contrib import admin
from .models import NetworkConnection
class NetworkConnectionAdmin(admin.ModelAdmin):
exclude = ('connection_info')
def get_readonly_fields(self, request, obj=None):
if obj:
return ["config", "description"]
else:
return []
admin.site.register(NetworkConnection, NetworkConnectionAdmin)
I got it from the Django Admin Cookbook.
Turns out this could be done using the ModelAdmin.get_from function but if anybody has a better answer, please share it
Solution using get_from would be:
admin.py
from django.contrib import admin
from .models import NetworkConnection
class NetworkConnectionAdmin(admin.ModelAdmin):
exclude = ('connection_info')
def get_form(self, request, obj=None, **kwargs):
if obj:
kwargs['exclude'] = ('config')
return super(NetworkConnectionAdmin, self).get_form(request, obj, **kwargs)
admin.site.register(NetworkConnection, NetworkConnectionAdmin)
I'm using Django-allauth, I have a list of emails and I want to restrict registration to this list. My idea was to check the signing up user email and, if not in the emails list, stop registration process and redirect.
As suggested by Chetan Ganji I tried editing allauth.account.views.SignupView but it does not subscribe the form_valid method. How can i do that? Thank you for help
from allauth.account.views import SignupView
class AllauthCustomSignupView(SignupView):
def form_valid(self, form):
email = form.cleaned_data['email']
auth_user_list = [ 'email_1',
'email_2',
...
]
if not any(email in s for s in auth_user_list):
return reverse('url')
return super(MySignupView, self).form_valid(form)
You can do it by extending the DefaultAccountAdapter class. You have to figure out a way to store and fetch the restricted list on demand.
You can then use the adapters and raise validation error in the registration from. Extend a DefaultAccountAdapter and override the clean_email method. Create an adapter.py in your project directory and extend the default adapter class.
from allauth.account.adapter import DefaultAccountAdapter
from django.forms import ValidationError
class RestrictEmailAdapter(DefaultAccountAdapter):
def clean_email(self,email):
RestrictedList = ['Your restricted list goes here.']
if email in RestrictedList
raise ValidationError('You are restricted from registering. Please contact admin.')
return email
Finally, point the account adapter in settings.py to your extended class.
ACCOUNT_ADAPTER = 'YourProject.adapter.RestrictEmailAdapter'
Maybe try with this code
class AllauthCustomSignupView(SignupView):
def form_valid(self, form):
email = form.cleaned_data['email']
auth_user_list = [ 'email_1',
'email_2',
...
]
if email in auth_user_list:
return reverse('blocked-email') # whatever url, make sure that the url is defined inside form_valid or in approriate location.
else:
return super(AllauthCustomSignupView, self).form_valid(form)
class BlockedEmailView(TemplateView):
template_name = "blocked-email.html"
Add below line to your urls.py
url(r'^signup/$', views.AllauthCustomSignupView.as_view(), name="signup"),
url(r'^blocked/email$', views.BlockedEmailView.as_view(), name="blocked-email"),
Also, you will need to change the action attribute of the form that SignupView has. So, your will have to override the template of that view, keep everything else the same, just change the action to point to "signup/".
I need to write a method in my ModelAdmin class that calls ModelAdmin.save_model(), without the user actually clicking on save.
(The reason I want to do this is that I set up some custom buttons in my Django Admin object view. They work but any modified form data is lost, I want to save the data in the form before running the actions connected to the button.)
Here's my code:
from django.contrib import admin
from .models import Object
class ObjectAdmin(admin.ModelAdmin):
def action_method(self, request, object):
self.save_model(request=request, obj=object, form=self.form, change=True)
admin.site.register(Object, ObjectAdmin)
This doesn't raise any errors but also doesn't save my data.
I'm guessing my problem might be to do with the form.
Any help?
class AuthorAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
# Code Logic
obj.save()
Documentation
I'm trying to crate Django web project , and there are some User backend application where every URL ( view ) only for authenticated users.
I can write a condition in every view for example:
User/urls.py
urlpatterns = patterns('',
url(r'^settings/', 'User.views.user_settings'),
url(r'^profile/', 'User.views.profile'),
#Other User Admin panel Urls
)
User/views.py
def user_settings(request):
if request.user is None:
return redirect('/login')
#Some other code
def profile(request):
if request.user is None:
return redirect('/login')
#Some other code
As you can see it's not well coded but it works fine.
The only thing that I want to know , is it possible to add some condition for urls.py file all urls for not adding same code lines in every view function.
For example Symfony , Laravel and Yii frameworks have something like that what I want to do.
Is it possible do this in Django ? :)
Edited Here
With #login_required I need to add it for every view, I need something for all urls for example:
In Symfony framework I can write
{ path: ^/myPage, roles: AUTHENTICATED } and every url like this /myPage/someUrl will be asked for authentication.
I believe that Django have something like that :)
Thanks.
Well if you use a class based view, it makes easier for you to add #login_required. For example you can create a class based view here:
class BaseView(TemplateView):
#method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super(BaseView, self).dispatch(*args, **kwargs)
Now override it each time you want to create a new view. For example:
class SettingsView(BaseView):
def get(request):
return (...)
It will check each time at url dispatch if the user is logged in.
Also, if you use a class based view, you can override get() to check if the user is authenticated.
class BaseView(TemplateView):
template_name= None
role= None
def get(self, request, *args, **kwargs):
if request.user is not None and role is "AUTHENTICATE":
return super(BaseView, self).get(request, *args, **kwargs)
else:
raise Exception('user is not logged in')
urls.py:
url(r'^settings/', BaseView.as_view(template_name='/sometemplate', role="AUTHENTICATE")
You can use the #login_required decorator:
https://docs.djangoproject.com/en/1.5/topics/auth/default/#the-login-required-decorator
From the docs:
login_required() does the following:
If the user isn’t logged in, redirect to settings.LOGIN_URL, passing the current absolute path in the query string. Example: /accounts/login/?next=/polls/3/.
If the user is logged in, execute the view normally. The view code is free to assume the user is logged in.
from django.contrib.auth.decorators import login_required
#login_required
def my_view(request):
...
You could use this : https://docs.djangoproject.com/en/1.6/topics/auth/default/#the-login-required-decorator
from django.contrib.auth.decorators import login_required
#login_required
def my_view(request):
...
I had to override the delete method for admin like so :
def fully_delete_selected_photos(self, request, queryset):
# Code to do my specific delete method.
fully_delete_selected_photos.short_description = "Delete Selected Photos"
class VehiclePhotoAdmin(admin.ModelAdmin):
search_fields = ('listing_id',)
list_display = ('listing_id', 'disp_VehiclePhoto')
actions = [fully_delete_selected_photos]
def get_actions(self, request):
actions = super(VehiclePhotoAdmin, self).get_actions(request)
del actions['delete_selected']
return actions
This works fine but I lost the confirmation of deletion. So when the user selects my "Delete Selected Photos" option it immediate just goes through with the delete. How can I get some kind of confirmation for the overridden deletion? I am confused about the redirections of views in admin.
Thank you for your time!
You can return HttpResponseRedirect in fully_delete_selected_photos method, and make custom view , thats inherit template from admin, and takes queryset parameter, and lists all selected photos.
But if you want to simplify it, add some JavaScript to admin view.