Fakultas Ilmu Komputer UI

Commit de1986f3 authored by vagrant's avatar vagrant
Browse files

Merge branch 'master' of...

Merge branch 'master' of https://gitlab.cs.ui.ac.id/pmpl/class-project/marjinal-digipus into 1706028676-121
parents f8d442fc 041f2dc8
Pipeline #59970 passed with stages
in 34 minutes and 26 seconds
......@@ -16,6 +16,32 @@ pylint:
paths:
- ./pylint/
.development:
variables:
IMAGE_SCOPE_TAG: "$CI_COMMIT_BRANCH"
except:
- master
environment: development
.production:
variables:
IMAGE_SCOPE_TAG: stable
only:
- master
environment: production
.docker-image:
stage: deploy
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [ "" ]
variables:
CONTEXT: $CI_PROJECT_DIR
script:
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
- /kaniko/executor --context ${CONTEXT} --dockerfile ${CONTEXT}/Dockerfile --destination ${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG} --destination ${CI_REGISTRY_IMAGE}:${IMAGE_SCOPE_TAG}
UnitTest:
services:
- postgres:alpine
......@@ -68,3 +94,14 @@ Deployment:
environment:
name: staging
url: $HEROKU_APP_HOST_STAGING
Dev Docker Image:
extends:
- .docker-image
- .development
allow_failure: true
Prod Docker Image:
extends:
- .docker-image
- .production
\ No newline at end of file
......@@ -82,6 +82,23 @@ DB_PORT=5432
You can adjust `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `DB_HOST`, and `DB_PORT`
based on your local configuration.
Furthermore, this project utilizes Django's mail engine to send notification email
for contributors that're subscribing to new comments on their uploaded materials.
The codebase already have a class email configured as a default sender email, but you can use your own too.
To do so, please configure your `.env` to have these values:
```bash
EMAIL_HOST_USER=<your-email>@gmail.com
EMAIL_HOST_PASSWORD=<your-email-password>
```
> Note:
> Be informed that only **Google Mail accounts** that can be used, since the default SMTP server host & port are Google's.
>
> Your email also have to have [`Less secure app access` turned **on**](https://www.google.com/settings/security/lesssecureapps).
> [Reference](https://stackoverflow.com/a/26852782)
>
> If you want to use other SMTP server, please configure through env var `EMAIL_HOST` and `EMAIL_PORT`.
After you clone this repository, let's make sure you installed all requirements, migrate, and collect static.
```bash
pip3 install -r requirements.txt
......
......@@ -37,7 +37,8 @@ class SuntingProfilForm(forms.ModelForm):
model = User
fields = ["email","name","instansi", "nik", "alamat", "nomor_telpon",
"profile_picture", "linkedin",
"facebook", "twitter", "instagram", "biography"]
"facebook", "twitter", "instagram", "biography",
"is_subscribing_to_material_comments"]
def __init__(self, *args, **kwargs):
super(SuntingProfilForm, self).__init__(*args, **kwargs)
......
# Generated by Django 3.1 on 2020-10-29 11:22
import app.models
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', '0024_merge_20201026_0812'),
]
operations = [
migrations.CreateModel(
name='Review',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(max_length=100)),
('profile', models.CharField(default=app.models.getRandomColor, max_length=100)),
('review', models.TextField(default='review')),
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
('materi', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='app.materi')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
]
# Generated by Django 3.1 on 2020-10-29 13:45
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('app', '0025_review'),
]
operations = [
migrations.CreateModel(
name='SubmitVisitor',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user_id', models.CharField(max_length=50)),
('email', models.CharField(max_length=50)),
('msg', models.CharField(max_length=100)),
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
],
),
]
......@@ -197,6 +197,12 @@ class ReqMaterial(models.Model):
title = models.CharField(max_length=100)
timestamp = models.DateTimeField(default=timezone.now)
class SubmitVisitor(models.Model):
user_id = models.CharField(max_length=50)
email = models.CharField(max_length=50)
msg = models.CharField(max_length=100)
timestamp = models.DateTimeField(default=timezone.now)
class ViewStatistics(models.Model):
materi = models.ForeignKey(Materi, models.SET_NULL, null=True, related_name="baca")
......
......@@ -134,6 +134,10 @@ class DetailMateriService:
user_name = request.user.name
return user_name
@staticmethod
def init_materi_download_count(context, materi):
context["materi_download_count"] = materi.unduh.all().count()
class CitationService:
@staticmethod
......
......@@ -164,6 +164,14 @@ div.review {
<p class="info-content">{{materi_data.content.size|filesizeformat}}</p>
</dd>
</div>
<div class="info" id="1">
<dl class="col col-4">
<dt class="info-name">Jumlah Download</dt>
</dl>
<dd>
<p class="info-content">{{materi_download_count}}</p>
</dd>
</div>
</div>
<div class="buttons d-flex flex-row bd-highlight mb-1">
<a href="{% url 'view-materi' materi_data.id %}"
......
......@@ -70,6 +70,8 @@
href="/req-materi">disini</a></p>
<p class="pageTitle">Ingin diskusi lebih mendalam? Silahkan kunjungi <a
href="{% url 'forum_home' %}">forum kami</a></p>
<p class="pageTitle">Terima kasih sudah berkunjung, mohon isi buku tamu terlebih dahulu <a
href="/submit-visitor">disini</a> jika kamu sudah terdaftar!</p>
</div>
</div>
</div>
......
{% extends "base.html" %}
{% load static %}
{% block title %}Digipus Home{% endblock %}
{% block header %}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Digipus Home</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="{% static 'app/css/katalog_materi.css' %}">
<!-- 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' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/main.css' %}">
<!--===============================================================================================-->
{% endblock header %}
{% block content %}
</head>
<body style="background-color: #f8f8f8;">
<!-- Page Content -->
<div class="container">
<header class="jumbotron my-4">
<div class="container">
<div class="row header">
<div class="col">
<h2 class="pageTitle">Hi! {{ user.name }} Selamat datang di halaman buku tamu!</h2>
<input type="hidden" id="user_id" name="user_id" value="{{ user.id}}"/>
<input type="hidden" id="email" name="email" value="{{ user.email}}"/>
{% if user %}
<p class="description">Sampaikan pesan dan kesan kamu terhadap aplikasi ini</p>
{% else %}
<p class="description">Sampaikan pesan dan kesan kamu terhadap aplikasi ini</p>
{% endif %}
<form class="searchBar" >
{% csrf_token %}
<div class="col-6 form-group">
<input type="text" name='search' class="form-control" placeholder="Tulis di sini" id="title" value="" required>
</div>
<button id="btn_req_submit" style="width: auto;padding-left: 10px;padding-right: 10px;" class="btn btn-cari">Submit pesan ke buku tamu</button>
</form>
<p id="msg" style="display: none;margin: 5px">Buku Tamu berhasil ditambahkan</p>
</div>
</div>
</div>
</header>
</div>
<!-- /.container -->
</body>
</html>
<script src="https://kit.fontawesome.com/bc2cedd6b2.js" crossorigin="anonymous"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
<script type="text/javascript">
var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
</script>
<script>
function csrfSafeMethod(method) {
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
</script>
<script type="text/javascript">
$('#btn_req_submit').click(function () {
var title = document.getElementById('title');
var msg = document.getElementById('msg');
var user_id = document.getElementById('user_id');
var email = document.getElementById('email');
if (title.value == ''){
return;
}
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
$.ajax({
type: 'POST',
url: "{% url 'submit-visitor' %}",
data: {
'title': title.value,
'user_id': user_id.value,
'email': email.value,
},
success: function(Json) {
/* clear the error message first */
if (Json.success){
swal("Informasi!", Json.msg, "success");
msg.style.display = 'block';
title.value = "";
setTimeout(function(){ $("#msg").fadeOut("slow"); }, 3000);
}else{
swal("Informasi!", Json.msg, "error");
msg.style.display = 'block';
setTimeout(function(){ $("#msg").fadeOut("slow"); }, 3000);
}
},
error: function (xhr, status, errorThrown) {
xhr.status;
swal("Error!", xhr.responseText, "error");
xhr.responseText;
}
});
return false;
});
</script>
{% endblock %}
\ No newline at end of file
import json, tempfile, os, mock, base64
import pandas as pd
from io import StringIO
import re
import time
from django.test import override_settings
......@@ -9,7 +10,7 @@ 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 import mail, serializers
from django.core.files import File
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
......@@ -42,6 +43,10 @@ from .models import (
ViewStatistics,
)
from .services import (
DetailMateriService,
)
from .views import (
DaftarKatalog,
DashboardKontributorView,
......@@ -62,6 +67,7 @@ from .views import (
)
from app.forms import SuntingProfilForm, year_choices
from app.utils.fileManagementUtil import get_random_filename, remove_image_exifdata
from app.utils.PasswordValidator import PasswordPolicyValidator
ERROR_403_MESSAGE = "Kamu harus login untuk mengakses halaman ini"
......@@ -277,6 +283,20 @@ class DaftarKatalogPerKontributorTest(TestCase):
class DetailMateriTest(TestCase):
def _get_materi_info_html(self, info_name, info_value):
info_html = '<div class="info" id="1"><dl class="col col-4">'
info_html += f'<dt class="info-name">{info_name}</dt>' + '</dl><dd>'
info_html += f'<p class="info-content">{info_value}</p>' + '</dd></div>'
return info_html
def check_materi_info_in_html(self, info_name, info_value, html_content):
expected_content = self._get_materi_info_html(info_name, info_value)
self.assertIn(expected_content, re.sub(">\s*<","><", html_content))
def check_materi_info_not_in_html(self, info_name, info_value, html_content):
expected_content = self._get_materi_info_html(info_name, info_value)
self.assertNotIn(expected_content, re.sub(">\s*<","><", html_content))
def setUp(self):
self.client = Client()
self.admin_credential = {
......@@ -302,11 +322,19 @@ class DetailMateriTest(TestCase):
"ExampleCover921.jpg", b"Test file")
self.content = SimpleUploadedFile("ExampleFile921.pdf", 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()
self.materi1 = Materi.objects.first()
self.materi1 = Materi.objects.create(title="Materi 1", author="Agas",
uploader=self.contributor, publisher="Kelas SC",
descriptions="Deskripsi Materi 1", status="APPROVE",
cover=self.cover, content=self.content)
self.materi2 = Materi.objects.create(title="Materi 2", author="Agad",
uploader=self.contributor, publisher="Kelas SM",
descriptions="Deskripsi Materi 2", status="APPROVE",
cover=self.cover, content=self.content)
self.url = "/materi/" + str(self.materi1.id) + "/"
self.download_url1 = self.url + "unduh"
self.url2 = "/materi/" + str(self.materi2.id) + "/"
self.download_url2 = self.url2 + "unduh"
self.dcount_info_name = "Jumlah Download"
self.materi_with_published_date = Materi.objects.create(title="Materi 1", author="Agas", uploader=self.contributor,
publisher="Kelas SC", descriptions="Deskripsi Materi 1",
......@@ -334,18 +362,18 @@ class DetailMateriTest(TestCase):
def test_category_models_can_create_new_object(self):
test = Category.objects.create(
id="1", name="medis", description="kategori medis")
countData = Category.objects.all().count()
self.assertEqual(1, countData)
self.assertNotEqual(0, countData)
count = Category.objects.all().count()
self.assertEqual(1, count)
self.assertNotEqual(0, count)
self.assertEqual(test.__str__(), "medis")
self.assertNotEqual(test.__str__(), "saul")
def test_comment_models_can_create_new_object(self):
test = Comment.objects.create(
username="saul", profile="121212", comment="232323")
countData = Comment.objects.all().count()
self.assertEqual(1, countData)
self.assertNotEqual(0, countData)
count = Comment.objects.all().count()
self.assertEqual(1, count)
self.assertNotEqual(0, count)
self.assertEqual(test.__str__(), "saul")
self.assertNotEqual(test.__str__(), "userlain")
......@@ -477,6 +505,90 @@ class DetailMateriTest(TestCase):
comment_like_counter = LikeComment.objects.filter(comment=comment, session_id=session_id).count()
self.assertEqual(comment_like_counter, 0)
def test_comment_sends_email_to_contributor_that_subscribes(self):
contributor_subscribed = get_user_model().objects.create_user(
email="contributor_subscribing@gov.id",
password="passwordtest",
name="Kontributor-subscribed",
is_contributor=True,
is_subscribing_to_material_comments=True
)
material = Materi.objects.create(title="Materi-subscribed", author="Tester",
uploader=contributor_subscribed, publisher="Kelas PMPL",
descriptions="Deskripsi Materi subscribed", status="APPROVE",
cover=self.cover, content=self.content)
url = "/materi/" + str(material.id) + "/"
self.client.login(**self.contributor_credential) # comment with other user
prev_outbox_count = len(mail.outbox)
comment_content = "Test comment should send email"
self.client.post(
url, {"comment": comment_content})
current_outbox_count = len(mail.outbox)
# Comment notification email sent
self.assertEqual(current_outbox_count, prev_outbox_count + 1)
def test_comment_doesnt_send_email_to_contributor_that_not_subscribes(self):
contributor_not_subscribed = get_user_model().objects.create_user(
email="contributor_not_subscribing@gov.id",
password="passwordtest",
name="Kontributor-not-subscribed",
is_contributor=True,
is_subscribing_to_material_comments=False
)
material = Materi.objects.create(title="Materi-not-subscribed", author="Tester",
uploader=contributor_not_subscribed, publisher="Kelas PMPL",
descriptions="Deskripsi Materi non-subscribed", status="APPROVE",
cover=self.cover, content=self.content)
url = "/materi/" + str(material.id) + "/"
self.client.login(**self.contributor_credential) # comment with other user
prev_outbox_count = len(mail.outbox)
comment_content = "Test comment should not send email"
self.client.post(
url, {"comment": comment_content})
current_outbox_count = len(mail.outbox)
# Comment notification email not sent
self.assertEqual(current_outbox_count, prev_outbox_count)
def test_comment_doesnt_send_email_to_contributor_that_self_commenting(self):
contributor_subscribed_credentials = {
"email": "contributor_subscribing@gov.id",
"password": "passwordtest"
}
contributor_subscribed = get_user_model().objects.create_user(
**contributor_subscribed_credentials,
name="Kontributor-subscribed",
is_contributor=True,
is_subscribing_to_material_comments=True
)
material = Materi.objects.create(title="Materi-subscribed", author="Tester",
uploader=contributor_subscribed, publisher="Kelas PMPL",
descriptions="Deskripsi Materi subscribed", status="APPROVE",
cover=self.cover, content=self.content)
url = "/materi/" + str(material.id) + "/"
self.client.login(**contributor_subscribed_credentials) # comment with the same user
prev_outbox_count = len(mail.outbox)
comment_content = "Test comment should not send email for self-comments"
self.client.post(
url, {"comment": comment_content})
current_outbox_count = len(mail.outbox)
# Comment notification email not sent
self.assertEqual(current_outbox_count, prev_outbox_count)
def test_detail_materi_contains_comment_count(self):
url = self.url
self.client.login(**self.contributor_credential)
......@@ -543,9 +655,9 @@ class DetailMateriTest(TestCase):
def test_review_models_can_create_new_object(self):
test = Review.objects.create(
username="saul", profile="121212", review="232323")
countData = Review.objects.all().count()
self.assertEqual(1, countData)
self.assertNotEqual(0, countData)
count = Review.objects.all().count()
self.assertEqual(1, count)
self.assertNotEqual(0, count)
self.assertEqual(test.__str__(), "saul")
self.assertNotEqual(test.__str__(), "userlain")
......@@ -590,14 +702,14 @@ class DetailMateriTest(TestCase):
def test_detail_materi_contains_review_count(self):
url = self.url
self.client.login(**self.contributor_credential)
review = "This is new review by Contributor"
response = self.client.get(url)
self.assertContains(response, "Review (0)")
self.client.post(
url, {"review": "This is new review by Contributor"})
url, {"review": review })
self.client.post(
url, {"review": "This is new review by Contributor"})
url, {"review": review})
response = self.client.get(url)
self.assertContains(response, "Review (2)")
......@@ -698,6 +810,95 @@ class DetailMateriTest(TestCase):
self.assertEqual(last_url, '/materi/%d/' % self.materi1.id)
self.assertEqual(status_code, 302)
def test_download_count_is_in_init_context(self):
context = {}
DetailMateriService.init_materi_download_count(context, self.materi1)
self.assertIn('materi_download_count', context.keys())
def test_download_count_is_integer(self):
context = {}
DetailMateriService.init_materi_download_count(context, self.materi1)
self.assertEqual(type(context['materi_download_count']), int)
def test_download_count_when_no_download(self):
context = {}
DetailMateriService.init_materi_download_count(context, self.materi1)
self.assertEqual(context['materi_download_count'], 0)
def test_download_count_when_single_download(self):
self.client.get(self.download_url1)
context = {}
DetailMateriService.init_materi_download_count(context, self.materi1)
self.assertEqual(context['materi_download_count'], 1)
def test_download_count_when_multiple_download(self):
self.client.get(self.download_url1)
self.client.get(self.download_url1)
self.client.get(self.download_url1)
context = {}
DetailMateriService.init_materi_download_count(context, self.materi1)
self.assertEqual(context['materi_download_count'], 3)
def test_different_material_has_different_download_count(self):
self.client.get(self.download_url1)
self.client.get(self.download_url1)
self.client.get(self.download_url1)
self.client.get(self.download_url2)