Fakultas Ilmu Komputer UI

Commit 9edc8814 authored by Kevin Raikhan Zain's avatar Kevin Raikhan Zain
Browse files

Merge branch '1706075041-43' into 'master'

[#43] Add rating in detail materi

See merge request !10
parents 34c193ee 8477d1cf
Pipeline #57816 passed with stages
in 8 minutes and 30 seconds
......@@ -57,7 +57,7 @@
</div>
</div>
</div>
</li>
</li>
{% endif %}
{% endif %}
......@@ -170,11 +170,39 @@
{% else %}
<button id="thumb" class="btn btn-link btn-book shadow-sm p-2 mr-2 bg-white rounded"><i id="thumbIcon" aria-hidden="true" class="far fa-thumbs-up"></i> Sukai</button>
{% endif %}
{% if user.is_authenticated %}
<div class="dropdown">
<button class="btn dropdown-toggle btn-book shadow-sm p-2 mr-2 bg-white rounded align-self-center"
type="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
<em id="button-rating-star-icon" class="align-self-center far fa-star"></em>
<span id="button-rating-text">Beri Rating</span>
</button>
<div id="star-dropdown" class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<div class="text-center btn-book">
<em onclick="postAddRating(1)" id="star-1" class="far fa-star fa-lg"></em>
<em onclick="postAddRating(2)" id="star-2" class="far fa-star fa-lg"></em>
<em onclick="postAddRating(3)" id="star-3" class="far fa-star fa-lg"></em>
<em onclick="postAddRating(4)" id="star-4" class="far fa-star fa-lg"></em>
<em onclick="postAddRating(5)" id="star-5" class="far fa-star fa-lg"></em>
</div>
</div>
</div>
{% else %}
<button class="btn dropdown-toggle btn-book shadow-sm p-2 mr-2 bg-white rounded align-self-center"
type="button"
id="dropdownMenuButton" aria-haspopup="true"
aria-expanded="false" data-toggle="modal"
data-target="#notLoggedInModal">
<em class="align-self-center far fa-star"></em> Beri Rating
</button>
{% endif %}
</div>
</div>
</div>
<div class="row menu-wrapper mr-4 ml-4 p-3">
<nav class="navbar navbar-expand-sm border-top border-bottom p-0 mt-3 mb-3">
<nav class="navbar navbar-expand-sm border-top border-bottom p-0 mt-3 mb-3">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="#deskripsi">Deskripsi</a>
......@@ -199,8 +227,8 @@
{% csrf_token %}
<h1>Komentar</h1>
<div class="form-group">
<textarea placeholder="Beri komentar..." class="form-control"
id="exampleFormControlTextarea1"
<textarea placeholder="Beri komentar..." class="form-control"
id="exampleFormControlTextarea1"
rows="3" name="comment" required
></textarea>
<button type="submit" class="btn btn-link btn-book shadow-sm p-2 mt-2 bg-white rounded">Kirim</button>
......@@ -246,12 +274,89 @@
</div>
</div>
</footer>
<!-- Modal -->
<div class="modal fade" id="notLoggedInModal" tabindex="-1" role="dialog" aria-labelledby="notLoggedInModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="notLoggedInModalLabel">Belum Login</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
Login untuk memberikan rating
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
<button type="button" class="btn btn-primary" onclick="window.location.href = '/login';">Login</button>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block extra_scripts %}
<script src="https://kit.fontawesome.com/bc2cedd6b2.js" crossorigin="anonymous"></script>
<script type="text/javascript">
// using jQuery
var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
let currentRating = {{ materi_rating_score }};
fillRatingStar(currentRating)
function changeButtonIntoRated() {
$('#button-rating-star-icon').addClass('fas');
$('#button-rating-star-icon').removeClass('far');
$('#button-rating-text').text("Rating Anda")
}
if (currentRating > 0) {
changeButtonIntoRated()
}
$("#star-dropdown").click(function (e) {
e.stopPropagation();
});
function clearRatingStar() {
for (let i = 1; i <= 5; i++) {
$('#star-' + i).addClass('far');
$('#star-' + i).removeClass('fas');
}
}
function fillRatingStar(ratingScore) {
for (let i = 1; i <= ratingScore; i++) {
$('#star-' + i).addClass('fas');
$('#star-' + i).removeClass('far');
}
}
function makeHoverStar(starAmount) {
function hoverStar() {
clearRatingStar()
fillRatingStar(starAmount)
}
return hoverStar
}
function makeUnHoverStar(starAmount) {
function unHoverStar() {
clearRatingStar()
fillRatingStar(currentRating)
}
return unHoverStar
}
$('#star-1').hover(makeHoverStar(1), makeUnHoverStar(1));
$('#star-2').hover(makeHoverStar(2), makeUnHoverStar(2));
$('#star-3').hover(makeHoverStar(3), makeUnHoverStar(3));
$('#star-4').hover(makeHoverStar(4), makeUnHoverStar(4));
$('#star-5').hover(makeHoverStar(5), makeUnHoverStar(5));
</script>
<script>
function csrfSafeMethod(method) {
......@@ -281,6 +386,32 @@
});
});
function postAddRating(rating_score) {
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
$.ajax({
type: 'POST',
url: "{% url 'rate-materi' %}",
data: {
'materi_id': "{{ materi_data.id }}",
'rating_score': rating_score
},
success: function (data) {
const response = JSON.parse(data)
currentRating = response.rating_score
makeUnHoverStar(currentRating)()
changeButtonIntoRated()
},
dataType: 'html'
});
}
function LikePost(data, jqXHR) {
var data = $.parseJSON(data)
......
......@@ -6,13 +6,14 @@ from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
from django.test import Client, RequestFactory, TestCase
from django.db import IntegrityError
from django.test import Client, TestCase
from django.urls import resolve
from administration.models import VerificationSetting, VerificationReport
from administration.utils import id_generator
from app.views import UploadMateriView
from app.forms import SuntingProfilForm
from app.views import UploadMateriView, add_rating_materi
from authentication.models import User
from .models import Category, Comment, Materi, Like, Rating
from .views import (DaftarKatalog, DashboardKontributorView, DetailMateri,
......@@ -218,7 +219,7 @@ class PostsViewTest(TestCase):
def test_url_resolves_to_posts_view(self):
found = resolve(self.url)
self.assertEqual(found.func.__name__, PostsView.as_view().__name__)
def test_returns_200_on_authenticated_access(self):
response = self._request_as_user()
self.assertEqual(response.status_code, 200)
......@@ -254,14 +255,14 @@ class PostsViewTest(TestCase):
}
self.assertRegex(
str(response.content),
str(response.content),
rf'.*(<div id="post-{posts[2]}">)' + \
rf'.*(<div id="post-{posts[2]}-comment-{comments[2][0]}">)' + \
rf'.*(<div id="post-{posts[2]}-comment-{comments[2][1]}">)' + \
rf'.*(<div id="post-{posts[2]}-comment-{comments[2][2]}">)' + \
rf'.*(<div id="post-{posts[2]}-comment-{comments[2][0]}">)' + \
rf'.*(<div id="post-{posts[2]}-comment-{comments[2][1]}">)' + \
rf'.*(<div id="post-{posts[2]}-comment-{comments[2][2]}">)' + \
rf'.*(<div id="post-{posts[1]}">)' + \
rf'.*(<div id="post-{posts[0]}">)' + \
rf'.*(<div id="post-{posts[0]}-comment-{comments[0][0]}">)'
rf'.*(<div id="post-{posts[0]}-comment-{comments[0][0]}">)'
)
......@@ -345,6 +346,7 @@ class UploadPageTest(TestCase):
# Negative tests
self.assertNotContains(response, "anything")
class DashboardKontributorViewTest(TestCase):
def setUp(self):
self.client = Client()
......@@ -406,6 +408,7 @@ class DashboardKontributorViewTest(TestCase):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 403)
class ProfilAdminTest(TestCase):
def setUp(self):
self.client = Client()
......@@ -444,6 +447,7 @@ class ProfilAdminTest(TestCase):
# Logout
self.client.logout()
class ProfilKontributorTest(TestCase):
def setUp(self):
self.client = Client()
......@@ -749,7 +753,7 @@ class LikeMateriTest(TestCase):
def test_like_materi(self):
# Verify that materi doesn't have any like to start with
num_of_likes = Like.objects.filter(materi = self.materi1).count()
num_of_likes = Like.objects.filter(materi=self.materi1).count()
self.assertEqual(num_of_likes, 0)
# Like a materi
......@@ -761,12 +765,12 @@ class LikeMateriTest(TestCase):
'session_id': session_id
}
ajax_response = Client().post(self.url_like, payload)
num_of_likes = Like.objects.filter(materi = self.materi1).count()
num_of_likes = Like.objects.filter(materi=self.materi1).count()
self.assertEqual(num_of_likes, 1)
def test_unlike_materi(self):
# Verify that materi doesn't have any like to start with
num_of_likes = Like.objects.filter(materi = self.materi1).count()
num_of_likes = Like.objects.filter(materi=self.materi1).count()
self.assertEqual(num_of_likes, 0)
# Like a materi
......@@ -778,7 +782,7 @@ class LikeMateriTest(TestCase):
'session_id': session_id
}
ajax_response = Client().post(self.url_like, payload)
num_of_likes = Like.objects.filter(materi = self.materi1).count()
num_of_likes = Like.objects.filter(materi=self.materi1).count()
self.assertEqual(num_of_likes, 1)
# Unlike a materi
......@@ -790,12 +794,12 @@ class LikeMateriTest(TestCase):
'session_id': session_id
}
ajax_response = Client().post(self.url_like, payload)
num_of_likes = Like.objects.filter(materi = self.materi1).count()
num_of_likes = Like.objects.filter(materi=self.materi1).count()
self.assertEqual(num_of_likes, 0)
def test_2_client_like_materi(self):
# Verify that materi doesn't have any like to start with
num_of_likes = Like.objects.filter(materi = self.materi1).count()
num_of_likes = Like.objects.filter(materi=self.materi1).count()
self.assertEqual(num_of_likes, 0)
# Client 1 like a materi
......@@ -807,7 +811,7 @@ class LikeMateriTest(TestCase):
'session_id': session_id
}
ajax_response = Client().post(self.url_like, payload)
num_of_likes = Like.objects.filter(materi = self.materi1).count()
num_of_likes = Like.objects.filter(materi=self.materi1).count()
self.assertEqual(num_of_likes, 1)
# Client 2 like a materi
......@@ -819,12 +823,12 @@ class LikeMateriTest(TestCase):
'session_id': session_id
}
ajax_response = Client().post(self.url_like, payload)
num_of_likes = Like.objects.filter(materi = self.materi1).count()
num_of_likes = Like.objects.filter(materi=self.materi1).count()
self.assertEqual(num_of_likes, 2)
def test_incomplete_like_parameter(self):
# Verify that materi doesn't have any like to start with
num_of_likes = Like.objects.filter(materi = self.materi1).count()
num_of_likes = Like.objects.filter(materi=self.materi1).count()
self.assertEqual(num_of_likes, 0)
# missing session id
......@@ -835,7 +839,7 @@ class LikeMateriTest(TestCase):
}
ajax_response = Client().post(self.url_like, payload)
ajax_response = json.loads(ajax_response.content)
num_of_likes = Like.objects.filter(materi = self.materi1).count()
num_of_likes = Like.objects.filter(materi=self.materi1).count()
self.assertEqual(num_of_likes, 0)
self.assertEqual(ajax_response.get("success", None), False)
......@@ -847,7 +851,7 @@ class LikeMateriTest(TestCase):
}
ajax_response = Client().post(self.url_like, payload)
ajax_response = json.loads(ajax_response.content)
num_of_likes = Like.objects.filter(materi = self.materi1).count()
num_of_likes = Like.objects.filter(materi=self.materi1).count()
self.assertEqual(num_of_likes, 0)
self.assertEqual(ajax_response.get("success", None), False)
......@@ -893,6 +897,7 @@ class ViewMateriStatissticsTest(TestCase):
num_of_views = self.materi1.baca.all().count()
self.assertEqual(num_of_views, 2)
class DownloadMateriStatissticsTest(TestCase):
def setUp(self):
self.contributor_credential = {
......@@ -934,6 +939,7 @@ class DownloadMateriStatissticsTest(TestCase):
num_of_downloads = self.materi1.unduh.all().count()
self.assertEqual(num_of_downloads, 2)
class RevisiMateriTest(TestCase):
def setUp(self):
self.client = Client()
......@@ -1040,7 +1046,6 @@ class RemoveDummyCommandTest(TestCase):
class RatingMateriTest(TestCase):
def setUp(self):
self.url = '/administration/'
self.contributor_credential = {
"email": "kontributor@gov.id",
"password": id_generator()
......@@ -1074,6 +1079,8 @@ class RatingMateriTest(TestCase):
status="APPROVE", cover=self.cover, content=self.content).save()
self.materi1 = Materi.objects.all()[0]
self.materi2 = Materi.objects.all()[1]
self.url_rate = '/materi/rate/'
self.url_materi = '/materi/{}/'.format(self.materi1.id)
def test_rating_model_can_be_created_with_proper_parameter(self):
Rating(materi=self.materi1, user=self.user_one, score=5).save()
......@@ -1131,6 +1138,105 @@ class RatingMateriTest(TestCase):
with self.assertRaises(TypeError):
Rating(materi=self.materi1, user=self.user_one).save()
def test_rating_materi_url_use_add_rating_materi_function(self):
found = resolve(self.url_rate)
self.assertEqual(found.func, add_rating_materi)
def test_rating_materi_get_method_should_return_403_forbidden(self):
response = self.client.get(self.url_rate)
response_json = json.loads(response.content)
self.assertEqual(response_json.get("success", None), False)
self.assertEqual(response_json.get("msg", None), "Forbidden")
self.assertEqual(response.status_code, 403)
def test_rating_materi_post_not_authenticated_should_return_403_forbidden(self):
response = self.client.post(self.url_rate, {'materi_id': 1, 'rating_score': 5})
response_json = json.loads(response.content)
self.assertEqual(response_json.get("success", None), False)
self.assertEqual(response_json.get("msg", None), "Forbidden")
self.assertEqual(response.status_code, 403)
def test_rating_materi_not_authenticated_post_wrong_param_should_return_403_forbidden(self):
for data in [{}, {'materi_id': 1}, {'rating_score': 1}, {'rating_score': 'STRING', 'materi_id': 'STRING'}]:
response = self.client.post(self.url_rate, data)
response_json = json.loads(response.content)
self.assertEqual(response_json.get("success", None), False)
self.assertEqual(response_json.get("msg", None), "Forbidden")
self.assertEqual(response.status_code, 403)
def test_rating_materi_authenticated_post_missing_param(self):
self.client.login(**self.user_one_credential)
for data in [{'rating_score': 1}, {'materi_id': 1}, {}]:
response = self.client.post(self.url_rate, data)
response_json = json.loads(response.content)
self.assertEqual(response_json.get("success", None), False)
self.assertEqual(response_json.get("msg", None), "Missing param")
self.assertEqual(response.status_code, 422)
def test_rating_materi_authenticated_materi_id_doesnt_exist_should_return_422(self):
self.client.login(**self.user_one_credential)
response = self.client.post(self.url_rate, {'materi_id': 123456, 'rating_score': 5})
response_json = json.loads(response.content)
self.assertEqual(response_json.get("success", None), False)
self.assertEqual(response_json.get("msg", None), "Materi does not exist")
self.assertEqual(response.status_code, 422)
def test_rating_materi_authenticated_param_wrong_data_type_should_return_422(self):
self.client.login(**self.user_one_credential)
response = self.client.post(self.url_rate, {'materi_id': "STRING", 'rating_score': 5})
response_json = json.loads(response.content)
self.assertEqual(response_json.get("success", None), False)
self.assertEqual(response_json.get("msg", None), "materi_id must be an integer")
self.assertEqual(response.status_code, 422)
response = self.client.post(self.url_rate, {'materi_id': 1, 'rating_score': "STRING"})
response_json = json.loads(response.content)
self.assertEqual(response_json.get("success", None), False)
self.assertEqual(response_json.get("msg", None), "rating_score must be an integer")
self.assertEqual(response.status_code, 422)
def test_rating_score_should_be_between_1_and_5(self):
self.client.login(**self.user_one_credential)
for i in range(1, 6):
Rating.objects.all().delete()
response = self.client.post(self.url_rate, {'materi_id': self.materi1.id, 'rating_score': i})
response_json = json.loads(response.content)
# self.assertEqual(response_json.get("success", None), True)
self.assertEqual(response_json.get("msg", None), "Rating successfully created")
self.assertEqual(response.status_code, 201)
for i in [-100, -7, -6, -1, 0, 6, 7, 100]:
Rating.objects.all().delete()
response = self.client.post(self.url_rate, {'materi_id': self.materi1.id, 'rating_score': i})
response_json = json.loads(response.content)
# self.assertEqual(response_json.get("success", None), False)
self.assertEqual(response_json.get("msg", None), "Rating must be an integer from 1 to 5")
self.assertEqual(response.status_code, 422)
def test_user_should_not_able_to_rate_materi_twice(self):
self.client.login(**self.user_one_credential)
Rating.objects.all().delete()
self.client.post(self.url_rate, {'materi_id': self.materi1.id, 'rating_score': 1})
response = self.client.post(self.url_rate, {'materi_id': self.materi1.id, 'rating_score': 2})
response_json = json.loads(response.content)
# self.assertEqual(response_json.get("success", None), False)
self.assertEqual(response_json.get("msg", None), "Rating already exist")
self.assertEqual(response.status_code, 409)
def test_user_authenticated_visit_unrated_should_get_0_materi_rating_score_context(self):
self.client.login(**self.user_one_credential)
response = self.client.get(self.url_materi)
self.assertEqual(0, response.context.get('materi_rating_score'))
def test_user_not_authenticated_visit_unrated_should_get_0_materi_rating_score_context(self):
response = self.client.get(self.url_materi)
self.assertEqual(0, response.context.get('materi_rating_score'))
def test_user_authenticated_visit_rated_should_get_correct_materi_rating_score_context(self):
self.client.login(**self.user_one_credential)
Rating(materi=self.materi1, user=self.user_one, score=1).save()
response = self.client.get(self.url_materi)
self.assertEqual(1, response.context.get('materi_rating_score'))
class fileManagementUtilTest(TestCase):
def setUp(self):
......
......@@ -28,4 +28,5 @@ urlpatterns = [
path("sunting-admin/", SuntingProfilAdminView.as_view(), name="sunting-admin"),
path("req-materi/", ReqMateriView.as_view(), name="req-materi"),
path("post-req-materi/", views.post_req_materi, name="post-req-materi"),
path("materi/rate/", views.add_rating_materi, name="rate-materi"),
]
......@@ -2,23 +2,20 @@ import mimetypes
import os
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.contrib import messages
from django.core import serializers
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.paginator import Paginator
from django.db.models import Q, Count
from django.http import (Http404, HttpResponse, HttpResponseRedirect,
JsonResponse)
from django.shortcuts import get_object_or_404, redirect, render
from django.shortcuts import get_object_or_404
from django.template import loader
from django.urls import reverse
from django.views.generic import TemplateView, ListView
from .models import Category, Comment, Materi
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.views.generic import TemplateView
from administration.models import VerificationReport
from app.forms import SuntingProfilForm, UploadMateriForm
from app.models import Category, Comment, Materi, Like, ViewStatistics, DownloadStatistics, ReqMaterial
from app.models import Category, Comment, Materi, Like, ViewStatistics, DownloadStatistics, ReqMaterial, Rating
from app.utils.fileManagementUtil import get_random_filename, remove_image_exifdata
from authentication.models import User
import django
......@@ -67,10 +64,10 @@ class DaftarKatalog(TemplateView):
elif(getSort == "terbaru"):
lstMateri = lstMateri.order_by('-date_created')
elif(getSort == "terlama"):
lstMateri = lstMateri.order_by('date_created')
lstMateri = lstMateri.order_by('date_created')
elif(getSort == "terpopuler"):
lstMateri = lstMateri.annotate(count=Count('like__id')).order_by('-count')
context["materi_list"] = lstMateri
paginator = Paginator(context["materi_list"], 15)
page_number = request.GET.get('page')
......@@ -94,6 +91,13 @@ class DetailMateri(TemplateView):
context["report"] = VerificationReport.objects.filter(materi=materi)
context["has_liked"] = Like.objects.filter(
materi=materi, session_id=self.request.session.session_key).exists()
context['materi_rating_score'] = 0
if self.request.user.is_authenticated:
materi_rating = Rating.objects.filter(materi=materi, user=self.request.user).first()
if materi_rating is not None:
context['materi_rating_score'] = materi_rating.score
return context
def get(self, request, *args, **kwargs):
......@@ -160,6 +164,41 @@ def delete_comment(request, pk_materi, pk_comment):
return HttpResponseRedirect(url)
def add_rating_materi(request):
if request.method == 'POST' and request.user.is_authenticated:
materi_id = request.POST.get('materi_id', None)
rating_score = request.POST.get('rating_score', None)
if materi_id is None or rating_score is None:
return JsonResponse({"success": False, "msg": "Missing param"}, status=422)
try:
rating_score = int(rating_score)
except ValueError:
return JsonResponse({"success": False, "msg": "rating_score must be an integer"}, status=422)
try:
materi_id = int(materi_id)
except ValueError:
return JsonResponse({"success": False, "msg": "materi_id must be an integer"}, status=422)
if rating_score not in range(1, 6):
return JsonResponse({"success": False, "msg": "Rating must be an integer from 1 to 5"}, status=422)
materi = Materi.objects.filter(pk=materi_id).first()
if materi is None:
return JsonResponse({"success": False, "msg": "Materi does not exist"}, status=422)
if Rating.objects.filter(materi=materi, user=request.user).first() is not None:
return JsonResponse({"success": False, "msg": "Rating already exist"}, status=409)
Rating(materi=materi, user=request.user, score=rating_score).save()
return JsonResponse({"success": True, "msg": "Rating successfully created", "rating_score": rating_score},
status=201)
return JsonResponse({"success": False, "msg": "Forbidden"}, status=403)
def download_materi(request, pk):
materi = get_object_or_404(Materi, pk=pk)
path = materi.content.path
......@@ -238,7 +277,7 @@ class UploadMateriView(TemplateView):
context = self.get_context_data(**kwargs)
context["form"] = UploadMateriForm
return self.render_to_response(context)
def validate_file_extension(self, value):
ext = os.path.splitext(value.name)[1] # [0] returns path+filename
valid_extensions = ['.pdf', '.doc', '.docx', '.jpg', '.png', '.xlsx', '.xls', '.mp4', '.mp3']
......@@ -549,7 +588,7 @@ class PostsView(TemplateView):
class RevisiMateriView(TemplateView):
template_name = "revisi.html"
def dispatch(self, request, *args, **kwargs):
def dispatch(self, request, *args, **kwargs):
if not request.user.is_contributor:
raise PermissionDenied(request)
return super(RevisiMateriView, self).dispatch(request, *args,<