I have a Mezzanine blog and I would like to add a little form to every page so users can type their email addresses and click 'subscribe' so, from that moment, an email will be sent to announce any new post to the blog.
I don't see that built in or any existing module for that purpose... Should I program that from scratch? Any ideas?
Since there are no answers, I will try to offer my best guess as to the easiest strategy in this situation. I don't know of any built-in functions in Mezzanine that perform this specific function, so here's how I would approach it:
python manage.py startapp subscriber
Build out basic model - email = models.EmailField(unique=True), etc.
Create basic admin.py
Update settings.py -> INSTALLED_APPS and ADMIN_MENU_ORDER
ADMIN_MENU_ORDER = (
(_("Web Content"), ((_("Pages"), "pages.Page"),
(_("Subscribers"), "subscriber.Subscriber"),
Create a forms.py - something like this:
class SubscriberForm(forms.ModelForm):
class Meta:
model = Subscriber
fields = ['email']
Setup a views.py for GET/POST of above form
Reconfigure urls.py to redirect to the new view function
Then, perhaps the only interesting part of my response, I would copy the Mezzanine Blog app directory into the project directory, and create a signals.py. Maybe add something like this:
#receiver(pre_save, sender=BlogPost, dispatch_uid="save_blogpost")
def save_blogpost(sender, instance, **kwargs):
""" Every time a new blog post is created, notify all subscribers."""
if instance.published:
subs = Subscriber.objects.all()
for sub in subs:
email = EmailMultiAlternatives(
subject="New Blog Post!",
body="A new blog post has been added!",
from_email="example#email.com",
to=[sub.email]
)
email.attach_alternative(self.body, "text/html")
email.send()
Update app.py in the blog app directory to contain this under the Config class:
def ready(self):
import blog.signals # noqa
If you've got Django configured to send mail through an external SMTP mail server, that's easier. If not, you'll likely want to setup Postfix with OpenDKIM and configure SPF to reduce probability the outgoing mail ends up in spam.
(Also, you'll obviously need to handle situations where an admin changes a draft post to a published post, etc.)
Not sure if this helps, but hopefully!
Related
I'm in the process of converting an existing Django project to Wagtail. One issue I'm having is email notifications. In the Django project, I have the ability for people to subscribe to a blog, and whenever a new post is published, the author can manually send out a notification to all subscribers in the admin. However, I'm not sure how to accomplish this in Wagtail.
I've read the docs about the page_published signal (https://docs.wagtail.io/en/stable/reference/signals.html#page-published), however, I'm not sure how I could integrate my current code into it. In addition, I would prefer for the author to manually send out the notification, as the author doesn't want to email their subscribers every time a blog post is edited and subsequently published.
For reference, the current code I have for the Django app is as follows (it only works if the blog app is in normal Django; because the blog models are now in the Wagtail app, the current code no longer works).
models.py
class Post(models.Model):
"""Fields removed here for brevity."""
...
def send(self, request):
subscribers = Subscriber.objects.filter(confirmed=True)
sg = SendGridAPIClient(settings.SENDGRID_API_KEY)
for sub in subscribers:
message = Mail(
from_email=settings.FROM_EMAIL,
to_emails=sub.email,
subject="New blog post!",
html_content=( #Abbreviated content here for brevity
'Click the following link to read the new post:' \
'{}'\
'Or, you can copy and paste the following url into your browser:' \
'{}/{}'\
'<hr>If you no longer wish to receive our blog updates, you can ' \
'unsubscribe.').format(
request.build_absolute_uri('/post'),
self.slug,
self.title,
request.build_absolute_uri('/post'),
self.slug,
request.build_absolute_uri('/delete'),
sub.email,
sub.conf_num
)
)
sg.send(message)
admin.py
def send_notification(modeladmin, request, queryset):
for post in queryset:
post.send(request)
send_notification.short_description = "Send selected Post(s) to all subscribers"
#admin.register(Post)
class PostAdmin(SummernoteModelAdmin):
...
actions = [send_notification]
Any suggestions or feedback would be greatly appreciated! Thanks in advance!
EDIT 1
I got a suggestion from the Wagtail Slack to use the register_page_action_menu_item hook (https://docs.wagtail.io/en/stable/reference/hooks.html?highlight=hooks#register-page-action-menu-item). I successfully implemented the action menu item on Wagtail's page editor, however I cannot get my email method to execute (likely due to my not knowing how to properly use the hook). Below is the code from my wagtail_hooks.py file.
from wagtail.admin.action_menu import ActionMenuItem
from wagtail.core import hooks
from .models import Subscriber
from django.conf import settings
from django.core.mail import send_mail
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import (Mail, Attachment, FileContent, FileName, FileType, Disposition)
class NotificationMenuItem(ActionMenuItem):
name = 'email-notification'
label = "Notify Subscribers of New Post"
def send(self, request):
"""Used the same def send() method here from my models.py above"""
#hooks.register('register_page_action_menu_item')
def register_notification_menu_item():
return NotificationMenuItem(order=100)
If anyone has advice on how to fix it so it executes, please let me know!
EDIT 2
More problems! (Though I think I'm getting closer.)
Modifying the wagtail_hooks.py to the following, I am able to send an email, but it happens on pageload. So every time I load a blog post in the editor, it sends an email. Clicking the action menu item I created triggers a page reload, which then sends another email (so I don't think my action menu item is actually working when clicked).
Another problem: Because I moved the send() method into the NotificationMenuItem class, I am unable to dynamically generate a blog post's slug and title in the urls of the email.
wagtail_hooks.py
class NotificationMenuItem(ActionMenuItem):
name = 'email-notification'
label = "Notify Subscribers of New Post"
def send(self, request):
"""Used the same def send() method here from my models.py above"""
def get_url(self, request, context):
self.send(request)
EDIT 3
I managed to get the notification system to work in the regular Django admin despite the models being Wagtail models. While this moves the current website's functionality over to the new wagtail site, I still have been unable to solve the most recent issues raised under Edit 2.
Here's the new code in the admin:
def send_notification(modeladmin, request, queryset):
for post in queryset:
post.send(request)
send_notification.short_description = "Send selected Post(s) to all subscribers"
class BlogPageAdmin(admin.ModelAdmin):
list_display = ('title', 'author', 'body')
search_fields = ['title', 'body']
actions = [send_notification]
admin.site.register(BlogPage, BlogPageAdmin)
Better you need to use a post_save signal.
#receiver(post_save, sender=BlogPost)
def send_mail_to_subs(sender, instance, created, **kwargs):
current_site = Site.objects.get_current()
domain = current_site.domain
if created:
for subs in instance.author.subscribed.all():
send_mail(
f'New Post from {instance.author}',
f'Title: {instance.post_title}\nContent: {instance.post_content}\nDate Created: {instance.created}\nUrl: {domain}',
'youremail',
[subs.email],
)
Good coding.
My site runs multiple domains. Users can register on each of these domains. As a result, I need to make django-registration-redux:
use the correct email address for sending registration/password reset emails
use the correct email password for sending registration/password reset emails
use the correct domain within registration/password reset emails
I've been digging into the source code for django-registration-redux and believe that I need to update the send_email method within registration/models.py (https://github.com/macropin/django-registration/blob/master/registration/models.py) with my required changes.
I'm assuming the best way to add this cusomtization is as follows:
run 'pip uninstall django-registration-redux==2.2'
run 'pip freeze > requirements.txt'
from the source code, pull the 'registration' folder into my project
go into myproject/registration/models.py and manually update the send_email method so that it includes my changes.
Is there an easier or more correct way to build my custom logic into def send_email without making the changes noted above?
Thanks!
You could subclass the model then override the function that calls send_email() and patch it with your custom function.
from unittest import mock
from django_registration import models
def custom_send_email():
print('sending custom email')
class CustomEmailRegistration(models.Registration):
def function_calling_send_email(self):
with mock.patch('models.send_email', side_effect=custom_send_email):
super().function_calling_send_email()
Solution is to add a context processor that grabs the company name and website based on the current website, which can be accessed via request, and then reference the context variable within the django-registration email templates :)
This is how you add a context processor that gets the current hostname from the request:
Add a file my_ctx_proc.py to your application myapp:
def get_info_email(request):
return {
'info_email': "info#" + request.META['HTTP_HOST'],
}
In your settings.py, add in TEMPLATES OPTIONS context_processors at the end:
'myapp.my_ctx_proc.get_info_email',
Now you can insert {{ info_email }} in your email template, eg. templates/django_registration/activation_body_email.txt
I am trying to add the content of Django-CMS placeholders to the search index (using Algolia, but I guess this could apply for any indexing service, like Elasticsearch or similar) as soon as they are updated.
Using Django 1.10, django-cms 3.42, I have this model (simplified for this question):
from django.db import models
from cms.models.fields import PlaceholderField
from cms.models import CMSPlugin
class NewsItem(models.Model):
title=models.CharField(_('title'), max_length=200),
content = PlaceholderField('news_content')
I need to do some extra processing as soon as the model field 'content' is saved, and apparently the best way to check for that is to monitor the CMSPlugin model. So I look for saves using from django.db.models.signals.post_save like this:
#receiver(post_save, sender=CMSPlugin)
def test(sender, **kwargs):
logger.info("Plugin saved.")
Now, the problem is that post_save is not triggered as I thought it would. With normal CMS Pages, I noticed that post_save is only triggered when a Page is published, but there is no apparent way to publish a placeholder when used outside the CMS.
The closest similar case I've found is Updating indexes on placeholderfields in real time with django/haystack/solr, but the suggested solution doesn't work.
How could I go about resolving this?
Thank you!
We also had the same search indexing problem when we were implementing djangocms-algolia package, since a placeholder update doesn't trigger an update of the index.
For CMS pages we utilized post_publish and post_unpublish from cms.signals module here.
And for cms apps that use placeholders (eg djangocms-blog) we attached the listeners to post_placeholder_operation, but beware that to make it work your ModelAdmin needs to inherit from PlaceholderAdminMixin:
def update_news_index(sender, operation: str, language: str, **kwargs) -> None:
placeholder: Optional[Placeholder] = None
if operation in (ADD_PLUGIN, DELETE_PLUGIN, CHANGE_PLUGIN, CLEAR_PLACEHOLDER):
placeholder = kwargs.get('placeholder')
elif operation in (ADD_PLUGINS_FROM_PLACEHOLDER, PASTE_PLUGIN, PASTE_PLACEHOLDER):
placeholder = kwargs.get('target_placeholder')
elif operation in (MOVE_PLUGIN, CUT_PLUGIN):
placeholder = kwargs.get('source_placeholder')
else:
pass
if placeholder:
post: Post = Post.objects.language(language_code=language).filter(content=placeholder).first()
if post:
post.save()
signals.post_placeholder_operation.connect(update_news_index, PostAdmin)
signals.post_placeholder_operation.connect(update_news_index, PageAdmin)
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'm using django-socialregistration to manage my site's connection with Facebook.
When a user clicks the "Connect with Facebook" button, I am able to automatically create a new Django user and log them in. However, I also need to create a UserProfile (my AUTH_PROFILE_MODULE) record for them which contains their Facebook profile information (email, name, location).
I believe I need to override socialregistration's "setup" view so I can do what I need to do with UserProfile. I've added the following to my project's urls.py file:
url( r'^social/setup/$', 'myapp.views.socialreg.pre_setup', name='socialregistration_setup'),
My custom view is here "/myapp/views/socialreg.py" and looks like:
from socialregistration.forms import UserForm
def pre_setup(request, template='socialregistration/setup.html',
form_class=UserForm, extra_context=dict()):
# will add UserProfile storage here...
return socialregistration.views.setup(request, template, form_class, extra_context)
The socialregistration view signature I'm overriding looks like this:
def setup(request, template='socialregistration/setup.html',
form_class=UserForm, extra_context=dict()):
...
I'm getting the error "ViewDoesNotExist at /social/setup/: Could not import myapp.views.socialreg. Error was: No module named socialregistration.views" when I try the solution above.
The socialregistration app is working fine when I don't try to override the view, so it is likely installed correctly in site-packages. Anyone know what I'm doing wrong?
OK, as Tim noted, this particular problem was path related.
Bigger picture, the way to accomplish what I wanted (creating a linked UserProfile when django-socialregistration creates a user) is best done by passing in a custom form into socialregistration's "setup" view, as the author suggested here: http://github.com/flashingpumpkin/django-socialregistration/issues/issue/36/#comment_482137
Intercept the appropriate url in your urls.py file:
from myapp.forms import UserForm
url('^social/setup/$', 'socialregistration.views.setup',
{ 'form_class': UserForm }, name='socialregistration_setup'),
(r'^social/', include('socialregistration.urls')),
You can base your UserForm off socialregistration's own UserForm, adding in code to populate and save the UserProfile.