Fakultas Ilmu Komputer UI

Commit 4a7c10fe authored by SAMUEL TUPA FEBRIAN's avatar SAMUEL TUPA FEBRIAN
Browse files

Merge branch '2006561102-77' into 'master'

[#77] Profile: User Download History

See merge request !21
parents f7557f93 afb13d6e
Pipeline #58005 passed with stages
in 13 minutes and 1 second
# 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),
),
]
# 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 = [
]
...@@ -98,6 +98,8 @@ class ViewStatistics(models.Model): ...@@ -98,6 +98,8 @@ class ViewStatistics(models.Model):
class DownloadStatistics(models.Model): class DownloadStatistics(models.Model):
materi = models.ForeignKey( materi = models.ForeignKey(
Materi, models.SET_NULL, null=True, related_name="unduh") 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) timestamp = models.DateTimeField(default=timezone.now)
......
...@@ -62,6 +62,21 @@ body{ ...@@ -62,6 +62,21 @@ body{
background-color: #615CFD; 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 { .content {
display: flex; display: flex;
margin: 5px; margin: 5px;
......
...@@ -49,7 +49,6 @@ ...@@ -49,7 +49,6 @@
<body style="background-color: #f8f8f8;"> <body style="background-color: #f8f8f8;">
<!-- Page Content --> <!-- Page Content -->
<div class="container"> <div class="container">
<header class="jumbotron my-4"> <header class="jumbotron my-4">
<div class="container"> <div class="container">
<div class="row header"> <div class="row header">
...@@ -61,15 +60,17 @@ ...@@ -61,15 +60,17 @@
<input type="text" name='search' class="form-control" placeholder="Tulis di sini" <input type="text" name='search' class="form-control" placeholder="Tulis di sini"
value='{{request.GET.search}}'> value='{{request.GET.search}}'>
</div> </div>
<button type="submit" class="btn btn-cari">Cari</button> <button type="submit" class="btn btn-cari">Cari</button>
</form> </form>
<p class="pageTitle">Tidak menemukan materi yang kamu cari ? ajukan permintaan materi kami <a <p class="pageTitle">Tidak menemukan materi yang kamu cari ? ajukan permintaan materi kami <a
href="/req-materi">disini</a></p> href="/req-materi">disini</a></p>
</div> </div>
</div> </div>
</div> </div>
</header> </header>
<a href="/download-history/" class="btn-history">Riwayat Unduh</a><br><br>
<div class="container"> <div class="container">
<div class="row content"> <div class="row content">
<div class="col-3 sidebar"> <div class="col-3 sidebar">
...@@ -154,7 +155,6 @@ ...@@ -154,7 +155,6 @@
<div class="center"> <div class="center">
<div class="pagination"> <div class="pagination">
<span class="step-links"> <span class="step-links">
<span class="current"> <span class="current">
Page {{ materi_list.number }} of {{ materi_list.paginator.num_pages }} Page {{ materi_list.number }} of {{ materi_list.paginator.num_pages }}
</span> </span>
......
{% 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
import json, tempfile, os import json, tempfile, os
import pandas as pd
from io import StringIO from io import StringIO
from bs4 import BeautifulSoup
from datetime import datetime
from django.conf import settings from django.conf import settings
from django.contrib import messages as dj_messages
from django.contrib.auth import get_user_model 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.exceptions import PermissionDenied, ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command 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.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.models import VerificationSetting, VerificationReport
from administration.utils import id_generator 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 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, from .views import (DaftarKatalog, DashboardKontributorView, DetailMateri,
ProfilKontributorView, SuksesLoginAdminView, ProfilKontributorView, SuksesLoginAdminView,
SuksesLoginKontributorView, SuntingProfilView, SuksesLoginKontributorView, SuntingProfilView,
ProfilAdminView, PostsView, SuntingProfilAdminView, ProfilAdminView, PostsView, SuntingProfilAdminView,
RevisiMateriView, ReqMateriView, KatalogPerKontributorView, RevisiMateriView, ReqMateriView, KatalogPerKontributorView,
UploadMateriExcelView) UploadMateriView, UploadMateriExcelView)
from app.forms import SuntingProfilForm from app.forms import SuntingProfilForm
from app.utils.fileManagementUtil import get_random_filename, remove_image_exifdata from app.utils.fileManagementUtil import get_random_filename, remove_image_exifdata
import pandas as pd
class DaftarKatalogTest(TestCase): class DaftarKatalogTest(TestCase):
def test_daftar_katalog_url_exist(self): def test_daftar_katalog_url_exist(self):
...@@ -1658,3 +1666,318 @@ class RatingContributorTest(TransactionTestCase): ...@@ -1658,3 +1666,318 @@ class RatingContributorTest(TransactionTestCase):
self.assertEqual(0, RatingContributor.objects.filter(user=self.contributor.id).count()) self.assertEqual(0, RatingContributor.objects.filter(user=self.contributor.id).count())
self.client.post(url, data={"user": self.contributor.id, "score": 0}) self.client.post(url, data={"user": self.contributor.id, "score": 0})
self.assertEqual(0, RatingContributor.objects.filter(user=self.contributor.id).count()) 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
...@@ -2,7 +2,7 @@ from django.urls import path, re_path ...@@ -2,7 +2,7 @@ from django.urls import path, re_path
from app import views from app import views
from app.views import (DashboardKontributorView, ProfilKontributorView, from app.views import (DashboardKontributorView, ProfilKontributorView,
SuksesLoginAdminView, SuksesLoginKontributorView, SuksesLoginAdminView, SuksesLoginKontributorView, DownloadHistoryView,
SuntingProfilView, UploadMateriHTML, UploadMateriView, UploadMateriExcelView, SuntingProfilView, UploadMateriHTML, UploadMateriView, UploadMateriExcelView,
ProfilAdminView, PostsView, SuntingProfilAdminView, ProfilAdminView, PostsView, SuntingProfilAdminView,
ReqMateriView, KatalogPerKontributorView) ReqMateriView, KatalogPerKontributorView)
...@@ -16,6 +16,7 @@ urlpatterns = [ ...@@ -16,6 +16,7 @@ urlpatterns = [