diff --git a/app/migrations/0021_dislikecomment_likecomment.py b/app/migrations/0021_dislikecomment_likecomment.py new file mode 100644 index 0000000000000000000000000000000000000000..1ce9111268da53f8e2a88fdf877c4250fb354695 --- /dev/null +++ b/app/migrations/0021_dislikecomment_likecomment.py @@ -0,0 +1,33 @@ +# Generated by Django 3.1 on 2020-10-09 16:19 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0020_merge_20201009_2039'), + ] + + operations = [ + migrations.CreateModel( + name='LikeComment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), + ('session_id', models.CharField(max_length=32)), + ('comment', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='app.comment')), + ], + ), + migrations.CreateModel( + name='DislikeComment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), + ('session_id', models.CharField(max_length=32)), + ('comment', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='app.comment')), + ], + ), + ] diff --git a/app/models.py b/app/models.py index aa54208425b22632be3f49ce1214851651e4359d..7edbd131100bd12ee005e2f4c1da3440a32a6fb8 100644 --- a/app/models.py +++ b/app/models.py @@ -108,6 +108,28 @@ class Comment(models.Model): def __str__(self): return self.username + @property + def like_count(self): + count = LikeComment.objects.filter(comment=self).count() + return count + + @property + def dislike_count(self): + count = DislikeComment.objects.filter(comment=self).count() + return count + + +class LikeComment(models.Model): + comment = models.ForeignKey(Comment, models.SET_NULL, null=True) + timestamp = models.DateTimeField(default=timezone.now) + session_id = models.CharField(max_length=32, blank=False) + + +class DislikeComment(models.Model): + comment = models.ForeignKey(Comment, models.SET_NULL, null=True) + timestamp = models.DateTimeField(default=timezone.now) + session_id = models.CharField(max_length=32, blank=False) + class Like(models.Model): materi = models.ForeignKey(Materi, models.SET_NULL, null=True) diff --git a/app/templates/app/detail_materi.html b/app/templates/app/detail_materi.html index 191fe2d5609640b111399aadae506fb5b5a4cda8..ccf5f3d89474fb00db7f07edefc485127f559bb6 100644 --- a/app/templates/app/detail_materi.html +++ b/app/templates/app/detail_materi.html @@ -288,11 +288,35 @@ {% else %} <span style="background-color: #{{comment.profile}}" class="profile p-1 bd-highligh"></span> {% endif %} - <p class="p-1 bd-highligh m-0"><strong>{{comment.user.name}}</strong></p> - <p class="p-1 bd-highligh m-0">•</p> - <p class="timestamp p-1 bd-highligh m-0 text-muted"> - {{ comment.timestamp|naturaltime }} - </p> + <div class="d-flex flex-row justify-content-end"> + <p class="p-1 bd-highligh m-0"><strong>{{comment.user.name}}</strong></p> + <p class="p-1 bd-highligh m-0">•</p> + <p class="timestamp p-1 bd-highligh m-0 text-muted"> + {{ comment.timestamp|naturaltime }} + </p> + <div> + <button id="thumb-like-comment-{{ comment.id }}" class="btn btn-link btn-book shadow-sm p-2 mr-2 bg-white rounded" onClick="postLikeComment({{ comment.id }})"> + <div class="d-flex flex-row"> + {% if has_liked.comment.id %} + <i id="thumb-like-comment-icon-{{ comment.id }}" aria-hidden="true" class="fas fa-thumbs-up"></i> + {% else %} + <i id="thumb-like-comment-icon-{{ comment.id }}" aria-hidden="true" class="far fa-thumbs-up"></i> + {% endif %} + <div id="like-comment-{{ comment.id }}">{{ comment.like_count }}</div> + </div + </button> + <button id="thumb-dislike-comment-{{ comment.id }}" class="btn btn-link btn-book shadow-sm p-2 mr-2 bg-white rounded" onClick="postDislikeComment({{ comment.id }})"> + <div class="d-flex flex-row"> + {% if has_disliked.comment.id %} + <i id="thumb-dislike-comment-icon-{{ comment.id }}" aria-hidden="true" class="fas fa-thumbs-down"></i> + {% else %} + <i id="thumb-dislike-comment-icon-{{ comment.id }}" aria-hidden="true" class="far fa-thumbs-down"></i> + {% endif %} + <div id="dislike-comment-{{ comment.id }}">{{ comment.dislike_count }}</div> + </div> + </button> + </div> + </div> {% if user.is_admin %} <a class="ml-auto p-1 bd-highlight close" href="{% url 'delete-comment' materi_data.id comment.id %}"> @@ -426,6 +450,46 @@ }); }); + function postLikeComment(comment_id) { + $.ajaxSetup({ + beforeSend: function (xhr, settings) { + if (!csrfSafeMethod(settings.type) && !this.crossDomain) { + xhr.setRequestHeader("X-CSRFToken", csrftoken); + } + } + }); + $.ajax({ + type: 'POST', + url: "{% url 'comment-like-toggle' %}", + data: { + 'comment_id': comment_id, + 'session_id': "{{ session_id }}" + }, + success: likeComment, + dataType: 'html' + }); + } + + function postDislikeComment(comment_id) { + $.ajaxSetup({ + beforeSend: function (xhr, settings) { + if (!csrfSafeMethod(settings.type) && !this.crossDomain) { + xhr.setRequestHeader("X-CSRFToken", csrftoken); + } + } + }); + $.ajax({ + type: 'POST', + url: "{% url 'comment-dislike-toggle' %}", + data: { + 'comment_id': comment_id, + 'session_id': "{{ session_id }}" + }, + success: dislikeComment, + dataType: 'html' + }); + } + function postAddRating(rating_score) { $.ajaxSetup({ beforeSend: function (xhr, settings) { @@ -463,6 +527,32 @@ document.getElementById("thumb").firstChild.data = " Disukai" } } + + function likeComment(data, jqXHR) { + var data = $.parseJSON(data) + likeIcon = $('#thumb-like-comment-icon-' + data['comment_id']) + likeValue = $('#like-comment-' + data['comment_id']) + if (data['liked']) { + likeIcon.removeClass("fas fa-thumbs-up").addClass('far fa-thumbs-up') + likeValue.text(parseInt(likeValue.text())-1) + } else { + likeIcon.removeClass("far fa-thumbs-up").addClass('fas fa-thumbs-up') + likeValue.text(parseInt(likeValue.text())+1) + } + } + + function dislikeComment(data, jqXHR) { + var data = $.parseJSON(data) + dislikeIcon = $('#thumb-dislike-comment-icon-' + data['comment_id']) + dislikeValue = $('#dislike-comment-' + data['comment_id']) + if (data['disliked']) { + dislikeIcon.removeClass("fas fa-thumbs-down").addClass('far fa-thumbs-down') + dislikeValue.text(parseInt(dislikeValue.text())-1) + } else { + dislikeIcon.removeClass("far fa-thumbs-down").addClass('fas fa-thumbs-down') + dislikeValue.text(parseInt(dislikeValue.text())+1) + } + } function getCitation(text){ var $temp = $("<input>"); diff --git a/app/tests.py b/app/tests.py index b0bd85ccad56f05f799e14205e34a2e133bb5730..731ce8fccee78e00aaefb5830bae2f831f331ad3 100644 --- a/app/tests.py +++ b/app/tests.py @@ -28,9 +28,11 @@ from digipus.settings import TIME_ZONE from .models import ( Category, Comment, + DislikeComment, DownloadStatistics, Materi, Like, + LikeComment, Rating, ReqMaterial, RatingContributor, @@ -363,6 +365,96 @@ class DetailMateriTest(TestCase): response = self.client.get(url) self.assertContains(response, "Anonymous") + def test_comment_disliked_by_anonymous(self): + url_materi = self.url + self.client.get("/logout/") + self.client.login(**self.anonymous_credential) + + self.client.post(url_materi, {"comment": "This is new comment by Anonymous"}) + comment = Comment.objects.get(comment="This is new comment by Anonymous").id + response = self.client.get(url_materi) + session_id = response.context["session_id"] + + payload = {"comment": comment, "session_id": session_id} + ajax_response = self.client.post("/comment/dislike/", payload) + num_of_comment_dislikes = DislikeComment.objects.filter(comment=comment).count() + self.assertEqual(num_of_comment_dislikes, 0) + + def test_comment_liked_by_anonymous(self): + url_materi = self.url + self.client.get("/logout/") + self.client.login(**self.anonymous_credential) + + self.client.post(url_materi, {"comment": "This is new comment by Anonymous"}) + comment = Comment.objects.get(comment="This is new comment by Anonymous").id + response = self.client.get(url_materi) + session_id = response.context["session_id"] + + payload = {"comment": comment, "session_id": session_id} + ajax_response = self.client.post("/comment/like/", payload) + num_of_comment_likes = LikeComment.objects.filter(comment=comment).count() + self.assertEqual(num_of_comment_likes, 0) + + def test_comment_undisliked_by_anonymous(self): + url_materi = self.url + self.client.get("/logout/") + self.client.login(**self.anonymous_credential) + + self.client.post(url_materi, {"comment": "This is new comment by Anonymous"}) + comment = Comment.objects.get(comment="This is new comment by Anonymous") + response = self.client.get(url_materi) + session_id = response.context["session_id"] + + payload = {"comment": comment, "session_id": session_id} + ajax_response = self.client.post("/comment/dislike/", payload) + + ajax_response = self.client.post("/comment/dislike/", payload) + num_of_comment_dislikes = DislikeComment.objects.filter(comment=comment, session_id=session_id).count() + self.assertEqual(num_of_comment_dislikes, 0) + + def test_comment_unliked_by_anonymous(self): + url_materi = self.url + self.client.get("/logout/") + self.client.login(**self.anonymous_credential) + + self.client.post(url_materi, {"comment": "This is new comment by Anonymous"}) + comment = Comment.objects.get(comment="This is new comment by Anonymous") + response = self.client.get(url_materi) + session_id = response.context["session_id"] + + payload = {"comment": comment, "session_id": session_id} + ajax_response = self.client.post("/comment/like/", payload) + + ajax_response = self.client.post("/comment/like/", payload) + num_of_comment_likes = LikeComment.objects.filter(comment=comment, session_id=session_id).count() + self.assertEqual(num_of_comment_likes, 0) + + def test_comment_new_does_not_have_dislike(self): + url_materi = self.url + self.client.get("/logout/") + self.client.login(**self.anonymous_credential) + + response = self.client.get(url_materi) + session_id = response.context["session_id"] + self.client.post(url_materi, {"comment": "This is new comment by Anonymous"}) + + comment = Comment.objects.get(comment="This is new comment by Anonymous") + comment_dislike_counter = DislikeComment.objects.filter(comment=comment, session_id=session_id).count() + self.assertEqual(comment_dislike_counter, 0) + + def test_comment_new_does_not_have_like(self): + url_materi = self.url + self.client.get("/logout/") + self.client.login(**self.anonymous_credential) + + response = self.client.get(url_materi) + session_id = response.context["session_id"] + self.client.post(url_materi, {"comment": "This is new comment by Anonymous"}) + + comment = Comment.objects.get(comment="This is new comment by Anonymous") + comment_like_counter = LikeComment.objects.filter(comment=comment, session_id=session_id).count() + self.assertEqual(comment_like_counter, 0) + def test_detail_materi_contains_form_comment(self): self.client.login(**self.contributor_credential) response = self.client.get(self.url) diff --git a/app/urls.py b/app/urls.py index 8cc94d38e80b5fc56f3b5a2d2dab1318408af9bc..baa6bccb6630849347f02cd47363536d7cc68d83 100644 --- a/app/urls.py +++ b/app/urls.py @@ -13,6 +13,8 @@ urlpatterns = [ path("materi/like/", views.toggle_like, name="PostLikeToggle"), path("delete/<int:pk_materi>/<int:pk_comment>", views.delete_comment, name="delete-comment"), + path("comment/like/", views.toggle_like_comment, name="comment-like-toggle"), + path("comment/dislike/", views.toggle_dislike_comment, name="comment-dislike-toggle"), path("materi/<int:pk>/delete", views.delete_materi, name="detele-materi"), path("materi/<int:pk>/unduh", views.download_materi, name="download-materi"), path("materi/<int:pk>/view", views.view_materi, name="view-materi"), diff --git a/app/views.py b/app/views.py index 034e74eb13eee27adcf8b3fca9a2ea038a374c05..a42118b080da8daf030d1b703c2f292072f7e4b6 100644 --- a/app/views.py +++ b/app/views.py @@ -20,8 +20,10 @@ from app.forms import SuntingProfilForm, UploadMateriForm, RatingContributorForm from app.models import ( Category, Comment, + DislikeComment, Materi, Like, + LikeComment, ViewStatistics, DownloadStatistics, ReqMaterial, @@ -169,7 +171,14 @@ class DetailMateri(TemplateView): def get(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) query_set_for_comment = Comment.objects.filter(materi=context["materi_data"]) + has_liked = {} + has_disliked = {} + for comment in query_set_for_comment: + has_liked[comment.id] = LikeComment.objects.filter(comment=comment, session_id=self.request.session.session_key).exists() + has_disliked[comment.id] = DislikeComment.objects.filter(comment=comment, session_id=self.request.session.session_key).exists() context["comment_data"] = query_set_for_comment + context["has_liked"] = has_liked + context["has_disliked"] = has_disliked return self.render_to_response(context=context) def get_user_name(self, request): @@ -227,6 +236,41 @@ def delete_comment(request, pk_materi, pk_comment): comment.delete() return HttpResponseRedirect(url) +def toggle_like_comment(request): + if request.method == "POST": + comment_id = request.POST.get("comment_id", None) + session_id = request.POST.get("session_id", None) + if comment_id is None or session_id is None: + return JsonResponse({"success": False, "msg": "Missing parameter", "comment_id": comment_id}) + comment = get_object_or_404(Comment, pk=comment_id) + has_liked = LikeComment.objects.filter(comment=comment, session_id=session_id).exists() + if has_liked: + like = get_object_or_404(LikeComment, comment=comment, session_id=session_id) + like.delete() + return JsonResponse({"success": True, "liked": True, "comment_id": comment_id}) + else: + LikeComment(comment=comment, session_id=session_id).save() + return JsonResponse({"success": True, "liked": False, "comment_id": comment_id}) + else: + return JsonResponse({"success": False, "msg": "Unsuported method", "comment_id": comment_id}) + +def toggle_dislike_comment(request): + if request.method == "POST": + comment_id = request.POST.get("comment_id", None) + session_id = request.POST.get("session_id", None) + if comment_id is None or session_id is None: + return JsonResponse({"success": False, "msg": "Missing parameter", "comment_id": comment_id}) + comment = get_object_or_404(Comment, pk=comment_id) + has_disliked = DislikeComment.objects.filter(comment=comment, session_id=session_id).exists() + if has_disliked: + dislike = get_object_or_404(DislikeComment, comment=comment, session_id=session_id) + dislike.delete() + return JsonResponse({"success": True, "disliked": True, "comment_id": comment_id}) + else: + DislikeComment(comment=comment, session_id=session_id).save() + return JsonResponse({"success": True, "disliked": False, "comment_id": comment_id}) + else: + return JsonResponse({"success": False, "msg": "Unsuported method", "comment_id": comment_id}) def get_citation_ieee(request, materi): current_date = datetime.datetime.now()