A django and async newbie here, trying to improve a simple message-board app. I'm sure you've all seen this problem dozens of times, but I'm unable to find a solution...
Currently, when a user likes a posted message, it refreshes the whole page. I'd like to use simple JavaScript with the fetch API to prevent this, without having to resort to Ajax, as I've never used it. The problem is, I'm very new to the fetch method as well and I'm struggling to find the correct syntax for the url in the fetch request, as it uses the post model's slug field as a parameter. Like so:
urls.py
urlpatterns = [
...
path('post/<slug:slug>/', views.FullPost.as_view(), name='boards_post'),
path('like/<slug:slug>/', views.PostLike.as_view(), name='post_like'),
...
]
models.py
...
class Post(models.Model):
"""
Model for message posts
"""
STATUS = ((0, "Draft"), (1, "Published"))
title = models.CharField(max_length=200, unique=True)
slug = models.SlugField(max_length=200, unique=True)
author = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="board_posts"
)
category = models.ForeignKey(
Category,
on_delete=models.CASCADE,
default="",
related_name="category_posts"
)
created_on = models.DateTimeField(auto_now_add=True)
updated_on = models.DateTimeField(auto_now=True)
content = models.TextField()
post_image = CloudinaryField('image', default='placeholder')
status = models.IntegerField(choices=STATUS, default=0)
likes = models.ManyToManyField(User, related_name="post_likes")
class Meta:
# Orders posts in descending order
ordering = ['-created_on']
def __str__(self):
return self.title
def number_of_likes(self):
return self.likes.count()
def get_absolute_url(self):
"""
Method to tell django how to find the url to any specific
instance of a post when the Post model is instantiated,
(i.e. a new post created). Returns the url generated by the
'boards_post' path from the FullPost class view, with
this model's slug field as a keyword argument. This
effectively acts as a redirect to the full_post.html template.
"""
return reverse('boards_post', kwargs={'slug': self.slug})
...
views.py
...
class FullPost(View):
"""
View for a single post, selected by the user, displaying
comments and likes. The url for each individual post is derived
from the Post model's slug field which is, in turn,
populated by the title.
"""
def get(self, request, slug, *args, **kwargs):
"""
Method to get post object.
"""
queryset = Post.objects.filter(status=1)
post = get_object_or_404(queryset, slug=slug)
comments = post.comments.order_by('created_on')
liked = False
if post.likes.filter(id=self.request.user.id).exists():
liked = True
return render(
request,
"full_post.html",
{
"post": post,
"comments": comments,
"liked": liked,
"comment_form": CommentForm()
},
)
def post(self, request, slug, *args, **kwargs):
"""
Post method for comment form.
"""
queryset = Post.objects.filter(status=1)
post = get_object_or_404(queryset, slug=slug)
comments = post.comments.order_by("-created_on")
liked = False
if post.likes.filter(id=self.request.user.id).exists():
liked = True
comment_form = CommentForm(data=request.POST)
if comment_form.is_valid():
comment_form.instance.name = self.request.user
comment = comment_form.save(commit=False)
comment.post = post
comment.save()
else:
comment_form = CommentForm()
return redirect(self.request.path_info)
...
...
class PostLike(View):
"""
View for liking and unliking posts.
"""
def post(self, request, slug):
"""
Method to toggle liked/unliked state on a particular post.
"""
post = get_object_or_404(Post, slug=slug)
liked = True
if post.likes.filter(id=self.request.user.id).exists():
post.likes.remove(request.user)
liked = False
else:
post.likes.add(request.user)
likes = post.number_of_likes()
# ----- The original return statement is the one commented out below:
# return HttpResponseRedirect(reverse('boards_post', args=[slug]))
return JsonResponse({"likes": likes, "liked": liked})
...
Snippet from full post template
...
<div class="row">
<div class="col-1">
{% if user.is_authenticated %}
<strong>
<form class="d-inline" action="{% url 'post_like' post.slug %}" method="POST">
{% csrf_token %}
{% if liked %}
<!-- I used 'post.id' for the argument passed to the like function here, as I couldn't get 'post.slug' to work -->
<button class="btn-like" type="submit" name="post_id" value="{{ post.slug }}" onclick="like({{ post.id }})">
<i id="like-btn" class="fas fa-thumbs-up"></i>
</button>
<span id="likes-count">{{ post.number_of_likes }}</span>
{% else %}
<button class="btn-like" type="submit" name="post_id" value="{{ post.slug }}" onclick="like({{ post.id }})">
<i id="like-btn" class="far fa-thumbs-up"></i>
</button>
<span id="likes-count">{{ post.number_of_likes }}</span>
{% endif %}
</form>
</strong>
{% else %}
<strong class="text-secondary"><i class="far fa-thumbs-up"></i> <span id="likes-count">{{ post.number_of_likes }}</span></strong>
{% endif %}
</div>
...
All I've got so far in JavaScript is the following...
function like(post_id) {
let likeButton = document.getElementById("like-btn");
let likeCount = document.getElementById("likes-count");
console.log(likeCount.innerText);
console.log(post_id);
// -------- I've no idea what works here! I've tried both of the below
// -------- and several variations.
// -------- Obviously, none work.
// const url = "{% url 'post_like' post.slug %}";
//or const url = `/like/${post.slug}`;
fetch(url, {method: "POST"})
.then(response => response.json())
.then(data => {
console.log(data);
// ------ If I ever get the json data, I'll use it here to manipulate the
// ------ likeButton and likeCount variables in the DOM. Something like:
likeCount.innerHTML = data["likes"];
if (data["liked"] === true) {
likeButton.className = "fas fa-thumbs-up";
} else {
likeButton.className = "far fa-thumbs-up";
}
})
.catch((e) => alert("Unable to like/unlike post."));
// -------- or something.
}
Also, I know I need to handle the csrf token somehow but I've no idea how. Like I said, total newbie.
So, can anyone help? And does anyone have any advice?
Cheers!
For csrf_token problem add to view this decorator:
from django.utils.decorators import method_decorator
#method_decorator(csrf_exempt, name='dispatch')
class PostLike(View):
...
For fetch you need to have whole path, not just relative path. This should work:
url = "{{ request.scheme }}://{{ request.get_host }}{% url 'post_like' post.slug %}"
Related
I am trying to create a website whose structure is something like this:
A main page with various links of similar kind (like a blog). User can click on any on those and go inside any of them (link will be like site.com/slug). Then again the same thing (this time the link will be like site.com/slug/slug).
individual_question.html
<div id="argument_container">
{% for argument in argument%}
<h6 id='{{ argument.argument_faction }}' style="display:none">{{ argument.argument_text }}
<br><br>
<button type="button" class="btn btn-outline-success btn-sm"><i class="fa fa-thumbs-o-down" style="padding-right: 3px;"></i>Agree</button>
<button type="button" class="btn btn-outline-danger btn-sm"><i class="fa fa-thumbs-o-up" style="padding-right: 3px;"></i>Disagree and debate</button>
</h6>
{% endfor %}
</div>
urls.py
urlpatterns = [
path('admin/', admin.site.urls),
path('', home_screen_view, name="home"),
path('register/', registration_view, name="register"),
path('logout/', logout_view, name="logout"),
path('login/', login_view, name="login"),
path('account/', account_view, name="account"),
path('<slug:question>', individual_question_view, name="individual_question_view"),
# for testing
path('<slug:argument>', individual_argument_view, name="individual_argument_view"),
models.py
class Faction(models.Model):
faction_id = models.BigAutoField(primary_key=True)
faction_question = models.ForeignKey(Question, on_delete=models.CASCADE)
faction_name = models.CharField(max_length=30)
def __str__(self):
return self.faction_name
class FactionArgument(models.Model):
argument_id = models.BigAutoField(primary_key=True)
argument_faction = models.ForeignKey(Faction, on_delete=models.CASCADE)
argument_question = models.ForeignKey(Question, on_delete=models.CASCADE, null=True)
argument_slug = models.SlugField(max_length=80, null=True)
argument_text = models.TextField()
user_id = models.ForeignKey(Account, on_delete=models.CASCADE)
datetime = models.DateTimeField(auto_now=True)
def get_absolute_url(self):
return reverse('individual_argument_view', args=[self.argument_slug]) # chances of error
def __str__(self):
return self.argument_text
views.py
def individual_question_view(request, question):
context={}
question = get_object_or_404(Question, question_slug=question)
context["question"] = question
factions = Faction.objects.filter(faction_question = Question.objects.filter(question = question.question).first())
context["faction"] = factions
arguments = FactionArgument.objects.filter(argument_question = Question.objects.filter(question = question.question).first())
context["argument"] = arguments
return render(request, "snippets/individual_question.html", context)
# for testing
def individual_argument_view(request, argument):
return render(request, "snippets/individual_chain.html")
I am able to get to the (site.com/slug) but not able to figure out the url dispatching for next step (site.com/slug/slug).
Currently it shows this error:
Page not found (404) Request Method: GET Request URL: http://127.0.0.1:8000/question_1/blue2 Using the URLconf defined in Version2.urls, Django tried these URL patterns, in this order:
admin/ [name='home'] register/ [name='register'] logout/ [name='logout'] login/ [name='login'] account/ [name='account'] <slug:question> [name='individual_question_view'] <slug:argument> [name='individual_argument_view'] password_change/done/ [name='password_change_done'] password_change/ [name='password_change'] password_reset/done/ [name='password_reset_done'] reset/<uidb64>/<token>/ [name='password_reset_confirm'] password_reset/ [name='password_reset'] reset/done/ [name='password_reset_complete'] The current path, question_1/blue2, didn’t match any of these.
You need to modify the endpoint signature in the urls.py file.
Right now it reads
path('<slug:question>', individual_question_view, name="individual_question_view"),
but this covers only one slug.
path('<slug:question>/<slug:param2>', individual_question_view, name="individual_question_view"),
This will pass 2 params to the view.
Next, the href param in the template does not work as is due to concatenation of two separate urls instead of one with two params:
href=" {{question.get_absolute_url}}{{argument.get_absolute_url}} "
it should be instead:
href="{% url 'individual_question_view' question.question_slug argument.argument_slug %}"
As a side note, the urls.py posted has a duplicate signature, in effect the second one will not do anything because the first one will catch it and stop searching:
path('<slug:question>', individual_question_view, name="individual_question_view"),
# for testing
path('<slug:argument>', individual_argument_view, name="individual_argument_view"),
Updated: follow-up issue discussed in answer comments.
I'm using Django 2.2 and PostgreSql. I'm trying to create a simple app that I want to follow neighboring users. 'Follow' button will increase the number of followed, 'Unfollow' button will decrease the number of followed. However, the 'Follow' button does not work. How can I solve this problem?
following/models.py
class Following(models.Model):
follower = models.ForeignKey(User, on_delete=models.CASCADE, related_name='fallower', null=True)
followed = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='fallowing')
following/views.py
def user_follow_unfollow(request):
response = sub_user_follow_unfollow(request)
data = response.get('data')
followed = response.get('followed')
numbers_followed_and_follower= Following.user_followed_and_follower(followed)
context = {'user': followed, 'followers': numbers_followed_and_follower['followers'],
'followeds': numbers_followed_and_follower['followeds']}
html = render_to_string('following/following_partion.html', context=context, request=request)
data.update({'html': html})
return JsonResponse(data=data)
def sub_user_follow_unfollow(request):
if not request.is_ajax():
return HttpResponseBadRequest()
data = {'follow status': True, 'html': '', 'is_valid': True, 'msg': '<b>Unfollow</b>'}
follower_username = request.GET.get('follower_username', None)
followed_username = request.GET.get('followed_username', None)
follower = get_object_or_404(User, username=follower_username)
followed = get_object_or_404(User, username=followed_username)
does_follow_the_user= Following.user_does_follow_the_user(follower=follower, followed=followed)
if not does_follow_the_user:
Following.user_follow(follower=follower, followed=followed)
else:
Following.user_unfollow(followed=followed, follower=follower)
data.update({'msg': '<b>Follow</b>', 'follow_status': False})
return {'data': data, 'followed': followed}
templates.following_partion.html
{% if request.neighbor_detail != user %}
<div>
<button followed='{{ neighbor_detail.username }}' followed='{{ request.neighbor_detail.username }}'
url="{% url 'following:user_follow_and_unfollow' %}" id="follow_unfollow_button"
class="btn btn-lg btn-success">
{% if does_follow_the_user%}
<b>Unfollow</b>
{% else %}
<b>Follow</b>
{% endif %}
</button>
</div>
{% endif %}
<div class="followers col-lg-offset-3 col-md-3 col-md-offset-3 col-lg-3 text-center">
<span><b>followers</b></span>
<button url="{% url 'following:fallowed-or-fallowers-list' 'fallowers' %}" follow_type="followers"
username="{{ neighbor_detail.username }}" class="follow_button btn-block btn btn-primary">
{{ followers}}
</button>
<div class="followeds col-lg-3 col-md-3 text-center">
<span><b>Followeds</b></span>
<button url="{% url 'following:followed-or-followers-list' 'followed' %}" follow_type="followed"
username="{{ neighbor_detail.username }}" class="follow_button btn-block btn btn-success">
{{ followeds}}
</button>
my script
<script>
$("#follow_unfollow_button").click(function () {
var $this = $(this);
var $url = $this.attr('url');
var $takip_eden = $this.attr('follower');
var $takip_edilen = $this.attr('followed');
var data = {follower_username: $follower, followed_username: $followed};
$.ajax({
url: $url,
type: 'GET',
dataType: 'json',
data: data,
success: function (data) {
if (data.is_valid) {
$this.html(data.msg);
$("#user_following").html(data.html)
}
}
})
});
</script>
We cannot help you as there's too many missing pieces. However, this should not be too difficult to debug, here are some things you should inspect:
In your browser developer tools, inspect the HTTP request (XHR request). What parameters are being sent?
In your python debugger, inspect the values of request.GET, data, numbers_followed_and_follower. If you don't know these, it's impossible to tell what's wrong.
If it's still not clear what's wrong, in your python IDE, set a breakpoint in your view and step through your code line by line, inspecting the results. I cannot stress how important it is to use an IDE with a proper debugger.
In your browser developer tools, inspect the HTTP response to your AJAX request. What is being returned?
You need to analyse step-by-step your various variables to understand where's the issue.
Suggestions
You should really use POST (not GET) in your AJAX call, since you're modifying the database.
Use a ManyToManyField to register followers. They are just a relationship between two users:
follows = models.ManyToManyField("self", symmetrical=False, related_name="followers")
By specifying symmetrical=False you tell Django to distinguish the two directions of the relationship. In order to create or remove the relationship in your view, and to view relationships, you can just:
user.follows.add(other_user) # user follows other_user
user.follows.remove(other_user) # user unfollows other_user
user.followers.all() # people following
user.followers.count() # number of followers
i'm quite new using django and i've been stuck in this problem for several days.
I have a form.Form in a bootstrap modal on my template with only 1 field (email_field) and basically i need to submit that form via ajax, check if that email address is registered in the database, then send an invitation to that email and close the modal. if the email is not registered show the form errors without closing the modal. I've tried with different examples but can find the solution either because the examples don't handle errors or the form is not inside a modal or not using class based views
.
I'm having 2 issues with my code:
Not sure what to return in my view if the form is valid or invalid and how to handle errors in my js code to show them on the modal.(return tha form to render the errors or a JSON response??).
After the first success submission is made the form cannot be used again.(The size of the submit button changes and if you click it return a error : CSRF token missing or incorrect)
Form.py
class CollaboratorForm(forms.Form):
email_address = forms.EmailField(required=True,widget=forms.TextInput(attrs={'class': 'form-control focus-text-box', 'type': 'email',
'placeholder': 'Enter email'}))
def clean_email_address(self):
email = self.cleaned_data['email_address']
if not User.objects.filter(email=email):
raise forms.ValidationError('This user is not registered')
return email
def sendEmail(self, datas):
message = "Hello, " + datas['user_name']+" "+ datas['email_from'] + " invited you to collaborate in an existing project. Follow this link if you are interested " + datas['invitation_link']
msg = EmailMessage('Invitation from ' + datas['user_name'],
message, to=[datas['email_to']])
msg.send()
Template.html (project_detail.html)
<script src="{% static '/experiments/js/invite_collaborator.js' %}"></script>
<div class="bootstrap-modal modal fade in" id="collaboratorModal" style="display: none;">
<div class="modal-body">
<form action="{% url 'experiments:invite-collaborator' project_id=project.id %}" method="post" id=collaborator-form >
{% csrf_token %}
<div class="form-group">
{% if collaborator_form.errors %}
<ol>
{% for error in collaborator_form.errors %}
<li><strong>{{ error|escape }}</strong></li>
{% endfor %}
</ol>
{% endif %}
<label class="control-label">Invite someone by email</label>
<div class="input-group mt10">
{{ collaborator_form }}
<span class="input-group-btn">
<input name="collaborator-commit" onClick="invite({{project.id}});" class="btn btn-primary" data-disable-with="Send Invitation" id="invite-button" type="submit">
</span>
</div>
</div>
</form>
</div>
</div>
Url.py
urlpatterns = [
url(r'^(?P<project_id>[0-9]+)/invite_collaborator$', views.InviteCollaborator.as_view(), name='invite-collaborator'),
]
View.py
class ProjectDetail(DetailView):
model = Project
template_name = 'experiments/project_detail.html'
pk_url_kwarg = 'project_id'
def get_context_data(self, **kwargs):
context = super(ProjectDetail, self).get_context_data()
project = get_object_or_404(Project,pk=self.kwargs["project_id"])
context["project"] = project
context["collaborator_form"] = CollaboratorForm()
return context
class InviteCollaborator(FormView):
form_class = CollaboratorForm
template_name = 'experiments/project_detail.html'
def post(self, request, *args, **kwargs):
collaborator_form = CollaboratorForm(data=request.POST)
project_id = request.POST['project_id']
current_project = Project.objects.get(id=project_id)
datas={}
if collaborator_form.is_valid():
cleaned_data = collaborator_form.cleaned_data
email_address = cleaned_data.get('email_address')
user = User.objects.get(pk=request.user.id)
invitation_link = "http://exp.innovationhackinglab.com/projects/"+ str(current_project.id) + "/join/" + current_project.invitation_key
datas['user_name'] = user.first_name + ' ' + user.last_name
datas['email_from'] = user.email
datas['email_to'] = email_address
datas['invitation_link'] = invitation_link
collaborator_form.sendEmail(datas)
data = simplejson.dumps("Success")
return HttpResponse(data, content_type='application/json')
else:
return super(InviteCollaborator, self).form_invalid(collaborator_form)
invite_collaborator.js
function invite(project_id) {
$('#collaborator-form').submit(function(e) {
e.preventDefault();
$.ajax({
data: $(this).serialize()+'&'+$.param({ 'project_id': project_id }),
type: $(this).attr('method'),
url: $(this).attr('action'),
});
$('#collaboratorModal').modal('toggle');
$('#collaboratorModal').on('hidden.bs.modal', function () {
$(this).find("input,textarea,select").val('').end();
});
});
};
I've read about using success: & error: on the js file but don't know how to use it without the appropriate "return" in the view
You need to have two ajax methods, one to get the form (as raw html) and one to post the form. You will have a corresponding get and post method in your view too.
get function of your view class:
def get(self, request, *args, **kwargs):
form = CollaboratorForm()
return render(request,'template.html',{'form':form})
def post(self, request, *args, **kwargs):
form = CollaboratorForm(request.POST)
if form.is_valid():
//save form
//return whatever you want to show on successful form submission
else:
//return bound form as html with errors
return render(request,'template.html',{'form':form})
js functions
have two seperate ajax function one for get (showing form) one for post(submitting form)
If you want to use templates on server's side, with FormView and ajax, I would suggest splitting templates into two parts - wrapper and form, load only wrapper via TemplateView, then fetch form with ajax. That allows you to send form with ajax and put responses (like form with errors) in wrapper.
Change your HTML template - take modal body's to another file, ex.:
project_detail.html
<script src="{% static '/experiments/js/invite_collaborator.js' %}"></script>
<div class="bootstrap-modal modal fade in" id="collaboratorModal" style="display: none;">
<div class="modal-body" id="collaboratorModalContent">
</div>
</div>
project_detail_content.html
<form action="{% url 'experiments:invite-collaborator' project_id=project.id %}" method="post" id=collaborator-form >
{% csrf_token %}
<div class="form-group">
{% if collaborator_form.errors %}
<ol>
{% for error in collaborator_form.errors %}
<li><strong>{{ error|escape }}</strong></li>
{% endfor %}
</ol>
{% endif %}
<label class="control-label">Invite someone by email</label>
<div class="input-group mt10">
{{ collaborator_form }}
<span class="input-group-btn">
<input name="collaborator-commit" onClick="invite({{project.id}});" class="btn btn-primary" data-disable-with="Send Invitation" id="invite-button" type="submit">
</span>
</div>
</div>
</form>
FormView should handle GET and POST - first one to get the form in project_detail_content.html into modal, second for sending email. Fortunately, FormView can do all that for us! (I don't know from where you get that project variable though)
View.py
class InviteCollaborator(FormView):
form_class = CollaboratorForm
template_name = 'experiments/project_detail_content.html'
def form_valid(self, form):
# This method is called when valid form data has been POSTed.
# It should return an HttpResponse.
project_id = self.request.POST['project_id']
current_project = Project.objects.get(id=project_id)
datas={}
cleaned_data = form.cleaned_data
email_address = cleaned_data.get('email_address')
user = User.objects.get(pk=request.user.id)
invitation_link = "http://exp.innovationhackinglab.com/projects/"+ str(current_project.id) + "/join/" + current_project.invitation_key
datas['user_name'] = user.first_name + ' ' + user.last_name
datas['email_from'] = user.email
datas['email_to'] = email_address
datas['invitation_link'] = invitation_link
form.sendEmail(datas)
data = simplejson.dumps("Success")
return HttpResponse(data, content_type='application/json')
Note few things - we use FormView, so for GET request it will return content of project_detail_content.html with CollaboratorForm, and on POST, same template with form and errors if form is invalid, or JSON with Success message otherwise.
What happened to project_detail.html? We will use TemplateView to create thw wrapper:
Url.py
urlpatterns = [
url(r'^invite_collaborator$', TemplateView.as_view(template_name="project_detail.html")),
url(r'^(?P<project_id>[0-9]+)/invite_collaborator/form$', views.InviteCollaborator.as_view(), name='invite-collaborator'),
]
Finally, JS
invite_collaborator.js
// In JS you need to make sure you fetch form from /project_id/invite_collaborator/form each time you show modal
$(document).ready(function(e) {
$('#collaboratorModalContent').load('invite_collaborator');
});
// Then, on submit we simply send data and handle response with success and error.
// With our current View, invalid form will generate successful response with form and error, so we need to check
function invite(project_id) {
$('#collaborator-form').submit(function(e) {
e.preventDefault();
$.ajax({
type: $(this).attr('method'),
url: $(this).attr('action'),
data: $(this).serialize()+'&'+$.param({ 'project_id': project_id }),
success: function ( response, status, xhr, dataType ) {
if( dataType === 'json' ){
//Make sure response is 'Success' and close modal
$('#collaboratorModal').modal('toggle');
$('#collaboratorModal').on('hidden.bs.modal', function () {
$(this).find("input,textarea,select").val('').end();
});
});
};
}
else {
// It's not JSON, it must be HttpResposne with forms and errors, so it goes into modal's body
$('#collaboratorModalContent').html(response)
}
}
});
I still don't know where and how you get/set you project variable, so maybe TemplateView is bad choice...
I have successfully created a like button which works, and if i like a post the counter is incremented and it doesn't show the unlike button immediately, I have to refresh the page for it display and even after refreshing it says like, instead of unlike but if I click on it it reduces the count i.e unlikes it.
So how to make dislike button display immediately after liking it and vice versa? Thank you.
Here's the code:
models.py
class Post(models.Model):
author = models.ForeignKey('auth.User')
title = models.CharField(max_length=200)
text = models.TextField()
created_date = models.DateTimeField(default=timezone.now)
published_date = models.DateTimeField(blank=True,null=True)
likes = models.IntegerField(default=0)
def publish(self):
self.published_date = timezone.now()
self.save()
def __str__(self):
return self.title
views.py
def like_post(request):
liked = False
if request.method == 'GET':
post_id = request.GET['post_id']
post = Post.objects.get(id=int(post_id))
if request.session.get('has_liked_'+post_id, liked):
print("unlike")
if post.likes > 0:
likes = post.likes - 1
try:
del request.session['has_liked_'+post_id]
except KeyError:
print("keyerror")
else:
print("like")
request.session['has_liked_'+post_id] = True
likes = post.likes + 1
post.likes = likes
post.save()
return HttpResponse(likes, liked)
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk)
Post.objects.get(pk=pk)
post_id = post.pk
liked = False
if request.session.get('has_liked_' + str(post_id), liked):
liked = True
print("liked {}_{}".format(liked, post_id))
context = {'post': post, 'liked': liked}
return render(request, 'blog/post_detail.html', {'post': post})
urls.py
url(r'like_post/$', views.like_post, name='like_post'),
ajax-blog.js
$('#likes').click(function(){
var postid;
postid= $(this).attr("data-post_id");
$.get('/blog/like_post/', {post_id: postid}, function(data){
$('#like_count').html(data);
$('#likes').hide();
});
});
post_detail.html
<p>
<strong id="like_count">{{ post.likes }}</strong> people like this category
{% if user.is_authenticated %}
<button id="likes" data-post_id="{{post.id}}" class="btn btn-primary" type="button">
{% if liked %}
<span class="glyphicon glyphicon-thumbs-down"></span>
Unlike
</button>
{% else %}
<span class="glyphicon glyphicon-thumbs-up"></span>
Like
</button>
{% endif %}
{% endif %}
</p>
$('#likes').click(function(){
$('.like_span').toggleClass('glyphicon-thumbs-down glyphicon-thumbs-up');
var postid;
postid= $(this).attr("data-post_id");
$.get('/blog/like_post/', {post_id: postid, liked:$('.like_span').hasClass('glyphicon-thumbs-up')}, function(data){
$('#like_count').html(data);
$('#likes').hide();
});
});
Then add class like_span
<span class="like_span glyphicon glyphicon-thumbs-up"></span>
<span class="like_span glyphicon glyphicon-thumbs-down"></span>
Then in views you will get request.GET['liked'] as true or false.Treat that value for increase and decrease
One of the good ways to handle is not to refresh the page when user click on like or dislike.
Instead, use jquery ajax with rest api(DRF) to increase and decrease the like count.
Use jquery to switch the icons instead of django template tag condition {% if liked %}
I have a form that uses multiple formsets. The formset forms are added dynamically via JS. I have been looking at a few different places to help myself along.
Add a dynamic form to a django formset using javascript in a right way
And
A nice post by Kevin Dias - Django class-based views with multiple inline formsets
The problem I am having is that when I post my data the outer form has data, but non of my formsets actually have any data in the cleaned_data dictionary when I start looping through them. Any thoughts on what I may be missing? The second formset is added with a very similar JS method.
Forms
class ShippingForm(Form):
is_partial = BooleanField(label='Partial?')
class ShippingActualProduct(Form):
box_no = CharField(label='Box Number', max_length=3)
upc = CharField(
widget=forms.TextInput(attrs={'class':'upcAjax'}),
)
serial_no = CharField(
label='Serial Number',
widget=forms.TextInput(attrs={'class':'serial'}),
)
sku = CharField(
widget=forms.TextInput(attrs={'class':'skuAjax'}),
)
description=CharField(label='Description')
on_hand=CharField(label='On Hand')
def __init__(self, *args, **kwargs):
super(ShippingActualProduct,self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_show_labels = True
self.helper.form_class = 'form-inline'
class ShippingNonInventoryProduct(Form):
non_box_no = CharField(label='Box Number', max_length=3)
quantity = IntegerField()
description = CharField()
serial_no = CharField()
def __init__(self, *args, **kwargs):
super(ShippingNonInventoryProduct,self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_show_labels = True
self.helper.form_class = 'form-inline'
ActualProductFormSet = formset_factory(ShippingActualProduct, extra=1, can_delete=True)
NonInventoryProductFormSet = formset_factory(ShippingNonInventoryProduct, extra=1, can_delete=True)
Views
class ShippingCreate(FormView):
template_name = 'jinja2/Shipping/shipping_create.html'
form_class = ShippingForm
success_url = reverse_lazy('shipping_create')
def get_context_data(self, **kwargs):
context = super(ShippingCreate, self).get_context_data(**kwargs)
input_invoice_no = self.request.GET['invoice_no']
try:
self.object = Invoice.objects.get(invoice_no = input_invoice_no)
context['invoice'] = self.object
except Invoice.DoesNotExist:
messages.error(self.request, 'We were unable to find invoice number %s, please try again' % input_invoice_no)
try:
context['voucher'] = Voucher.objects.get(voucher_no = self.object.voucher_no)
except Voucher.DoesNotExist:
messages.error(self.request, 'We were unable to find an installation voucher for claim number %s' % self.object.voucher_no)
context['actual_items_forms'] = ActualProductFormSet(prefix='actual')
context['non_inventory_items_forms'] = NonInventoryProductFormSet(prefix='non')
context['form'] = ShippingForm()
return context
def get(self, request, *args, **kwargs):
self.object = None
context = self.get_context_data()
return render(request, self.template_name, context)
def post(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
actual_product = ActualProductFormSet(self.request.POST, prefix='actual')
non_inv_product = NonInventoryProductFormSet(self.request.POST, prefix='non')
if actual_product.is_valid():
for product in actual_product:
data = product.cleaned_data
sku = data.get('sku')
context={}
return render(request, self.template_name, context)
Template
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% load static from staticfiles %}
{% load socialaccount %}
{% load sitetree %}
{% block headcss %}
{{ block.super }}
<link rel="stylesheet" href="{% static "css/shipping.css" %}">
{% endblock headcss %}
{% block headjs %}
{{ block.super }}
<script src="{% static "js/shipping.js" %}"></script>
{% endblock headjs %}
{% block content %}
{% block messages %}
{{ block.super }}
{% endblock messages %}
<div class="container-fluid">
<form action="." method="post">
<div>
<h3>Item information:</h3>
{% csrf_token %}
{{ actual_items_forms.management_form }}
<div id="actual-items-form-container">
</div>
Add Item
</div>
<div>
<h3>Non Inventory Items Shipped:</h3>
{{ non_inventory_items_forms.management_form }}
<div id="non-inv-items-form-container">
</div>
Add Item
</div>
{{ form.as_p }}
<input type="submit" value="Complete" class="submit btn btn-success" />
</form>
</div>
{% include "jinja2/hub/loading_modal.html" %}
{% endblock content %}
</html>
The JavaScript
function getNewActualItemForm() {
// unbind this ajax call from the overlay displayed when looking data up.
$(document).unbind(".items");
var count = $('#actual-items-form-container').children().length;
$.get("/forms/actualitemform",function(data, status){
var form = data.replace(/__prefix__/g, count);
// Get the html contents of the form, all together to iterate over.
var htmlForm = $('<form>').html(form).contents();
// Just grab the children of that form
var rows = htmlForm.children();
// loop through the inputs locating the DELETE input and label.
$(rows).each( function(index, value) {
var row = $(this);
var del = $(row).find('input:checkbox[id $= "-DELETE"]');
// Only move forward if we have found the DELETE input
if (del.length){
//Write the form ot the Dom so the search for the label will succeed.
$('div#actual-items-form-container').append(form);
var label ='label[for="id_form-' + count + '-DELETE"]';
$(label).hide();
}
});
// update form count
$('#id_actual-TOTAL_FORMS').attr('value', count+1);
// some animate to scroll to view our new form
$('html, body').animate({
scrollTop: $('#actual-item-btn').position().top-200
}, 800);
// get the max box number.
var maxBoxNo = getBoxNo();
// Set focus to the next UPC
var boxID = '#id_form-var-box_no';
var upcID = '#id_form-var-upc';
var nextBox = boxID.replace('var',count);
var nextUpc = upcID.replace('var',count);
// set the box number for the new line.
$(nextBox).val(maxBoxNo);
$(nextUpc).focus();
});
return count;
}
There were two issues that were causing the troubles here.
I had the crispy forms helper rendering the form tags for each formset. This is a bad idea. Setting form_tag = False fixed that.
I had forgotten to set the prefix argument on my formsets in the views I created to grab the next form via JavaScript.
Once both of these were implemented the data was now available from the forms on submission.