how to use action object and target in django notifications - django

i"m working on a project with django something blogging like webapp. I'm using django notifications for my websites notification. i'm recieving notifications if someone comment on my post or like the post. However i can't go to the specific post from the notifications by clicking the notification.
my views.py :
#login_required
def like_post(request):
# posts = get_object_or_404(Post, id=request.POST.get('post_id'))
posts = get_object_or_404(post, id=request.POST.get('id'))
# posts.likes.add for the particular posts and the post_id for the post itself its belongs to the post without any pk
is_liked = False
if posts.likes.filter(id=request.user.id).exists():
posts.likes.remove(request.user)
is_liked = False
else:
posts.likes.add(request.user)
is_liked = True
notify.send(request.user, recipient=posts.author, actor=request.user, verb='liked your post.', nf_type='liked_by_one_user')
context = {'posts':posts, 'is_liked': is_liked, 'total_likes': posts.total_likes(),}
if request.is_ajax():
html = render_to_string('blog/like_section.html', context, request=request)
return JsonResponse({'form': html})

From the project's readme, we can see that the Notification model allows you to store extra data in a JSON field. To enable this, you'll first need to add this to your settings file
DJANGO_NOTIFICATIONS_CONFIG = { 'USE_JSONFIELD': True}
After doing so you can store the url of the target object in the field by passing it as a kwarg to the notify.send signal
notify.send(request.user, recipient=posts.author, actor=request.user, verb='liked your post.', nf_type='liked_by_one_user', url=object_url)
You should note however that doing it this way would result in a broken link should you change your url conf so an alternative way of doing it would be to create a view that will return the target objects url which you can call while rendering the notifications.

Related

Sending email notifications to subscribers when new blog post is published in Wagtail

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.

How to get request.user in a code, which does not have any relation to views?

I want to get user.pk from request (for logged in user) in order to avoid additional DB queries.
In views there is incoming request variable. How to get this request somewhere else (not in views)? Directly importing HttpRequest does not help, because HttpRequest gets user object because of middleware modification. How can I get this 'modified' HttpRequest with user object? What do I need to import?
EDIT:
I want to implement customized user address, so user may have 2 types of addresses, like mysite.com/username or mysite.com/id123, so one link from navigation menu (My page) is dynamic. For the creation of navigation menu I use django-sitetree, where I want to do something like:
from sitetree.sitetreeapp import register_items_hook
def my_items_processor(tree_items, tree_sender):
# get somehow `request`
if tree_sender == 'menu.children':
for item in tree_items:
if request.user.username:
item.url = request.user.username
else:
item.url = request.user.pk
return tree_items
I just answered this same question (albeit relating to forms) in another question. Please see my answer there.
Assuming you have setup threadlocals.py as detailed in that post, and stored it as sitetree/threadlocals.py:
from sitetree.sitetreeapp import register_items_hook
from sitetree.threadlocals import get_current_user
def my_items_processor(tree_items, tree_sender):
user = get_current_user()
if tree_sender == 'menu.children':
for item in tree_items:
if user.username:
item.url = user.username
else:
item.url = user.pk
return tree_items

Django: How to return user to correct pagination page after CreateView form_invalid?

I have a paginated comments page. Pagination is provided by django-endless-pagination. When clicking to new paginated pages a get parameter is append to the url. Such as ?page=4.
Each comment on each paginated page displays a 'reply to' comment form containing a captcha field.
My view uses CreateView and I implement form_invalid myself in order to add some data to the context variable. At the end of my form_invalid method I return self.render_to_response(context)
The Problem
If a user attempts to reply to a comment when on page 4, and that user supplies and invalid captcha, then the pagination get parameter (?page=4) is lost during the response.
How can I redirect to the full path (keeping get params) and pass context data along with it?
Thanks
This problem is similar to this SO question, but my scenario is a little different given that I want to maintain my form state (the captcha error mentioned in question) in the redirect.
Thank you #petkostas for pointing out HTTP_REFERER
My resolution to this involved storing the context in cache with a cache key derived from the current timestamp. I redirect to the (same) comments url, but in doing this I append the current comment page as a get parameter and also the timestamp as another get parameter.
Then during get requests the view checks for the existence of the timestamp parameter. If it is provided, then a cache.get call is made to retrieve the needed context data. And finally, the cached item is deleted.
from datetime import datetime
from django.core.cache import cache
from django.shortcuts import redirect
from django.utils.dateformat import format
class MyView(CreateView):
def form_invalid(self, form, **kwargs):
context = {
'form': form,
... other context stuff ...
}
timestamp = format(datetime.now(), u'U')
cache.set('invalid_context_{0}'.format(timestamp), context)
page = 1
full_path = self.request.META.get('HTTP_REFERER')
if '?' in full_path:
query_string = full_path.split('?')[1].split('&')
for q in query_string:
if q.startswith('page='):
page = q.split('=')[1]
response = redirect('my_comments_page')
response['Location'] += '?page={0}&x={1}'.format(page, timestamp)
return response
def get_context_data(self, **kwargs):
context = super(MyView, self).get_context_data(**kwargs)
context.update({
... add context stuff ...
})
if 'x' in self.request.GET:
x = self.request.GET.get('x')
cache_key = 'invalid_context_{0}'.format(x)
invalid_context = cache.get(cache_key)
if invalid_context:
context.update(invalid_context)
cache.delete(cache_key)
return context
I haven't used django-endless-pagination, but Django offers 2 ways to get either the full current request path, or the Refferer of the request:
In your view or template (check documentation on how to use request in templates) for the referring requesting page:
request.META.get('HTTP_REFERER')
Or for the current request full path:
request.full_path()

django - passing information when redirecting after POST

I have a simple form, that when submitted redirects to a success page.
I want to be able to use the data that was submitted in the previous step, in my success page.
As far as I know, you can't pass POST data when redirecting, so how do you achieve this?
At the moment I'm having to just directly return the success page from the same URL, but this causes the dreaded resubmission of data when refreshed.
Is using request.session the only way to go?
I do this all the time, no need for a session object. It is a very common pattern POST-redirect-GET. Typically what I do is:
Have a view with object list and a form to post data
Posting successfully to that form saves the data and generates a redirect to the object detail view
This way you save upon POST and redirect after saving.
An example view, assuming a model of thingies:
def all_thingies(request, **kwargs):
if request.POST:
form = ThingieForm(request.POST)
if form.is_valid():
thingie = form.save()
return HttpResponseRedirect(thingie.get_absolute_url())
else:
form = ThingieForm()
return object_list(request,
queryset = Thingie.objects.all().order_by('-id'),
template_name = 'app/thingie-list.html',
extra_context = { 'form': form },
paginate_by = 10)
You can:
Pass the data (either full data or just id to object) in request.session
Redirect with something like ?id=[id] in URL - where [id] points to your object.
Update:
Regarding pt. 1 above, I meant that you could do (in POST handler):
my_object = MyModel.objects.create(...)
request.session['my_object_id'] = my_object.id
Or you could try passing the whole object (it should work but I'm not 100% certain):
my_object = MyModel.objects.create(...)
request.session['my_object'] = my_object

Django blog reply system

i'm trying to build a mini reply system, based on the user's posts on a mini blog.
Every post has a link named reply. if one presses reply, the reply form appears, and one edits the reply, and submits the form.The problem is that i don't know how to take the id of the post i want to reply to. In the view, if i use as a parameter one number (as an id of the blog post),it inserts the reply to the database.
But how can i do it by not hardcoding?
The view is:
def save_reply(request):
if request.method == 'POST':
form = ReplyForm(request.POST)
if form.is_valid():
new_obj = form.save(commit=False)
new_obj.creator = request.user
new_post = New(1) #it works only hardcoded
new_obj.reply_to = new_post
new_obj.save()
return HttpResponseRedirect('.')
else:
form = ReplyForm()
return render_to_response('replies/replies.html', {
'form': form,
},
context_instance=RequestContext(request))
i have in forms.py:
class ReplyForm(ModelForm):
class Meta:
model = Reply
fields = ['reply']
and in models:
class Reply(models.Model):
reply_to = models.ForeignKey(New)
creator = models.ForeignKey(User)
reply = models.CharField(max_length=140,blank=False)
objects = NewManager()
mentioning that New is the micro blog class
thanks
heyy there. i solved the problem,using your advices, but I've created another.
I was thinking that as the reply form is in another page, simply clicking on that reply link ain't gonna help me retain the post id anyhow, because the blog page is gone, after i push thet reply button. So, in my view, i 've created a function that holds the id of the blog post as a parameter. It saves just as it should, no problem, but now my problem is: HOW CAN I PASS A LINK LIKE
url(r'^save_reply/(?P<id>\d+)/$',
save_reply,
name='save_reply'),
(this is what i hold in my urls.py)
to the reply under each post? I mean, until now, my reply link was simply calling the function replies/save_reply(i had Reply) but now, when i have the id as a parameter, how can i put it in my a href = 'what here'?
here is my views.py that works right:
def save_reply(request, id):
if request.method == 'POST':
form = ReplyForm(request.POST)
if form.is_valid():
new_obj = form.save(commit=False)
new_obj.creator = request.user
u = New.objects.get(pk=id)
new_obj.reply_to = u
new_obj.save()
return HttpResponseRedirect('.')
else:
form = ReplyForm()
return render_to_response('replies/replies.html', {
'form': form,
},
context_instance=RequestContext(request))
and i'm callin it by typing in my browser:
http://127.0.0.1:8000/replies/save_reply/1/ (for instance)
of course, i've removed my foreign key field, as now it is unnecessarry
Thank you!
You need to have a hidden field in your form to capture the PK of whichever instance of New the comment is related to.
Since you're using a ModelForm, your Reply model already has the ForiegnKey relationship established. You can set the widget type to be hidden, so your users don't see it..
# forms.py
class ReplyForm(ModelForm):
class Meta:
model = Reply
fields = ['reply', 'reply_to']
widgets = {
'reply_to': forms.HiddenInput),
}
When you initialize the ReplyForm, you can populate the reply_to field like form = ReplyForm({'reply_to': new.pk}) - where new is an instance of New
BTW you might consider changing the name of your New model to something like Post. 'New' is a bit hard to talk about, and a little confusing since 'new' usually means something completely different in a programming context.
if one presses reply, the reply form appears,
I think this is the part you need to work on. When the reply form is rendered it needs to have the id of the post being replied to with it (the instance of New). This presumably has to come via the request unless you have some other way of keeping track of it?
Something along the lines of:
def save_reply(request):
...
else:
form = ReplyForm()
form.reply_to = New.objects.get(id=request.REQUEST["post_id"])
Thus you'll need to ensure that the link which causes the form to be rendered includes a 'post_id' param (or similar - presumably you already have an equivalent, used for displaying the post in question?).
Alongside the
widgets = {
'reply_to': forms.HiddenInput),
}
code this should render the form as you need it.
The post id has to be passed all the way along the chain
--post_id-> Render Post --post_id-> Render Reply Form --post_id-> Store Reply