diff --git a/app/migrations/0015_downloadstatistics_downloader.py b/app/migrations/0015_downloadstatistics_downloader.py new file mode 100644 index 0000000000000000000000000000000000000000..16780d0c070064f852acf2e180dfcb1ba5179afc --- /dev/null +++ b/app/migrations/0015_downloadstatistics_downloader.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.4 on 2020-09-30 04:50 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('app', '0014_rating'), + ] + + operations = [ + migrations.AddField( + model_name='downloadstatistics', + name='downloader', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='riwayat_unduh', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/app/migrations/0018_merge_20201009_0700.py b/app/migrations/0018_merge_20201009_0700.py new file mode 100644 index 0000000000000000000000000000000000000000..2e5d56480fa1f304956819f4012e5e52c2100d5e --- /dev/null +++ b/app/migrations/0018_merge_20201009_0700.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1 on 2020-10-09 00:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0017_auto_20201005_2145'), + ('app', '0015_downloadstatistics_downloader'), + ] + + operations = [ + ] diff --git a/app/models.py b/app/models.py index 54e05f404d9b68827b86d9ce247a2f7873caa26c..4164b8443638d80ea89f091d476ef96ff0ce954d 100644 --- a/app/models.py +++ b/app/models.py @@ -98,6 +98,8 @@ class ViewStatistics(models.Model): class DownloadStatistics(models.Model): materi = models.ForeignKey( Materi, models.SET_NULL, null=True, related_name="unduh") + downloader = models.ForeignKey( + User, models.SET_NULL, blank=True, null=True, related_name="riwayat_unduh") timestamp = models.DateTimeField(default=timezone.now) diff --git a/app/static/app/css/katalog_materi.css b/app/static/app/css/katalog_materi.css index fa4103f99ac5280444c273fa45d38b3b9c8da919..67a035f6e97ee499d66aaf892692709b38e2ea1e 100644 --- a/app/static/app/css/katalog_materi.css +++ b/app/static/app/css/katalog_materi.css @@ -62,6 +62,21 @@ body{ background-color: #615CFD; } +.btn-history { + width: auto; + padding: 8px 8px; + border-radius: 2px; + background-color: #ffffff; + color: #615CFD; + border: none; + box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); +} + +.btn-history:hover{ + color: #ffffff; + background-color: #615CFD; +} + .content { display: flex; margin: 5px; diff --git a/app/templates/app/katalog_materi.html b/app/templates/app/katalog_materi.html index c4013e21b6549391bcea6a7281e3540f782a98d0..0d02d0b102957774ca1586703592ac85b5d393da 100644 --- a/app/templates/app/katalog_materi.html +++ b/app/templates/app/katalog_materi.html @@ -49,7 +49,6 @@ <body style="background-color: #f8f8f8;"> <!-- Page Content --> <div class="container"> - <header class="jumbotron my-4"> <div class="container"> <div class="row header"> @@ -61,15 +60,17 @@ <input type="text" name='search' class="form-control" placeholder="Tulis di sini" value='{{request.GET.search}}'> </div> - <button type="submit" class="btn btn-cari">Cari</button> + <button type="submit" class="btn btn-cari">Cari</button> </form> <p class="pageTitle">Tidak menemukan materi yang kamu cari ? ajukan permintaan materi kami <a href="/req-materi">disini</a></p> </div> </div> - </div> + </div> </header> - + + <a href="/download-history/" class="btn-history">Riwayat Unduh</a><br><br> + <div class="container"> <div class="row content"> <div class="col-3 sidebar"> @@ -154,7 +155,6 @@ <div class="center"> <div class="pagination"> <span class="step-links"> - <span class="current"> Page {{ materi_list.number }} of {{ materi_list.paginator.num_pages }} </span> diff --git a/app/templates/download_history.html b/app/templates/download_history.html new file mode 100644 index 0000000000000000000000000000000000000000..cbb47b957137bd20fa472b0f7aed5559cc30b330 --- /dev/null +++ b/app/templates/download_history.html @@ -0,0 +1,61 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %} +<title>Riwayat Unduh | Digipus</title> +{% endblock %} + +{% block header %} +<link rel="icon" type="image/png" href="{% static 'images/icons/logo.ico' %}" /> +<link href="https://fonts.googleapis.com/css2?family=Poppins&display=swap" rel="stylesheet"> + +<link href="{% static 'css/sb-admin-2.min.css' %}" rel="stylesheet"> +<link rel="stylesheet" href="{% static 'css/button.css' %}"> + +<link href="{% static 'vendor/datatables/dataTables.bootstrap4.min.css' %}" rel="stylesheet"> +{% endblock %} + +{% block content %} +<br><br> +<h1 id="riwayat_unduh" class="h3 mb-2 text-gray-800">Riwayat Unduh | {{ user_name }}</h1> +{% if riwayat_list %} +<p class="mb-4">Tekan tombol detail untuk informasi lebih lanjut tentang materi</p> + +<div class="card shadow mb-4"> + <div class="card-body"> + <div class="table-responsive"> + <table aria-describedby="riwayat_unduh" class="table table-bordered" id="dataTable"> + <thead> + <tr> + <th id="judul_materi">Judul Materi</th> + <th id="author">Pembuat Materi</th> + <th id="download_time">Waktu Download</th> + <th id="detail_buttons">Detail</th> + </tr> + </thead> + <tbody> + {% for riwayat in riwayat_list %} + <tr> + <td>{{riwayat.materi.title}}</td> + <td>{{riwayat.materi.author}}</td> + <td>{{riwayat.timestamp|date:"d F Y H:i:s"}}</td> + <td class="verif-buttons"> + <span> + <a href="/materi/{{riwayat.materi.id}}/" class="accept-button button-decoration" + style="background-color:#4e73df">Detail</a> + </span> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> +{% else %} +<p class="mb-4">Anda belum mengunduh materi. Silahkan unduh materi yang anda butuhkan</p> +{% endif %} +<span> + <a href="/" class="btn btn-primary main-content" style="background-color:#4e73df; right: 0px;">Back to Katalog</a> +</span> +<br> +{% endblock %} \ No newline at end of file diff --git a/app/tests.py b/app/tests.py index cff2740ab0736020aa14278053b5180e6ff2b562..2b65c523038a63a196033069604832b23b10bd92 100644 --- a/app/tests.py +++ b/app/tests.py @@ -1,31 +1,39 @@ import json, tempfile, os +import pandas as pd from io import StringIO +from bs4 import BeautifulSoup +from datetime import datetime from django.conf import settings +from django.contrib import messages as dj_messages from django.contrib.auth import get_user_model +from django.core import serializers +from django.core.files import File 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, TestCase, TransactionTestCase -from django.urls import resolve -from django.contrib import messages as dj_messages from django.db.utils import IntegrityError +from django.urls import resolve +from django.test import Client, RequestFactory, TestCase, TransactionTestCase +from pytz import timezone +from time import sleep from administration.models import VerificationSetting, VerificationReport from administration.utils import id_generator -from app.views import UploadMateriView, add_rating_materi +from app.views import UploadMateriHTML, add_rating_materi from authentication.models import User -from .models import Category, Comment, Materi, Like, Rating, ReqMaterial, RatingContributor +from digipus.settings import TIME_ZONE +from .models import (Category, Comment, DownloadStatistics, Materi, Like, + Rating, ReqMaterial, RatingContributor, ViewStatistics) + from .views import (DaftarKatalog, DashboardKontributorView, DetailMateri, ProfilKontributorView, SuksesLoginAdminView, SuksesLoginKontributorView, SuntingProfilView, ProfilAdminView, PostsView, SuntingProfilAdminView, RevisiMateriView, ReqMateriView, KatalogPerKontributorView, - UploadMateriExcelView) + UploadMateriView, UploadMateriExcelView) from app.forms import SuntingProfilForm from app.utils.fileManagementUtil import get_random_filename, remove_image_exifdata -import pandas as pd - class DaftarKatalogTest(TestCase): def test_daftar_katalog_url_exist(self): @@ -1658,3 +1666,318 @@ class RatingContributorTest(TransactionTestCase): self.assertEqual(0, RatingContributor.objects.filter(user=self.contributor.id).count()) self.client.post(url, data={"user": self.contributor.id, "score": 0}) self.assertEqual(0, RatingContributor.objects.filter(user=self.contributor.id).count()) + +class UserDownloadHistoryTest(TestCase): + def setUp(self): + self.user1_credential = { + "email": "anonim1@gov.id", + "password": id_generator() + } + self.user1_anonim = get_user_model().objects.create_user( + **self.user1_credential, name="Anonim1") + self.user2_credential = { + "email": "anonim2@gov.id", + "password": id_generator() + } + self.user2_anonim = get_user_model().objects.create_user( + **self.user2_credential, name="Anonim2") + self.contributor_credential = { + "email": "kontributor@gov.id", + "password": id_generator() + } + self.contributor = get_user_model().objects.create_user( + **self.contributor_credential, name="Kontributor", is_contributor=True + ) + self.client = Client() + content = b"Test file" + self.cover = SimpleUploadedFile( + "cover.jpg", + content + ) + self.content = SimpleUploadedFile( + "content.txt", + content + ) + Materi(title="Materi 1", author="Agas", uploader=self.contributor, + publisher="Kelas SC", descriptions="Deskripsi Materi 1", + status="PENDING", cover=self.cover, content=self.content).save() + self.materi1 = Materi.objects.first() + self.download_url = f"/materi/{self.materi1.id}/unduh" + self.history_url = "/download-history/" + + def test_multiple_insert_download_statistic_with_user(self): + DownloadStatistics(materi=self.materi1, downloader=self.user1_anonim).save() + num_of_downloads = self.user1_anonim.riwayat_unduh.all().count() + self.assertEqual(num_of_downloads, 1) + + DownloadStatistics(materi=self.materi1, downloader=self.user1_anonim).save() + num_of_downloads = self.user1_anonim.riwayat_unduh.all().count() + self.assertEqual(num_of_downloads, 2) + + def test_download_statistics_bound_to_specific_user(self): + DownloadStatistics(materi=self.materi1, downloader=self.user1_anonim).save() + num_of_downloads = self.user1_anonim.riwayat_unduh.all().count() + self.assertEqual(num_of_downloads, 1) + + DownloadStatistics(materi=self.materi1).save() + num_of_downloads = self.user1_anonim.riwayat_unduh.all().count() + self.assertEqual(num_of_downloads, 1) + + DownloadStatistics(materi=self.materi1, downloader=self.user2_anonim).save() + user1_num_of_downloads = self.user1_anonim.riwayat_unduh.all().count() + user2_num_of_downloads = self.user2_anonim.riwayat_unduh.all().count() + self.assertEqual(user1_num_of_downloads, 1) + self.assertEqual(user2_num_of_downloads, 1) + + def test_registered_user_download(self): + # Login + self.client.login(**self.user1_credential) + + self.client.get(self.download_url) + num_of_downloads = self.user1_anonim.riwayat_unduh.all().count() + self.assertEqual(num_of_downloads, 1) + + # Logout + self.client.logout() + + def test_unregistered_user_download(self): + self.client.get(self.download_url) + downloaded_materi = self.client.session['downloaded_materi'] + num_of_downloads = DownloadStatistics.objects.filter( + pk__in=downloaded_materi).count() + self.assertEqual(num_of_downloads, 1) + + def test_registered_user_multiple_download(self): + # Login + self.client.login(**self.user1_credential) + self.client.get(self.download_url) + num_of_downloads = self.user1_anonim.riwayat_unduh.all().count() + self.assertEqual(num_of_downloads, 1) + + self.client.get(self.download_url) + num_of_downloads = self.user1_anonim.riwayat_unduh.all().count() + self.assertEqual(num_of_downloads, 2) + + # Logout + self.client.logout() + + def test_unregistered_user_multiple_download(self): + self.client.get(self.download_url) + downloaded_materi = self.client.session['downloaded_materi'] + num_of_downloads = DownloadStatistics.objects.filter( + pk__in=downloaded_materi).count() + self.assertEqual(num_of_downloads, 1) + + self.client.get(self.download_url) + downloaded_materi = self.client.session['downloaded_materi'] + num_of_downloads = DownloadStatistics.objects.filter( + pk__in=downloaded_materi).count() + self.assertEqual(num_of_downloads, 2) + + def test_registered_user_doesnt_use_session_when_download(self): + # Login + self.client.login(**self.user1_credential) + + self.client.get(self.download_url) + self.assertFalse('downloaded_materi' in self.client.session) + + # Logout + self.client.logout() + + def test_download_history_bound_to_specific_user(self): + # Login Anonym 1 + self.client.login(**self.user1_credential) + self.client.get(self.download_url) + num_of_downloads = self.user1_anonim.riwayat_unduh.all().count() + self.assertEqual(num_of_downloads, 1) + + # Logout Anonym 1 + self.client.logout() + + # Unregistered User download + self.client.get(self.download_url) + user1_num_of_downloads = self.user1_anonim.riwayat_unduh.all().count() + downloaded_materi = self.client.session['downloaded_materi'] + guest_num_of_downloads = DownloadStatistics.objects.filter( + pk__in=downloaded_materi).count() + self.assertEqual(user1_num_of_downloads, 1) + self.assertEqual(guest_num_of_downloads, 1) + + # Login Anonym 2 + self.client.login(**self.user2_credential) + self.client.get(self.download_url) + user1_num_of_downloads = self.user1_anonim.riwayat_unduh.all().count() + user2_num_of_downloads = self.user2_anonim.riwayat_unduh.all().count() + self.assertEqual(user1_num_of_downloads, 1) + self.assertEqual(guest_num_of_downloads, 1) + self.assertEqual(user2_num_of_downloads, 1) + + # Logout Anonym 2 + self.client.logout() + +class DownloadHistoryViewTest(TestCase): + def setUp(self): + self.user_credential = { + "email": "anonim1@gov.id", + "password": id_generator() + } + self.user_anonim = get_user_model().objects.create_user( + **self.user_credential, name="Anonim") + self.contributor_credential = { + "email": "kontributor@gov.id", + "password": id_generator() + } + self.contributor = get_user_model().objects.create_user( + **self.contributor_credential, name="Kontributor", is_contributor=True + ) + self.client = Client() + + content1 = b"Test file" + content2 = b"File Test" + + self.cover1 = SimpleUploadedFile("cover1.jpg",content1) + self.content1 = SimpleUploadedFile("content1.txt",content1) + + self.cover2 = SimpleUploadedFile("cover2.jpg",content2) + self.content2 = SimpleUploadedFile("content2.txt",content2) + + self.materi1 = Materi.objects.create(title="Materi 1", author="Agas", uploader=self.contributor, + publisher="Kelas SC", descriptions="Deskripsi Materi 1", + status="PENDING", cover=self.cover1, content=self.content1) + self.materi2 = Materi.objects.create(title="Materi 2", author="Danin", uploader=self.contributor, + publisher="Kelas DDP", descriptions="Deskripsi Materi 2", + status="PENDING", cover=self.cover2, content=self.content2) + + self.download_url1 = f"/materi/{self.materi1.id}/unduh" + self.download_url2 = f"/materi/{self.materi2.id}/unduh" + self.history_url = "/download-history/" + + # Login + self.client.login(**self.user_credential) + + def tearDown(self): + # Logout + self.client.logout() + + def test_allow_registered_user(self): + response = self.client.get(self.history_url) + self.assertEqual(response.status_code, 200) + + def test_allow_unregistered_user(self): + # Forced Logout + self.client.logout() + + response = self.client.get(self.history_url) + self.assertEqual(response.status_code, 200) + + def test_download_history_using_correct_template(self): + response = self.client.get(self.history_url) + self.assertTemplateUsed(response, "download_history.html") + + def test_download_history_has_user_name(self): + response = self.client.get(self.history_url) + resp_html = response.content.decode('utf8') + self.assertIn(self.user_anonim.name, resp_html) + + def test_registered_user_download_history_correctly_displayed(self): + self.client.get(self.download_url1) + self.client.get(self.download_url2) + self.client.get(self.download_url1) + + jkt_timezone = timezone(TIME_ZONE) + + download_history = self.user_anonim.riwayat_unduh.all() + response = self.client.get(self.history_url) + resp_html = response.content.decode('utf8') + for riwayat in download_history: + downloaded_materi = riwayat.materi + self.assertIn(downloaded_materi.title, resp_html) + self.assertIn(downloaded_materi.author, resp_html) + + jkt_timestamp = riwayat.timestamp.astimezone(jkt_timezone) + self.assertIn(jkt_timestamp.strftime("%d %B %Y %H:%M:%S"), resp_html) + + def test_unregistered_user_download_history_correctly_displayed(self): + self.client.logout() + + self.client.get(self.download_url1) + self.client.get(self.download_url2) + self.client.get(self.download_url1) + + jkt_timezone = timezone(TIME_ZONE) + + response = self.client.get(self.history_url) + resp_html = response.content.decode('utf8') + for riwayat_id in self.client.session['downloaded_materi']: + riwayat = DownloadStatistics.objects.get(pk=riwayat_id) + downloaded_materi = riwayat.materi + self.assertIn(downloaded_materi.title, resp_html) + self.assertIn(downloaded_materi.author, resp_html) + + jkt_timestamp = riwayat.timestamp.astimezone(jkt_timezone) + self.assertIn(jkt_timestamp.strftime("%d %B %Y %H:%M:%S"), resp_html) + + def test_download_history_not_display_if_user_changed(self): + self.client.get(self.download_url1) + self.client.get(self.download_url2) + self.client.get(self.download_url1) + + self.client.logout() + + jkt_timezone = timezone(TIME_ZONE) + + download_history = self.user_anonim.riwayat_unduh.all() + response = self.client.get(self.history_url) + resp_html = response.content.decode('utf8') + for riwayat in download_history: + downloaded_materi = riwayat.materi + self.assertNotIn(downloaded_materi.title, resp_html) + self.assertNotIn(downloaded_materi.author, resp_html) + + jkt_timestamp = riwayat.timestamp.astimezone(jkt_timezone) + self.assertNotIn(jkt_timestamp.strftime("%d %B %Y %H:%M:%S"), resp_html) + + def test_unregistered_user_download_history_wont_be_saved_if_user_changes(self): + self.client.logout() + + self.client.get(self.download_url1) + self.client.get(self.download_url2) + self.client.get(self.download_url1) + + self.client.get(self.history_url) + + self.client.login(**self.user_credential) + self.client.logout() + self.assertFalse('downloaded_materi' in self.client.session) + + def test_download_history_sorted_by_download_time(self): + # download with 1 second interval to differ download time + self.client.get(self.download_url1) + sleep(1) + self.client.get(self.download_url2) + sleep(1) + self.client.get(self.download_url1) + sleep(1) + self.client.get(self.download_url2) + + response = self.client.get(self.history_url) + resp_html = response.content.decode('utf8') + + table_html = ("<table" + resp_html.split("<table")[1]).split("</table>")[0] + "</table>" + soup = BeautifulSoup(table_html, 'html.parser') + histories_html = soup.find('tbody').find_all('tr') + prev_timestamp = None + + for riwayat_html in histories_html: + materi_data = riwayat_html.find_all("td") + date_format = "%d %B %Y %H:%M:%S" + materi_timestamp = datetime.strptime(materi_data[2].get_text(), date_format) + if prev_timestamp: + self.assertTrue(prev_timestamp > materi_timestamp) + prev_timestamp = materi_timestamp + + def test_no_history_display_message(self): + no_history_msg = "Anda belum mengunduh materi. Silahkan unduh materi yang anda butuhkan" + response = self.client.get(self.history_url) + resp_html = response.content.decode('utf8') + self.assertIn(no_history_msg, resp_html) \ No newline at end of file diff --git a/app/urls.py b/app/urls.py index e401f2ecc8735b95178609f659d757635d2f8881..a0230cfb6efe40b0c558c4e9588559f609c3b3f6 100644 --- a/app/urls.py +++ b/app/urls.py @@ -2,7 +2,7 @@ from django.urls import path, re_path from app import views from app.views import (DashboardKontributorView, ProfilKontributorView, - SuksesLoginAdminView, SuksesLoginKontributorView, + SuksesLoginAdminView, SuksesLoginKontributorView, DownloadHistoryView, SuntingProfilView, UploadMateriHTML, UploadMateriView, UploadMateriExcelView, ProfilAdminView, PostsView, SuntingProfilAdminView, ReqMateriView, KatalogPerKontributorView) @@ -16,6 +16,7 @@ urlpatterns = [ path("materi/<int:pk>/unduh", views.download_materi, name="download-materi"), path("materi/<int:pk>/view", views.view_materi, name="view-materi"), path("dashboard/", DashboardKontributorView.as_view(), name="dashboard"), + path("download-history/", DownloadHistoryView.as_view(), name="download-history"), path("revisi/materi/<int:pk>/", views.RevisiMateriView.as_view(), name="revisi"), path("unggah/", UploadMateriView.as_view(), name="unggah"), path("unggah_excel/", UploadMateriExcelView.as_view(), name="unggah_excel"), diff --git a/app/views.py b/app/views.py index 8a6887565562d847ff8fa1c52c40b3c240811419..d204493a86f270c11059acfb7a7b9a35c2908081 100644 --- a/app/views.py +++ b/app/views.py @@ -248,7 +248,14 @@ def download_materi(request, pk): response = HttpResponse(fh.read(), content_type=mimetype[0]) response["Content-Disposition"] = "attachment; filename=" + \ os.path.basename(file_path) - DownloadStatistics(materi=materi).save() + if request.user.is_authenticated: + DownloadStatistics(materi=materi, downloader=request.user).save() + else: + downloaded_materi = DownloadStatistics.objects.create(materi=materi) + if 'downloaded_materi' not in request.session: + request.session['downloaded_materi'] = [] + request.session['downloaded_materi'].append(downloaded_materi.pk) + request.session.modified = True return response except Exception as e: raise Http404("File tidak dapat ditemukan.") @@ -750,3 +757,27 @@ def pages(request): template = loader.get_template("error-404.html") return HttpResponse(template.render(context, request)) + +class DownloadHistoryView(TemplateView): + template_name = "download_history.html" + + def get_context_data(self, **kwargs): + context = super(DownloadHistoryView, + self).get_context_data(**kwargs) + return context + + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + if request.user.is_authenticated: + current_user = self.request.user + riwayat_list = current_user.riwayat_unduh.all().order_by('-timestamp') + context["riwayat_list"] = riwayat_list + context["user_name"] = current_user.name + else: + has_downloaded_materi = 'downloaded_materi' in request.session + downloaded_materi = request.session['downloaded_materi'] if has_downloaded_materi else [] + riwayat_list = DownloadStatistics.objects.filter( + pk__in=downloaded_materi).order_by('-timestamp') + context["riwayat_list"] = riwayat_list + context["user_name"] = 'Guest' + return self.render_to_response(context)