Fakultas Ilmu Komputer UI

Commit 599343b4 authored by Anthony Dewa Priyasembada's avatar Anthony Dewa Priyasembada
Browse files

[#82] Reading List

parent f7999a2f
# Generated by Django 3.1 on 2020-10-30 13:26
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('app', '0027_auto_20201030_1648'),
]
operations = [
migrations.CreateModel(
name='ReadLater',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
('materi', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.materi')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('materi', 'user')},
},
),
]
......@@ -272,3 +272,11 @@ class LaporanMateri(models.Model):
laporan = models.TextField(validators=[MinValueValidator(30), MaxValueValidator(120)], default="")
timestamp = models.DateTimeField(default=timezone.now)
is_rejected = models.BooleanField(default=False)
class ReadLater(models.Model):
materi = models.ForeignKey(Materi, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
timestamp = models.DateTimeField(default=timezone.now)
class Meta:
unique_together = ["materi", "user"]
\ No newline at end of file
......@@ -15,7 +15,7 @@ from pydrive.drive import GoogleDrive
from administration.models import VerificationReport
from app.forms import SuntingProfilForm
from app.models import Category, Like, LikeComment, DislikeComment, Materi, Comment, Rating, DownloadStatistics, \
ViewStatistics
ViewStatistics, ReadLater
from app.utils.fileManagementUtil import get_random_filename, remove_image_exifdata
from digipus import settings
import requests
......@@ -470,3 +470,18 @@ class GoogleDriveUploadService:
file1["title"] = title
print("title: %s, mimeType: %s" % (file1["title"], file1["mimeType"]))
file1.Upload()
class ReadLaterService:
@staticmethod
def toggle_read_later(materi_id, current_user):
materi = get_object_or_404(Materi, pk=materi_id)
read_later_item_exist = ReadLater.objects.filter(materi=materi, user=current_user).exists()
if read_later_item_exist:
read_later_item = get_object_or_404(ReadLater, materi=materi, user=current_user)
read_later_item.delete()
response = {"success": True, "read_later_checked": False}
else:
ReadLater(materi=materi, user=current_user).save()
response = {"success": True, "read_later_checked": True}
return response
\ No newline at end of file
......@@ -106,7 +106,7 @@ div.review {
<div class="col col-3 cover">
<img src={{materi_data.cover.url}} alt="cover">
</div>
<div class="col col-6 ml-3 book">
<div class="col col-8 ml-3 book">
<h2>{{materi_data.title}}</h2>
<div class="category-wrapper">
{% for category in materi_data.categories.all %}
......@@ -256,12 +256,32 @@ div.review {
</div>
</div>
</div>
{% if is_in_read_later_list %}
<button class="btn btn-book shadow-sm p-2 mr-2 bg-primary text-white rounded align-self-center"
type="button" id="readLaterButton" aria-haspopup="true" aria-expanded="false"
onclick="postToggleReadLater()">
<em class="align-self-center far fa-check-square" id="readLaterText"></em> Baca Nanti
</button>
{% else %}
<button class="btn btn-book shadow-sm p-2 mr-2 bg-white text-primary rounded align-self-center"
type="button" id="readLaterButton" aria-haspopup="true" aria-expanded="false"
onclick="postToggleReadLater()">
<em class="align-self-center far fa-square" id="readLaterText"></em> Baca Nanti
</button>
{% endif %}
{% 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>
<button class="btn btn-book shadow-sm p-2 mr-2 bg-white text-primary rounded align-self-center"
type="button" id="readLaterButton" aria-haspopup="true" aria-expanded="false"
onclick="postToggleReadLater()">
<em class="align-self-center far fa-square" id="readLaterText"></em> Baca Nanti
</button>
{% endif %}
</div>
</div>
......@@ -603,6 +623,37 @@ div.review {
});
}
function postToggleReadLater() {
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
$.ajax({
type: 'POST',
url: "{% url 'toggle-read-later' %}",
data: {
'materi_id': "{{ materi_data.id }}",
},
success: changeReadLaterButton,
dataType: 'html'
});
}
function changeReadLaterButton(data, jqXHR) {
var data = $.parseJSON(data)
if (data['read_later_checked']) {
$('#readLaterButton').removeClass("bg-white text-primary").addClass("bg-primary text-white")
$('#readLaterText').removeClass("fa-square").addClass("fa-check-square")
} else {
$('#readLaterButton').removeClass("bg-primary text-white").addClass("bg-white text-primary")
$('#readLaterText').removeClass("fa-check-square").addClass("fa-square")
}
}
function LikePost(data, jqXHR) {
var data = $.parseJSON(data)
var likeCount = parseInt($('.info-content')[6].textContent)
......
......@@ -27,4 +27,9 @@
<a class="nav-link" href="/given-rating/">
<span>Rating Diberikan</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/baca-nanti">
<span>Baca Nanti</span></a>
</li>
</ul>
\ No newline at end of file
{% extends 'app/base_profile.html' %}
{% load static %}
{% block title %}
<title>Baca Nanti | Digipus</title>
{% endblock %}
{% block stylesheets %}
<link rel="stylesheet" type="text/css" href="{% static 'app/css/katalog_materi.css' %}">
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"
integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
crossorigin="anonymous"></script>
<!-- Bootstrap core CSS -->
<link href="../../static/app/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="../../static/app/css/heroic-features.css" rel="stylesheet">
<link rel="icon" type="image/png" href="{% static 'images/icons/logo.ico' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'fonts/font-awesome-4.7.0/css/font-awesome.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'vendor/animate/animate.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'vendor/css-hamburgers/hamburgers.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'vendor/animsition/css/animsition.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'vendor/select2/select2.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'vendor/daterangepicker/daterangepicker.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/util.css' %}">
{% endblock %}
{% block content %}
<div class="container">
<div class="col-20">
<h1 class="mt-2">Daftar Materi Yang Belum Dibaca</h1>
<hr class="mt-0 mb-4">
{% if read_later_list %}
<div class="container row content">
<div class="col-20 books">
{% for item in read_later_list %}
<div class="card book">
<img src={{item.materi.cover.url}} class="card-img-top" alt="cover"
style="height:200px; widows: 200px; overflow: hidden;"></img>
<div class="card-body">
<h5 class="card-title">{{item.materi.title}}</h5>
<p class="card-text">{{item.materi.author}}</p>
<p class="card-text">Diunggah oleh
<a class="card-link" href="{% url 'katalog-per-kontributor' item.materi.uploader.email %}">
{{item.materi.uploader.name}}
</a>
</p>
<a href="{% url 'view-materi' item.materi.id %}" class="btn btn-book">Baca</a>
<a href="{% url 'detail-materi' item.materi.id %}" class="btn btn-book">Detail</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% else %}
<h1>Anda Tidak Memiliki Daftar Baca Nanti</h1>
{% endif %}
</div>
</div>
{% endblock %}
......@@ -42,6 +42,7 @@ from .models import (
ReqMaterial,
RatingContributor,
ViewStatistics,
ReadLater
)
from .services import (
......@@ -3444,6 +3445,148 @@ class MateriRecommendationTest(TestCase):
list = [int(id) for id in re.findall(r"Materi\s(\d+)[^\d]", response.content.decode())]
self.assertEqual(list, [1, 2])
class BacaNantiTest(TestCase):
def setUp(self):
self.contributor_credential = {
"email": "kontributor@gov.id",
"password": id_generator()
}
self.user_one_credential = {
"email": "user_one@user.id",
"password": id_generator()
}
self.user_two_credential = {
"email": "user_two@user.id",
"password": id_generator()
}
self.contributor = get_user_model().objects.create_user(
**self.contributor_credential, name="Kontributor", is_contributor=True
)
self.user_one = get_user_model().objects.create_user(**self.user_one_credential, name="User One")
self.user_two = get_user_model().objects.create_user(**self.user_two_credential, name="User Two")
self.cover = SimpleUploadedFile(
"cover.jpg",
b"Test file"
)
self.content = SimpleUploadedFile(
"content.txt",
b"Test file"
)
Materi(title="Materi 1", author="Agas", uploader=self.contributor,
publisher="Kelas SC", descriptions="Deskripsi Materi 1",
status="APPROVE", cover=self.cover, content=self.content).save()
Materi(title="Materi Dua", author="Author", uploader=self.contributor,
publisher="Publisher", descriptions="Deskripsi Materi Dua",
status="APPROVE", cover=self.cover, content=self.content).save()
self.materi1 = Materi.objects.filter(title='Materi 1').get()
self.materi2 = Materi.objects.filter(title='Materi Dua').get()
self.url = '/baca-nanti/'
self.toggle_url = '/baca-nanti-toggle/'
self.url_materi = '/materi/{}/'.format(self.materi1.id)
def test_readlater_object_can_be_created(self):
ReadLater(materi=self.materi1, user=self.user_one).save()
read_later = ReadLater.objects.first()
self.assertEqual(read_later.materi, self.materi1)
self.assertEqual(read_later.user, self.user_one)
def test_readlater_materi_must_not_unique(self):
ReadLater(materi=self.materi1, user=self.user_one).save()
ReadLater(materi=self.materi1, user=self.user_two).save()
read_later_one = ReadLater.objects.get(user=self.user_one)
read_later_two = ReadLater.objects.get(user=self.user_two)
self.assertEqual(read_later_one.materi, self.materi1)
self.assertEqual(read_later_one.user, self.user_one)
self.assertEqual(read_later_two.materi, self.materi1)
self.assertEqual(read_later_two.user, self.user_two)
def test_readlater_user_must_not_unique(self):
ReadLater(materi=self.materi1, user=self.user_one).save()
ReadLater(materi=self.materi2, user=self.user_one).save()
read_later_one = ReadLater.objects.get(materi=self.materi1)
read_later_two = ReadLater.objects.get(materi=self.materi2)
self.assertEqual(read_later_one.materi, self.materi1)
self.assertEqual(read_later_one.user, self.user_one)
self.assertEqual(read_later_two.materi, self.materi2)
self.assertEqual(read_later_two.user, self.user_one)
def test_readlater_materi_combined_with_user_must_be_unique(self):
with self.assertRaises(IntegrityError) as context:
ReadLater(materi=self.materi1, user=self.user_one).save()
ReadLater(materi=self.materi1, user=self.user_one).save()
self.assertTrue('already exists' in str(context.exception))
def test_readlater_materi_cant_null(self):
with self.assertRaises(IntegrityError):
ReadLater(user=self.user_one).save()
def test_readlater_user_cant_null(self):
with self.assertRaises(IntegrityError):
ReadLater(materi=self.materi1).save()
def test_readlater_profile_page_url_exist(self):
self.client.login(**self.user_one_credential)
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
def test_readlater_profile_page_using_template(self):
self.client.login(**self.user_one_credential)
response = self.client.get(self.url)
self.assertTemplateUsed(response=response, template_name="baca-nanti.html")
def test_toggle_readlater_url_exist(self):
self.client.login(**self.user_one_credential)
response = self.client.post(self.toggle_url, {'materi_id': self.materi1.id})
self.assertEqual(response.status_code, 200)
def test_checking_readlater_in_materi_create_object(self):
self.client.login(**self.user_one_credential)
self.client.post(self.toggle_url, {'materi_id': self.materi1.id})
read_later_exist = ReadLater.objects.filter(materi=self.materi1, user=self.user_one).exists()
self.assertEqual(read_later_exist, True)
def test_unchecking_readlater_in_materi_delete_object(self):
self.client.login(**self.user_one_credential)
self.client.post(self.toggle_url, {'materi_id': self.materi1.id})
sleep(1)
self.client.post(self.toggle_url, {'materi_id': self.materi1.id})
read_later_exist = ReadLater.objects.filter(materi=self.materi1, user=self.user_one).exists()
self.assertEqual(read_later_exist, False)
def test_checking_readlater_in_materi_with_complete_paramater_return_success(self):
self.client.login(**self.user_one_credential)
response = self.client.post(self.toggle_url, {'materi_id': self.materi1.id})
self.assertJSONEqual(
str(response.content, encoding='utf-8'),
{"success": True, "read_later_checked": True}
)
def test_unchecking_readlater_in_materi_with_complete_paramater_return_success(self):
self.client.login(**self.user_one_credential)
self.client.post(self.toggle_url, {'materi_id': self.materi1.id})
sleep(1)
response = self.client.post(self.toggle_url, {'materi_id': self.materi1.id})
self.assertJSONEqual(
str(response.content, encoding='utf-8'),
{"success": True, "read_later_checked": False}
)
def test_toggle_readlater_return_if_method_snot_post(self):
self.client.login(**self.user_one_credential)
response = self.client.get(self.toggle_url, {'materi_id': self.materi1.id})
self.assertJSONEqual(
str(response.content, encoding='utf-8'),
{"success": False, "msg": "Unsuported method"}
)
def test_toggle_readlater_return_if_paramater_materi_id_not_found(self):
self.client.login(**self.user_one_credential)
response = self.client.post(self.toggle_url)
self.assertJSONEqual(
str(response.content, encoding='utf-8'),
{"success": False, "msg": "Missing parameter"}
)
class MateriStatsTest(TestCase):
def setUp(self):
......
......@@ -5,7 +5,8 @@ from app import views
from app.views import (DashboardKontributorView, ProfilView, StatisticsView,
SuksesLoginAdminView, SuksesLoginKontributorView, DownloadHistoryView,
SuntingProfilView, UploadMateriHTML, UploadMateriView, UploadMateriExcelView, PostsView,
ReqMateriView, KatalogPerKontributorView, MateriFavorite, PasswordChangeViews, password_success, SubmitVisitorView)
ReqMateriView, KatalogPerKontributorView, MateriFavorite, PasswordChangeViews, password_success,
SubmitVisitorView, ReadLaterView)
urlpatterns = [
......@@ -41,5 +42,7 @@ urlpatterns = [
path("password_success/", views.password_success, name="password_success"),
path("given-rating/", views.see_given_rating, name="see_given_rating"),
path("submit-visitor/", SubmitVisitorView.as_view(), name="submit-visitor"),
path("baca-nanti/", ReadLaterView.as_view(), name="read-later"),
path("baca-nanti-toggle/", views.toggle_readlater, name="toggle-read-later"),
path("stats/", StatisticsView.as_view(), name="stats"),
]
......@@ -31,12 +31,13 @@ from app.models import (
Materi,
ReqMaterial,
Rating, RatingContributor,
SubmitVisitor
SubmitVisitor,
ReadLater
)
from authentication.models import User
from .services import DafterKatalogService, DetailMateriService, LikeDislikeService, MateriFieldValidationHelperService, \
DownloadViewMateriHelperService, UploadMateriService, EditProfileService, RevisiMateriService, \
DownloadHistoryService, GoogleDriveUploadService
DownloadHistoryService, GoogleDriveUploadService, ReadLaterService
def permission_denied(request, exception, template_name="error_403.html"):
......@@ -154,6 +155,12 @@ class DetailMateri(TemplateView):
if materi_rating is not None:
context['materi_rating_score'] = materi_rating.score
materi_read_later = ReadLater.objects.filter(materi=materi, user=self.request.user).first()
if materi_read_later is not None:
context['is_in_read_later_list'] = True
else:
context['is_in_read_later_list'] = False
context['is_authenticated'] = self.request.user.is_authenticated
return context
......@@ -781,6 +788,34 @@ class SubmitVisitorView(TemplateView):
SubmitVisitor(msg=title, user_id=user_id, email=email).save()
return JsonResponse({"success": True, "msg": "Buku tamu berhasil ditambahkan"})
class ReadLaterView(TemplateView):
template_name = 'baca-nanti.html'
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
raise PermissionDenied(request)
return super(ReadLaterView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(ReadLaterView, self).get_context_data(**kwargs)
return context
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
user = self.request.user
context["read_later_list"] = ReadLater.objects.filter(user=user).order_by('-timestamp')
return self.render_to_response(context)
def toggle_readlater(request):
if request.method == "POST":
materi_id = request.POST.get("materi_id", None)
if materi_id is None:
return JsonResponse({"success": False, "msg": "Missing parameter"})
return JsonResponse(ReadLaterService.toggle_read_later(materi_id, request.user))
else:
return JsonResponse({"success": False, "msg": "Unsuported method"})
class StatisticsView(TemplateView):
template_name = "statistik.html"
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment