How can i implement Notifications system in django - python

I created an app where user's can post a question and get answers from others users.
Now I want to implement a notification system, so that when a user answer a question, the author of that question will receive notification. Like social media notifications.
The home templates:
<div class="container">
<div class="row justify-content-center">
<div class="row justify-content-center">
<div class="col-md-6">
Ask Question
Notifications
FeedBack
Log Out
</div>
</div>
</div>
</div>
<div class="container">
<div class="row justify-content-center">
{% for question in list_of_question reversed %}
<div class="col-md-4">
<div class="card my-3">
<div class="card-header">
<p class="card-title">{{question.user.username.upper}}</p>
</div>
<div class="card-body">
<a href="{% url 'view-Question' question.id %}" style="text-decoration: none;">
<p class="card-title">{{question.title}}</p>
</a>
<p>Category: {{question.category}}</p>
</div>
</div>
</div>
{%endfor%}
</div>
</div>
the models:
class Question(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=100, blank=False, null=False)
body = RichTextField(blank=False, null=False)
category = models.CharField(max_length=50, blank=False, null=False)
def __str__(self):
return str(self.title)
class Answer(models.Model):
user = models.ForeignKey(User, blank=False, null=False, on_delete=models.CASCADE)
answer = RichTextField(blank=False, null=False)
post = models.ForeignKey(Question, blank=False, null=False, on_delete=models.CASCADE)
def __str__(self):
return str(self.user)
The views:
class My_Question(LoginRequiredMixin, CreateView):
model = Question
fields = ['title', 'body', 'category']
template_name = 'question.html'
success_url = reverse_lazy('index')
def form_valid(self, form):
form.instance.user = self.request.user
return super (My_Question, self).form_valid(form)
class My_Answer(LoginRequiredMixin, CreateView):
model = Answer
fields = ['answer']
template_name = 'answer.html'
success_url = reverse_lazy('index')
def form_valid(self, form):
form.instance.user = self.request.user
form.instance.post_id = self.kwargs['pk']
return super (My_Answer, self).form_valid(form)
def viewQuestion(request, pk):
question = Question.objects.get(id=pk)
answers = Answer.objects.filter(post_id=question)
context = {'question':question, 'answers':answers}
return render(request, 'viewQuestion.html', context)
the home page view:
#login_required(login_url='login')
def index(request):
query = request.GET.get('q', None)
list_of_question = Question.objects.all()
if query is not None:
list_of_question = Question.objects.filter(
Q(title__icontains=query) |
Q(category__icontains=query)
)
context = {'list_of_question':list_of_question}
return render(request, 'index.html', context)
the urls
path('index/', views.index, name='index'),
path('view/<int:pk>/', views.viewQuestion, name='view-Question'),
path('question/<int:pk>/answer/', views.My_Answer.as_view(),
name='answer'),
path('question/', views.My_Question.as_view(), name='question'),

Here is an outline for a basic notification system in Django:
Model
You need a model to store notifications. Each notification belongs to a user and has content (i.e. a text message). You also need to store whether a message has been read and a timestamp:
class Notification(models.Model):
is_read = models.BooleanField(default=False)
message = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
Creating a notification
You can then create a new notification for a user when needed, e.g. when another user answers a question you can also create a notification for the question's owner in the view.
class My_Answer(LoginRequiredMixin, CreateView):
...
def form_valid(self, form):
...
form.instance.user = self.request.user
question_author = form.instance.post.user
Notification.objects.create(user=question_author, text="New answer!")
...
return super().form_valid(form)
List of notifications
Then you need a page that lists all notifications for the current user. That can be implemented with a standard list view.
The query would look something like this:
class NotificationListView(ListView):
model = Notification
def get_queryset(self):
return Notifications.objects.filter(user=self.request.user).order_by("-timestamp")
You of course also need to define a URL and a template for this new view. We will define the URL name as notifications.
Showing new notifications to users
Finally, you need to inform users about new notifications. This can be done by checking how many unread notifications the current user has and showing a badge on the web badge. This would be part of the index view.
#login_required(login_url='login')
def index(request):
...
unread_notifications = Notification.objects.filter(user=request.user, is_read=False).count()
context["unread_notifications"] = unread_notifications
...
Then on your home page you need a link to a page that shows all notifications and a badge that shows how many unread notifications the current user has. Something like this:
<a href="{% url "notifications" %}">
Notifications
{% if unread_notifications %}
<span class="badge bg-secondary">{{ unread_notifications }}</span>
{% endif %}
</a>
Real life example
If you want to see how this is implemented in a real project, here is the link to an open source project called "Alliance Auth" that implements a portal web page and has a very similar notification architecture. The link is to the notification app within that portal: https://gitlab.com/allianceauth/allianceauth/-/tree/master/allianceauth/notifications

Related

Django: get_context_data for comments related post

I have a models:
class Post(models.Model):
post_text = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
class Comment(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='author')
post_relation = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='comments')
comment_text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
is_active = models.BooleanField(default=True)
In my views I need get comments for posts in get_context_data:
class ResultsView(DetailView, FormMixin):
model = Post
template_name = 'posts.html'
form_class = CommentForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['comm'] = Comment.objects.filter(is_active=True)
return context
But in comm i get all comments in db.
In html:
{% for comment in question.comments.all %}
<div class="media mb-4">
<div class="media-body">
<h5 class="mt-0">{{ comment.author }}</h5>
{{ comment.comment_text }}
</div>
</div>
{% endfor %}
I try {% for comment in comm %}, try {% for comment in comm.all %} and always get all comments in db, not only comments in post.
Also I try fix this string in views: context['comm'] = Comment.objects.filter(is_active=True), but don't have a result.
The answer seems to be very simple, but I've already spent several hours trying and reading.
You can filter with:
class ResultsView(FormMixin, DetailView):
model = Post
template_name = 'posts.html'
form_class = CommentForm
def get_context_data(self, **kwargs):
return super().get_context_data(
**kwargs, comms=self.object.comments.filter(is_active=True)
)
and then render with:
{% for comment in comms %}
# …
{% endfor %}
Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.
Note: The related_name=… parameter [Django-doc]
is the name of the relation in reverse, so from the User model to the Comment
model in this case. Therefore it (often) makes not much sense to name it the
same as the forward relation. You thus might want to consider renaming the author relation to comments.

How can I link a comment to its corresponding post in Django?

I am building a Django project where I have an index page that lists all posts. The user can click on the name of a post and this will take them to a detail page with the complete post information (date, content, category). This detail page also has a link that will take the user to a form where they can leave a comment. Once the user clicks submit they are supposed to navigate back to the post detail page and the comment is supposed to be there. The issue I am having right now is that the comment is being automatically assigned to the first post in the index list rather than the post the user had visited (I think this may have something to do with the current default setting in my models, but how else can I get the post id?). How can I make it so that the comment is assigned to its correct post? I have tried everything with the models and views but nothing seems to work. Thank you for your help, I think the solution to this might be simple but I can't find it anywhere.
Here is my relevant models:
class UserPost(models.Model):
title = models.CharField(max_length=255)
category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True)
content = models.TextField()
created_on = models.DateTimeField(auto_now_add=True)
author = models.ForeignKey(User, default=1,
on_delete = models.CASCADE
)
#id = models.AutoField(primary_key=True)
def __str__(self):
"""String for representing the UserPost object."""
return self.title
def get_absolute_url(self):
"""Returns the url to access a detail record for this user post."""
return reverse('userpost-detail', args=[str(self.id)])
class Comment(models.Model):
author = models.ForeignKey(User, default=1,
on_delete = models.CASCADE
)
content = models.TextField()
created_on = models.DateTimeField(auto_now_add=True)
def comment_default():
return {UserPost.id}
post = models.ForeignKey(UserPost, default= comment_default, on_delete=models.CASCADE, related_name="comments")
def __str__(self):
"""String for representing the comment object."""
return '%s - %s - %s' % (self.post.title, self.author, self.created_on)
def get_absolute_url(self):
return reverse('userpost-detail', args=[str(self.post.id)])
And my views:
class UserPostDetailView(generic.DetailView):
model = UserPost
#post = UserPost.objects.get(id=id)
#comments = Comment.objects.filter(Comment.post)
def get_context_data(self, **kwargs):
context = super(UserPostDetailView, self).get_context_data(**kwargs)
context['Comment'] = UserPost.comments
return context
class PostCreate(CreateView):
model = UserPost
fields = ['title', 'category', 'content']
class CommentCreate(CreateView):
model = Comment
fields = ['post','content']
And my html:
{% extends "base.html" %}
{% block page_content %}
<h1>Title: {{ userpost.title }}</h1>
<p><strong>Author:</strong> {{ userpost.author }}</p>
<p><strong>Content:</strong> {{ userpost.content }}</p>
<p><strong>Category:</strong> {{ userpost.category }}</p>
<a class="btn btn-primary" href="{% url 'comment-create' %}" role="button">Leave a Comment</a>
<h3>Comments:</h3>
{% for comment in userpost.comments.all %}
<p>
On {{comment.created_on.date }}
<b>{{ comment.author }}</b> wrote:
</p>
<p>{{ comment.content }}</p>
<hr>
{% endfor %}
{% endblock %}
You need to pass Post ID in your url for this.
path("comment/<int:post_id>/", CommentCreateView, name="comment-create")
Now in template
<a class="btn btn-primary" href="{% url 'comment-create' userpost.id %}" role="button">Leave a Comment</a>
Views
class CommentCreateView(CreateView):
model = Comment
fields = ['content'] # remove field post here
def form_valid(self, form):
form.instance.post_id = self.kwargs.get("post_id")
return super().form_valid(form)

duplicate key value violates unique constraint "photo_photo_user_id_key" DETAIL: Key (user_id)=(102) already exists

I receive the error above whenever users want to change their already existing profile.
This is the views.py
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.db import transaction
from .models import Photo
from .forms import PhotoForm
Create your views here.
#login_required
#transaction.atomic
def upload(request, pk=None):
instance = Photo.objects.get(pk=pk) if pk else None
context = dict(save_pk=pk or "")
if request.method == 'POST':
# Only backend upload should be posting here
context['backend_form'] = form = PhotoForm(request.POST, request.FILES, instance=instance)
if form.is_valid():
# Uploads image and creates a model instance for it
context['posted'] = form.save(commit=False)
context['posted'].user = request.user
context['posted'].save()
instance = Photo.objects.get(pk=pk) if pk else None
else:
# Form demonstrating backend upload
context['backend_form'] = PhotoForm(instance=instance)
return render(request, 'photo/upload.html', context)
from the stack trace, the problem is coming from this line
context['posted'].save()
How do I go about it? I want the users to be able to change their profiles without trying to create a new instance afresh.
This is the HTML template
<main class="mt-5 pt-5 container">
<!--A standard form for sending the image data to your server -->
<div class="mt-5 pt-5 container bg-white">
<h3 class="text-center">Upload</h3>
<div class="mb-3">
<div class="mb-auto p-2 bd-highlight">
<form action="{% url 'upload' %}" method="post" enctype="multipart/form-data" class="text-center mt-5 ">
{% csrf_token %}
{{ backend_form }}
</div>
<div class="p-2 bd-highlight text-center">
<input type="submit" value="Upload" class="text-center xbtn btn btn-primary">
</div>
</form>
</div>
{% if posted %}
<div class="results">
{% if posted.errors %}
Errors: {{ posted.errors }}
{% else %}
<div class="uploaded_info">
<div class="img-responsive">
{% cloudinary posted.image THUMBNAIL %}
</div>
</div>
{% endif %}
</div>
{% endif %}
</div>
</main>
<footer class="footerofupload">
{% block footer %} {% include 'footer-min.html' %} {% endblock footer %}
</footer>
models.py
from django.db import models
from cloudinary.models import CloudinaryField
from xpro.models import User
# Create your models here.
class Photo(models.Model):
# Misc Django Fields
user = models.OneToOneField(User, on_delete=models.CASCADE, null=True, related_name='photo')
create_time = models.DateTimeField(auto_now_add=True)
title = models.CharField("Title (optional)", max_length=150, blank=True)
# Points to a Cloudinary image
image = CloudinaryField('image')
""" Informative name for mode """
def __unicode__(self):
try:
public_id = self.image.public_id
except AttributeError:
public_id = ''
return "Photo <%s:%s>" % (self.title, public_id)
Below is the user model
class User(AbstractUser):
GENDER_CHOICES = (
('O', 'Other'),
('M', 'Male'),
('F', 'Female'),
)
is_student = models.BooleanField(default=False)
is_school = models.BooleanField(default=False)
is_sponsor = models.BooleanField(default=False)
email = models.EmailField(unique=True)
email_confirmed = models.BooleanField(default=False)
phone_number = models.CharField(max_length=12)
gender = models.CharField(max_length=1, choices=GENDER_CHOICES, default='O')
organization = models.ForeignKey(Organization, on_delete=models.CASCADE, null=True, editable=False)
# avatar = models.ImageField(upload_to='media/avarta', blank=True, default= '/media/avarta/default.png', null=True,)
You have a OneToOneField between User and Photo as here:
user = models.OneToOneField(User, on_delete=models.CASCADE, null=True, related_name='photo')
This essentially means for one User there can only be one Photo (and vice-versa). If you want multiple photos for one user change it to a ForeignKey:
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='photos')
If you want only one photo for a user delete any previous photos before adding any new one:
Photo.objects.filter(user=request.user).delete()
context['posted'] = form.save(commit=False)

How to fix: TypeError: QuerySet.annotate() received non-expression(s): eqmunir

I am adding a like functionality in my website where users can like each others posts.
I have done this successfully, however have one issue. This is checking whether the user has already liked the post, which has to be performed specifically in my HOME view.
This is so I can render my home page. To encounter this issue, I perform a .annotate() on my posts when retrieving them, and see if a user has liked a post.
I then pass this onto my home template and check if the user exists within the posts likes property.
Here's the related code.
models.py:
class Post(models.Model):
file = models.ImageField(upload_to='images/')
summary = models.TextField(max_length=600)
pub_date = models.DateTimeField(auto_now=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
likes = models.ManyToManyField(User, through='Like', related_name='likes')
def __str__(self):
return self.user.username
def pub_date_pretty(self):
return self.pub_date.strftime('%b %e %Y')
def summary_pretty(self):
return self.summary[:50]
#property
def total_likes(self):
return self.likes.count()
class Like(models.Model):
status = models.BooleanField()
post = models.ForeignKey(Post, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
views.py:
def home(request):
posts = Post.objects.all()
liked_or_not = Post.objects.annotate(likes__user=request.user)
return render(request, 'posts/home.html', {'posts': posts, 'liked_or_not': liked_or_not})
home.html:
{% if liked_or_not == True %}
<button class="btn btn-primary btn-lg btn-block"><span class="oi oi-caret-top"></span> Unlike {{ post.total_likes }} </button>
{% else %}
<button class="btn btn-primary btn-lg btn-block"><span class="oi oi-caret-top"></span> Like {{ post.total_likes }} </button>
{% endif %}
<form id="likepost{{ post.id }}" method="POST" action="{% url 'likepost' post.id %}">
{% csrf_token%}
<input type="hidden">
</form>
For those coming here for other possibilities in debugging the error message, I had made a mistake and entered in a query analogous to
Like.objects.filter(user=request.user).values('status', flat=True)
# instead of the correct
Like.objects.filter(user=request.user).values_list('status', flat=True)
I don't quite understand what do you want to do. If you want to check if the user liked at least one post, you can do it like this:
liked_or_not = Like.objects.filter(user=request.user).exists
If you want to check if a user liked a specific post, you can dot it like this:
liked_or_not = Likes.objects.filter(post_id=post_id, user=request.user).exists()
annotate has a different purpose. It annotates each object in the QuerySet with the provided list of query expressions. An expression may be a simple value, a reference to a field on the model (or any related models), or an aggregate expression (averages, sums, etc.) that has been computed over the objects that are related to the objects in the QuerySet. read more here https://docs.djangoproject.com/en/2.2/ref/models/querysets/#annotate
I encounterd similar problem and solved using .annotate
and below is my definition of Like in models.py
class Like(models.Model):
user = models.ForeignKey(
"users.User", on_delete=models.CASCADE, related_name="likes"
)
writing = models.ForeignKey(
"writings.Writing", on_delete=models.CASCADE, related_name="likes"
)
in views.py
filter_kwargs = {}
filter_kwargs["writing"] = OuterRef("pk")
filter_kwargs["user"] = request_user
subquery = Like.objects.filter(**filter_kwargs)
writing = (
Writing.objects.annotate(user_likes=Exists(subquery))
.get(id=id)
)

Django update view won't save

I have this update view but it will not save upon form submission. 5 minutes ago everything was working fine and then I added the edit feature for user posts and all of a sudden nothing will save when trying to edit things.
users app views:
class UserEditProfileView(LoginRequiredMixin,UpdateView):
login_url = '/login/'
model = UserProfile
fields = [
'first_name',
'profile_pic',
'location',
'title',
'user_type',
'website',
'about',
'twitter',
'dribbble',
'github'
]
template_name_suffix = '_edit_form'
def get_success_url(self):
userid = self.kwargs['pk']
return reverse_lazy('users:user_profile',kwargs={'pk': userid})
users app models:
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
first_name = models.CharField(max_length=50,default='User')
join_date = models.DateTimeField(default=timezone.now)
profile_pic = models.ImageField(upload_to='profile_pics',null=True,blank=True)
location = models.CharField(max_length=150)
title = models.CharField(max_length=250)
user_type = models.IntegerField(choices=USER_TYPE_CHOICES,default=1)
website = models.URLField(max_length=100,blank=True)
about = models.TextField(max_length=500,default='about')
twitter = models.CharField(max_length=50,blank=True)
dribbble = models.CharField(max_length=50,blank=True)
github = models.CharField(max_length=50,blank=True)
#receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
#receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.userprofile.save()
def __str__(self):
return self.user.username
user profile_edit_form.html:
{% extends "users/base.html" %}
{% block content %}
<div class="form-title">
<h2 class="form-title-text">Edit Profile</h2>
</div>
<div class="user-forms-base">
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Save" />
</form>
</div>
{% endblock %}
I am having the same issue with updating posts on the home page, however I am assuming the issues are the same and so I'll be able to just replicate this solution over there.
Someone mentioned this in the comments but basically the form was returning in valid so I override form_invalid to print any errors that may have been causing it. This showed that it was sending a string when it was expecting an int at the model level. Once I switched it back to send an int the problem went away and now it works. Thanks guys.

Categories

Resources