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): ...@@ -272,3 +272,11 @@ class LaporanMateri(models.Model):
laporan = models.TextField(validators=[MinValueValidator(30), MaxValueValidator(120)], default="") laporan = models.TextField(validators=[MinValueValidator(30), MaxValueValidator(120)], default="")
timestamp = models.DateTimeField(default=timezone.now) timestamp = models.DateTimeField(default=timezone.now)
is_rejected = models.BooleanField(default=False) 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 ...@@ -15,7 +15,7 @@ from pydrive.drive import GoogleDrive
from administration.models import VerificationReport from administration.models import VerificationReport
from app.forms import SuntingProfilForm from app.forms import SuntingProfilForm
from app.models import Category, Like, LikeComment, DislikeComment, Materi, Comment, Rating, DownloadStatistics, \ 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 app.utils.fileManagementUtil import get_random_filename, remove_image_exifdata
from digipus import settings from digipus import settings
import requests import requests
...@@ -470,3 +470,18 @@ class GoogleDriveUploadService: ...@@ -470,3 +470,18 @@ class GoogleDriveUploadService:
file1["title"] = title file1["title"] = title
print("title: %s, mimeType: %s" % (file1["title"], file1["mimeType"])) print("title: %s, mimeType: %s" % (file1["title"], file1["mimeType"]))
file1.Upload() 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 { ...@@ -106,7 +106,7 @@ div.review {
<div class="col col-3 cover"> <div class="col col-3 cover">
<img src={{materi_data.cover.url}} alt="cover"> <img src={{materi_data.cover.url}} alt="cover">
</div> </div>
<div class="col col-6 ml-3 book"> <div class="col col-8 ml-3 book">
<h2>{{materi_data.title}}</h2> <h2>{{materi_data.title}}</h2>
<div class="category-wrapper"> <div class="category-wrapper">
{% for category in materi_data.categories.all %} {% for category in materi_data.categories.all %}
...@@ -256,12 +256,32 @@ div.review { ...@@ -256,12 +256,32 @@ div.review {
</div> </div>
</div> </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 %} {% else %}
<button class="btn dropdown-toggle btn-book shadow-sm p-2 mr-2 bg-white rounded align-self-center" <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" type="button" id="dropdownMenuButton" aria-haspopup="true" aria-expanded="false" data-toggle="modal"
data-target="#notLoggedInModal"> data-target="#notLoggedInModal">
<em class="align-self-center far fa-star"></em> Beri Rating <em class="align-self-center far fa-star"></em> Beri Rating
</button> </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 %} {% endif %}
</div> </div>
</div> </div>
...@@ -603,6 +623,37 @@ div.review { ...@@ -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) { function LikePost(data, jqXHR) {
var data = $.parseJSON(data) var data = $.parseJSON(data)
var likeCount = parseInt($('.info-content')[6].textContent) var likeCount = parseInt($('.info-content')[6].textContent)
......
...@@ -27,4 +27,9 @@ ...@@ -27,4 +27,9 @@
<a class="nav-link" href="/given-rating/"> <a class="nav-link" href="/given-rating/">
<span>Rating Diberikan</span></a> <span>Rating Diberikan</span></a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="/baca-nanti">
<span>Baca Nanti</span></a>
</li>
</ul> </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 ( ...@@ -42,6 +42,7 @@ from .models import (
ReqMaterial, ReqMaterial,
RatingContributor, RatingContributor,
ViewStatistics, ViewStatistics,
ReadLater
) )
from .services import ( from .services import (
...@@ -3444,6 +3445,148 @@ class MateriRecommendationTest(TestCase): ...@@ -3444,6 +3445,148 @@ class MateriRecommendationTest(TestCase):
list = [int(id) for id in re.findall(r"Materi\s(\d+)[^\d]", response.content.decode())] list = [int(id) for id in re.findall(r"Materi\s(\d+)[^\d]", response.content.decode())]
self.assertEqual(list, [1, 2]) 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): class MateriStatsTest(TestCase):
def setUp(self): def setUp(self):
......
...@@ -5,7 +5,8 @@ from app import views ...@@ -5,7 +5,8 @@ from app import views
from app.views import (DashboardKontributorView, ProfilView, StatisticsView, from app.views import (DashboardKontributorView, ProfilView, StatisticsView,
SuksesLoginAdminView, SuksesLoginKontributorView, DownloadHistoryView, SuksesLoginAdminView, SuksesLoginKontributorView, DownloadHistoryView,
SuntingProfilView, UploadMateriHTML, UploadMateriView, UploadMateriExcelView, PostsView, SuntingProfilView, UploadMateriHTML, UploadMateriView, UploadMateriExcelView, PostsView,
ReqMateriView, KatalogPerKontributorView, MateriFavorite, PasswordChangeViews, password_success, SubmitVisitorView) ReqMateriView, KatalogPerKontributorView, MateriFavorite, PasswordChangeViews, password_success,
SubmitVisitorView, ReadLaterView)
urlpatterns = [ urlpatterns = [
...@@ -41,5 +42,7 @@ urlpatterns = [ ...@@ -41,5 +42,7 @@ urlpatterns = [
path("password_success/", views.password_success, name="password_success"), path("password_success/", views.password_success, name="password_success"),
path("given-rating/", views.see_given_rating, name="see_given_rating"), path("given-rating/", views.see_given_rating, name="see_given_rating"),
path("submit-visitor/", SubmitVisitorView.as_view(), name="submit-visitor"), 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"), path("stats/", StatisticsView.as_view(), name="stats"),
] ]
...@@ -31,12 +31,13 @@ from app.models import ( ...@@ -31,12 +31,13 @@ from app.models import (
Materi, Materi,
ReqMaterial, ReqMaterial,
Rating, RatingContributor, Rating, RatingContributor,
SubmitVisitor SubmitVisitor,
ReadLater
) )
from authentication.models import User from authentication.models import User
from .services import DafterKatalogService, DetailMateriService, LikeDislikeService, MateriFieldValidationHelperService, \ from .services import DafterKatalogService, DetailMateriService, LikeDislikeService, MateriFieldValidationHelperService, \
DownloadViewMateriHelperService, UploadMateriService, EditProfileService, RevisiMateriService, \ DownloadViewMateriHelperService, UploadMateriService, EditProfileService, RevisiMateriService, \
DownloadHistoryService, GoogleDriveUploadService DownloadHistoryService, GoogleDriveUploadService, ReadLaterService
def permission_denied(request, exception, template_name="error_403.html"): def permission_denied(request, exception, template_name="error_403.html"):
...@@ -154,6 +155,12 @@ class DetailMateri(TemplateView): ...@@ -154,6 +155,12 @@ class DetailMateri(TemplateView):
if materi_rating is not None: if materi_rating is not None:
context['materi_rating_score'] = materi_rating.score 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 context['is_authenticated'] = self.request.user.is_authenticated
return context return context
...@@ -781,6 +788,34 @@ class SubmitVisitorView(TemplateView): ...@@ -781,6 +788,34 @@ class SubmitVisitorView(TemplateView):
SubmitVisitor(msg=title, user_id=user_id, email=email).save() SubmitVisitor(msg=title, user_id=user_id, email=email).save()
return JsonResponse({"success": True, "msg": "Buku tamu berhasil ditambahkan"}) 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): class StatisticsView(TemplateView):
template_name = "statistik.html" 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