diff --git a/administration/templates/edit_kontributor.html b/administration/templates/edit_kontributor.html
deleted file mode 100644
index 47e16ef164f42dc12bc70358741ae0ca3a566c9d..0000000000000000000000000000000000000000
--- a/administration/templates/edit_kontributor.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{% extends 'administration/base_administrasi.html' %}
-{% load static %}
-
-{% block content %}
-<div class="card shadow mb-4">
-	<div class="card-header py-3">
-		<h6 class="m-0 font-weight-bold text-primary">
-			Edit {{ page_title }}
-		</h6>
-	</div>
-	<div class="card-body">
-		<form method="POST">
-			{% csrf_token %}
-			{{ item.name }}
-			<div class="status d-flex align-items-baseline">
-				{{ form.is_active }} <br> Active
-			</div>
-			<div class="">
-				<button class="btn-sm btn-primary rounded p-12" type="submit">
-					<i class="far fa-save" aria-hidden="true"></i>
-					Simpan
-				</button>
-			</div>
-		</form>
-	</div>
-</div>
-{% endblock %}
\ No newline at end of file
diff --git a/administration/templates/kelola_admin.html b/administration/templates/kelola_admin.html
index dd020c9771ca6b73161182317499809216bf6e16..320b37e2db12b7e9067ee3c81de0430cdff2e6c9 100644
--- a/administration/templates/kelola_admin.html
+++ b/administration/templates/kelola_admin.html
@@ -1,5 +1,4 @@
 {% extends 'administration/base_administrasi2.html' %}
-{% load static %}
 
 {% block title %}
 <title>Kelola Admin | Digipus</title>
diff --git a/administration/templates/registrasi_admin.html b/administration/templates/registrasi_admin.html
index 659c58865f762f7ffe3ca580fa47189fc9da113c..3a14f4caafaeb9bc7d7fde1fff17de5a17728972 100644
--- a/administration/templates/registrasi_admin.html
+++ b/administration/templates/registrasi_admin.html
@@ -1,290 +1,89 @@
-{% load static %}
-
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-
-  <meta charset="utf-8">
-  <meta http-equiv="X-UA-Compatible" content="IE=edge">
-  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
-  <meta name="description" content="">
-  <meta name="author" content="">
-
-  <title>Dasbor - Kelola Admin</title>
-
-  <!-- Custom fonts for this template -->
-  <link href="https://fonts.googleapis.com/css2?family=Poppins&display=swap" rel="stylesheet">
-
-  <!-- Custom styles for this template -->
-  <link rel="icon" type="image/png" href="{% static 'images/icons/logo.ico' %}" />
-  <link href="{% static 'css/sb-admin-2.min.css' %}" rel="stylesheet">
-  <link rel="stylesheet" href="{% static 'css/button.css' %}">
-
-  <!-- Custom styles for this page -->
-  <link href="{% static 'vendor/datatables/dataTables.bootstrap4.min.css' %}" rel="stylesheet">
-
-</head>
-
-<body id="page-top" style="font-family: 'Poppins', sans-serif;">
-
-  <!-- Page Wrapper -->
-  <div id="wrapper">
-
-    <!-- Sidebar -->
-    <ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
-
-      <!-- Sidebar - Brand -->
-      <a class="sidebar-brand d-flex align-items-center justify-content-center" href="{% url 'daftar_katalog' %}">
-        <div class="sidebar-brand-icon rotate-n-15">
+{% extends 'administration/base_administrasi2.html' %}
+
+{% block title %}
+<title>Dasbor - Kelola Admin</title>
+{% endblock %}
+
+{% block content %}
+  <h1 class="h3 mb-2 text-gray-800">Registrasi Admin</h1>
+  <div class="form-margin"></div>
+  <div class="container admin-page">
+    <div class="col-20">
+      <form id="add_form" method="POST" novalidate enctype="multipart/form-data">
+        {% csrf_token %}
+        <div class="col-md-6 admin-page">
+          <div class="fieldWrapper">
+            {{ form.name.errors }}
+            <label for="{{ form.subject.id_for_label }}">Nama:</label>
+            {{ form.name }}
+          </div>
         </div>
-        <div class="sidebar-brand-text mx-3">Digipus</div>
-      </a>
-
-      <!-- Divider -->
-      <hr class="sidebar-divider my-0">
-
-      <!-- Nav Item - Dashboard -->
-      <li class="nav-item">
-        <a class="nav-link" href="/administration/">
-          <span>Verifikasi Materi</span></a>
-      </li>
-
-      <li class="nav-item">
-        <a class="nav-link" href="#">
-          <span>Statistik Materi</span></a>
-      </li>
-
-      <li class="nav-item">
-        <a class="nav-link" href="/administration/laporan-materi/">
-          <span>Laporan Materi</span></a>
-      </li>
-
-      <!-- Divider -->
-      <hr class="sidebar-divider my-0">
-
-      <li class="nav-item">
-        <a class="nav-link" href="/administration/setting/verification/">
-          <span>Pengaturan Verifikasi</span></a>
-      </li>
-
-      <li class="nav-item">
-        <a class="nav-link" href="/administration/setting/category/">
-          <span>Pengaturan Kategori</span></a>
-      </li>
-
-      <!-- Divider -->
-      <hr class="sidebar-divider my-0">
-
-      <li class="nav-item">
-        <a class="nav-link" href="/administration/kelola-kontributor/">
-          <span>Kelola Kontributor</span></a>
-      </li>
-
-      <li class="nav-item">
-        <a class="nav-link" href="/administration/kelola-admin/">
-          <span>Kelola Admin</span></a>
-      </li>
-
-      <!-- Divider -->
-      <hr class="sidebar-divider my-0">
-
-    </ul>
-    <!-- End of Sidebar -->
-
-    <!-- Content Wrapper -->
-    <div id="content-wrapper" class="d-flex flex-column">
-
-      <!-- Main Content -->
-      <div id="content">
-
-        <!-- Topbar -->
-        <nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
-
-          <!-- Sidebar Toggle (Topbar) -->
-          <button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
-            <em class="fa fa-bars"></em>
-          </button>
-
-          <div class="sidebar-brand-text mx-3">Diskominfo Kota Depok</div>
-
-          <!-- Topbar Navbar -->
-          <ul class="navbar-nav ml-auto">
-
-            <li class="nav-item">
-                <a class="nav-link" href="/dashboard/">
-                    <span class="mr-2 d-none d-lg-inline text-gray-600 small">Administrasi</span>
-                </a>
-            </li>
-    
-            <li class="nav-item">
-                <a class="nav-link" href="/profil/">
-                    <span class="mr-2 d-none d-lg-inline text-gray-600 small">Profil</span>
-                </a>
-            </li>
-
-            <li class="nav-item">
-              <a class="nav-link" href="/logout/">
-                  <span class="mr-2 d-none d-lg-inline text-gray-600 small">Logout</span>
-              </a>
-            </li>
-    
-            <div class="topbar-divider d-none d-sm-block"></div>
-
-            <!-- Nav Item - User Information -->
-            <li class="nav-item dropdown no-arrow">
-              <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-toggle="dropdown"
-                aria-haspopup="true" aria-expanded="false">
-                <span class="mr-2 d-none d-lg-inline text-gray-600 small">{{ user.name }}</span>
-                {% if not user.default_profile_picture %}
-                <img class="img-profile rounded-circle" src="https://i.ibb.co/9wgPzyZ/default-image.png" alt="User profile picture">
-                {% else %}
-                <img class="img-profile rounded-circle" src="{{ user.profile_picture.url }}" alt="User profile picture">
-                {% endif %}
-              </a>
-            </li>
-
-          </ul>
-
-        </nav>
-        <!-- End of Topbar -->
-
-        <!-- Begin Page Content -->
-        <div class="container-fluid">
-
-          <!-- Page Heading -->
-          <h1 class="h3 mb-2 text-gray-800">Registrasi Admin</h1>
-          <div class="form-margin"></div>
-            <div class="container admin-page">
-                <div class="col-20">
-                    <form id="add_form" method="POST" novalidate enctype="multipart/form-data">
-                        {% csrf_token %}
-                        <div class="col-md-6 admin-page">
-                            <div class="fieldWrapper">
-                                {{ form.name.errors }}
-                                <label for="{{ form.subject.id_for_label }}">Nama:</label>
-                                {{ form.name }}
-                            </div>
-                        </div>
-                        <div class="col-md-6 admin-page">
-                            <div class="fieldWrapper">
-                                {{ form.instansi.errors }}
-                                <label for="{{ form.subject.id_for_label }}">Instansi:</label>
-                                {{ form.instansi }}
-                            </div>
-                        </div>
-                        <div class="col-md-6 admin-page">
-                            <div class="fieldWrapper">
-                                {{ form.nik.errors }}
-                                <label for="{{ form.subject.id_for_label }}">NIK:</label>
-                                {{ form.nik }}
-                            </div>
-                        </div>
-                        <div class="col-md-6 admin-page">
-                            <div class="fieldWrapper">
-                                {{ form.alamat.errors }}
-                                <label for="{{ form.subject.id_for_label }}">Alamat:</label>
-                                {{ form.alamat }}
-                            </div>
-                        </div>
-                        <div class="col-md-6 admin-page">
-                            <div class="fieldWrapper">
-                                {{ form.email.errors }}
-                                <label for="{{ form.subject.id_for_label }}">Email:</label>
-                                {{ form.email }}
-                            </div>
-                        </div>
-                        <div class="col-md-6 admin-page">
-                            <div class="fieldWrapper">
-                                {{ form.nomor_telpon.errors }}
-                                <label for="{{ form.subject.id_for_label }}">Nomor Telepon:</label>
-                                {{ form.nomor_telpon }}
-                            </div>
-                        </div>
-                        <div class="col-md-6 admin-page">
-                            <div class="fieldWrapper">
-                                {{ form.password.errors }}
-                                <label for="{{ form.subject.id_for_label }}">Kata Sandi:</label>
-                                {{ form.password }}
-                            </div>
-                        </div>
-                        <div class="col-md-6 admin-page">
-                            <div class="fieldWrapper">
-                                {{ form.password2.errors }}
-                                <label for="{{ form.subject.id_for_label }}">Ulang Kata Sandi:</label>
-                                {{ form.password2 }}
-                            </div>
-                        </div>
-                        
-                        <div class="form-margin"></div>
-                        <div class="col-md-6 admin-page">
-                            <div class="d-flex flex-row">
-                                <div class="p-2">
-                                    <button type="submit" class="btn btn-success btn-edit" style="background-color: #615CFD; border-color: #615CFD;">Simpan</button>
-                                </div>
-                                <div class="p-2">
-                                    <a href="/administrasi/kelola-admin" class="btn btn-secondary">Kembali Ke Kelola Admin</a>
-                                </div>
-                              </div>
-                            <div class="fieldWrapper">
-                               
-                            </div>
-                        </div>
-                    </form>
-                </div>
-            </div>
+        <div class="col-md-6 admin-page">
+          <div class="fieldWrapper">
+            {{ form.instansi.errors }}
+            <label for="{{ form.subject.id_for_label }}">Instansi:</label>
+            {{ form.instansi }}
+          </div>
+        </div>
+        <div class="col-md-6 admin-page">
+          <div class="fieldWrapper">
+            {{ form.nik.errors }}
+            <label for="{{ form.subject.id_for_label }}">NIK:</label>
+            {{ form.nik }}
+          </div>
+        </div>
+        <div class="col-md-6 admin-page">
+          <div class="fieldWrapper">
+            {{ form.alamat.errors }}
+            <label for="{{ form.subject.id_for_label }}">Alamat:</label>
+            {{ form.alamat }}
+          </div>
+        </div>
+        <div class="col-md-6 admin-page">
+          <div class="fieldWrapper">
+            {{ form.email.errors }}
+            <label for="{{ form.subject.id_for_label }}">Email:</label>
+            {{ form.email }}
+          </div>
+        </div>
+        <div class="col-md-6 admin-page">
+          <div class="fieldWrapper">
+            {{ form.nomor_telpon.errors }}
+            <label for="{{ form.subject.id_for_label }}">Nomor Telepon:</label>
+            {{ form.nomor_telpon }}
+          </div>
+        </div>
+        <div class="col-md-6 admin-page">
+          <div class="fieldWrapper">
+            {{ form.password.errors }}
+            <label for="{{ form.subject.id_for_label }}">Kata Sandi:</label>
+            {{ form.password }}
+          </div>
+        </div>
+        <div class="col-md-6 admin-page">
+          <div class="fieldWrapper">
+            {{ form.password2.errors }}
+            <label for="{{ form.subject.id_for_label }}">Ulang Kata Sandi:</label>
+            {{ form.password2 }}
+          </div>
         </div>
-        <!-- /.container-fluid -->
 
-      </div>
-      <!-- End of Main Content -->
+        <div class="form-margin"></div>
+        <div class="col-md-6 admin-page">
+          <div class="d-flex flex-row">
+            <div class="p-2">
+              <button type="submit" class="btn btn-success btn-edit"
+                style="background-color: #615CFD; border-color: #615CFD;">Simpan</button>
+            </div>
+            <div class="p-2">
+              <a href="/administration/kelola-admin" class="btn btn-secondary">Kembali Ke Kelola Admin</a>
+            </div>
+          </div>
+          <div class="fieldWrapper">
 
-      <!-- Footer -->
-      <footer class="sticky-footer bg-white">
-        <div class="container my-auto">
-          <div class="copyright text-center my-auto">
-            <span>Copyright &copy; Diskominfo Kota Depok 2020</span>
           </div>
         </div>
-      </footer>
-      <!-- End of Footer -->
-
+      </form>
     </div>
-    <!-- End of Content Wrapper -->
-
   </div>
-  <!-- End of Page Wrapper -->
-
-  <!-- Scroll to Top Button-->
-  <a class="scroll-to-top rounded" href="#page-top">
-    <em class="fas fa-angle-up"></em>
-  </a>
-
-  <!-- Bootstrap core JavaScript-->
-  <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
-    integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
-    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>
-
-  <!-- Core plugin JavaScript-->
-  <script src="https://code.jquery.com/jquery-3.5.0.min.js"
-    integrity="sha256-xNzN2a4ltkB44Mc/Jz3pT4iU1cmeR0FkXs4pru/JxaQ=" crossorigin="anonymous"></script>
-
-  <!-- Custom scripts for all pages-->
-  <script src="{% static 'js/sb-admin-2.min.js' %}"></script>
-
-  <!-- Page level plugins -->
-  <script src="{% static 'vendor/datatables/jquery.dataTables.min.js' %}"></script>
-  <script src="{% static 'vendor/datatables/dataTables.bootstrap4.min.js' %}"></script>
-
-  <!-- Page level custom scripts -->
-  <script src="{% static 'js/demo/datatables-demo.js' %}"></script>
-
-</body>
-
-</html>
\ No newline at end of file
+{% endblock %}
\ No newline at end of file
diff --git a/administration/views.py b/administration/views.py
index 43a42b877c2b712001de03d85c77c362a8fab0b7..52cb59f2a4a0e4656b0d3ad0113a5533270e3a20 100644
--- a/administration/views.py
+++ b/administration/views.py
@@ -9,9 +9,10 @@ from django.utils import timezone
 from administration.models import VerificationReport, VerificationSetting, DeletionHistory
 from administration.forms import CategoryForm, VerificationSettingForm, RegistrasiAdminForm, PeriodForm, EditAdminStatusForm, EditKontributorStatusForm
 from administration.services import StatisticService, DetailVerificationService, LaporanMateriService
-from app.models import Category, Materi, ViewStatistics, DownloadStatistics, Comment, Like, LaporanMateri
+from app.models import Category, Materi, ViewStatistics, DownloadStatistics, Comment, Like, LaporanMateri, AdminNotification
 from authentication.models import User
 from datetime import datetime
+from app.models import NotifikasiKontributor
 
 from administration.utils import generate_time_step
 
@@ -82,11 +83,17 @@ class DetailVerificationView(TemplateView):
         verif_report = VerificationReport(
             report=report, materi=materi, user=self.request.user, status=materi.get_status_display())
         verif_report.save()
+
+        new_kontributor_notif = NotifikasiKontributor(materi=materi, user=materi.uploader, feedback=materi.get_status_display())
+        new_kontributor_notif.save()
+
         return HttpResponseRedirect("/administration/")
 
 
     def get(self, request, *args, **kwargs):
         context = self.get_context_data(**kwargs)
+        if(request.user.is_admin):
+            AdminNotification.objects.filter(materi=context["materi_data"]).delete()
         return self.render_to_response(context=context)
 
 
@@ -421,7 +428,7 @@ class EditAdminStatusView(TemplateView):
 
 
 class EditKontributorStatusView(TemplateView):
-    template_name = "edit_kontributor.html"
+    template_name = "edit_admin.html"
 
     def dispatch(self, request, *args, **kwargs):
         if not request.user.is_authenticated or not request.user.is_admin:
@@ -583,6 +590,8 @@ def blok_materi(request, *args, **kwargs):
     if materi.status == "APPROVE" and LaporanMateri.objects.filter(is_rejected=False, materi_id=materi.id):
         materi.status = "BLOCKED"
         materi.save()
+        new_kontributor_notif = NotifikasiKontributor(materi=materi, user=materi.uploader, feedback=materi.get_status_display())
+        new_kontributor_notif.save()
 
     return HttpResponseRedirect(ADMINISTRATION_REPORT)
 
diff --git a/app/admin.py b/app/admin.py
index 8c38f3f3dad51e4585f3984282c2a4bec5349c1e..333a094883a54391f866114ba0b3d146ae5d181b 100644
--- a/app/admin.py
+++ b/app/admin.py
@@ -1,3 +1,5 @@
 from django.contrib import admin
+from .models import AdminNotification
 
 # Register your models here.
+admin.site.register(AdminNotification)
\ No newline at end of file
diff --git a/app/forms.py b/app/forms.py
index 266720e86f912083e2c1335923114cc5c21c4bd3..7f22c130a7bd62a0d90ceb8e4fc88037f0f33eeb 100644
--- a/app/forms.py
+++ b/app/forms.py
@@ -1,18 +1,22 @@
 from django import forms
 
-from app.models import Materi, Category, RatingContributor
+from app.models import Materi, Category, RatingContributor, GuestBook
 from authentication.models import User
 import datetime
 
+
 def year_choices():
-    return[(r,r) for r in range(2000, datetime.date.today().year+1)]
+    return[(r, r) for r in range(2000, datetime.date.today().year+1)]
+
 
 class UploadMateriForm(forms.ModelForm):
-    
-    categories = forms.ModelMultipleChoiceField(queryset=Category.objects.all(),widget=forms.CheckboxSelectMultiple(attrs={'style' : 'column-count:2'}),required=True)
-    release_year = forms.TypedChoiceField(coerce=int, choices=year_choices, initial=datetime.date.today().year)
-    yt_video_id = forms.CharField(label="Youtube Video Id", \
-    help_text="This is not required.<br>\
+
+    categories = forms.ModelMultipleChoiceField(queryset=Category.objects.all(
+    ), widget=forms.CheckboxSelectMultiple(attrs={'style': 'column-count:2'}), required=True)
+    release_year = forms.TypedChoiceField(
+        coerce=int, choices=year_choices, initial=datetime.date.today().year)
+    yt_video_id = forms.CharField(label="Youtube Video Id",
+                                  help_text="This is not required.<br>\
         Please insert only Youtube link videos! Take a note for the video id!<br>\
         Example : https://www.youtube.com/watch?v=DkJ-50GLi2I <br> has video id DkJ-50GLi2I", required=False)
 
@@ -29,14 +33,13 @@ class UploadMateriForm(forms.ModelForm):
             field.widget.attrs["class"] = "form-control"
             field.widget.attrs["placeholder"] = field.initial
             field.initial = ""
-        self.fields['categories'].widget.attrs.update({'class' : "native"})
-        
+        self.fields['categories'].widget.attrs.update({'class': "native"})
 
 
 class SuntingProfilForm(forms.ModelForm):
     class Meta:
         model = User
-        fields = ["email","name","instansi", "nik", "alamat", "nomor_telpon",
+        fields = ["email", "name", "instansi", "nik", "alamat", "nomor_telpon",
                   "profile_picture", "linkedin",
                   "facebook", "twitter", "instagram", "biography",
                   "is_subscribing_to_material_comments"]
@@ -50,7 +53,6 @@ class SuntingProfilForm(forms.ModelForm):
         if any(self.errors):
             key = list(self.errors)[0]
             self.fields[key].widget.attrs["autofocus"] = ""
-            
 
         self.fields["email"].widget.attrs["readonly"] = True
 
@@ -61,14 +63,43 @@ class RatingContributorForm(forms.ModelForm):
         fields = ['score', 'user', 'contributor']
         SCORE_CHOICE = (
             ('', 'Select score'),
-            ('1', '1'),  # First one is the value of select option and second is the displayed value in option
+            # First one is the value of select option and second is the displayed value in option
+            ('1', '1'),
             ('2', '2'),
             ('3', '3'),
             ('4', '4'),
             ('5', '5'),
         )
         widgets = {
-            'score': forms.Select(choices=SCORE_CHOICE, attrs={'class': 'form-control', 'id':'form-rating'},),
+            'score': forms.Select(choices=SCORE_CHOICE, attrs={'class': 'form-control', 'id': 'form-rating'},),
             'user': forms.HiddenInput(),
             'contributor': forms.HiddenInput()
         }
+
+
+class GuestBookForm(forms.models.ModelForm):
+    class Meta:
+        model = GuestBook
+        fields = ['name', 'job', 'gender']
+        gender_choices = (
+            ('Male', 'Male'),
+            ('Female', 'Female')
+        )
+        widgets = {
+            'name': forms.fields.TextInput(attrs={
+                'placeholder': 'Input your name',
+                'class': 'form-control input-lg'
+            }),
+            'job': forms.fields.TextInput(attrs={
+                'placeholder': 'Input your job',
+                'class': 'form-control input-lg'
+            }),
+            'gender': forms.fields.Select(choices=gender_choices, attrs={
+                'class': 'form-control input-lg'
+            })
+        }
+        error_messages = {
+            'name': {'required': 'Name is required'},
+            'job': {'required': 'Job is required'},
+            'gender': {'required': 'Gender is required'}
+        }
diff --git a/app/migrations/0028_adminnotification.py b/app/migrations/0028_adminnotification.py
new file mode 100644
index 0000000000000000000000000000000000000000..c50ac34f7e7c30fe27215374bd50cbc26aa45b04
--- /dev/null
+++ b/app/migrations/0028_adminnotification.py
@@ -0,0 +1,21 @@
+# Generated by Django 3.1 on 2020-10-31 14:46
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0027_readlater'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='AdminNotification',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('materi', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.materi')),
+            ],
+        ),
+    ]
diff --git a/app/migrations/0028_notifikasikontributor.py b/app/migrations/0028_notifikasikontributor.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b198645300bae7ee7dcf9dd16dc7119de709310
--- /dev/null
+++ b/app/migrations/0028_notifikasikontributor.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.1 on 2020-10-31 16:29
+
+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', '0027_readlater'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='NotifikasiKontributor',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('feedback', models.CharField(default='', max_length=20)),
+                ('materi', models.ForeignKey(max_length=120, 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)),
+            ],
+        ),
+    ]
diff --git a/app/migrations/0029_guestbook.py b/app/migrations/0029_guestbook.py
new file mode 100644
index 0000000000000000000000000000000000000000..57fc440f8a0ba208b54902b0259221c854158c99
--- /dev/null
+++ b/app/migrations/0029_guestbook.py
@@ -0,0 +1,22 @@
+# Generated by Django 3.1 on 2020-10-31 18:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0028_adminnotification'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='GuestBook',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.TextField(default='')),
+                ('job', models.TextField(default='')),
+                ('gender', models.CharField(default='Male', max_length=6)),
+            ],
+        ),
+    ]
diff --git a/app/migrations/0029_merge_20201101_0217.py b/app/migrations/0029_merge_20201101_0217.py
new file mode 100644
index 0000000000000000000000000000000000000000..69826d1afcfbd93ecf7643f8109e292879d785b4
--- /dev/null
+++ b/app/migrations/0029_merge_20201101_0217.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.1 on 2020-10-31 19:17
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0028_notifikasikontributor'),
+        ('app', '0028_adminnotification'),
+    ]
+
+    operations = [
+    ]
diff --git a/app/migrations/0030_merge_20201101_0621.py b/app/migrations/0030_merge_20201101_0621.py
new file mode 100644
index 0000000000000000000000000000000000000000..e4e725f5d7b212a3eec1de8bb51575762eb79218
--- /dev/null
+++ b/app/migrations/0030_merge_20201101_0621.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.1 on 2020-10-31 23:21
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0029_guestbook'),
+        ('app', '0028_notifikasikontributor'),
+    ]
+
+    operations = [
+    ]
diff --git a/app/migrations/0031_merge_20201101_1729.py b/app/migrations/0031_merge_20201101_1729.py
new file mode 100644
index 0000000000000000000000000000000000000000..e284b5ff2882614bf7c9f7fae48e11fcf2d18c4c
--- /dev/null
+++ b/app/migrations/0031_merge_20201101_1729.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.1 on 2020-11-01 10:29
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0030_merge_20201101_0621'),
+        ('app', '0029_merge_20201101_0217'),
+    ]
+
+    operations = [
+    ]
diff --git a/app/models.py b/app/models.py
index fb932f7546c54b0f22af0e02c9e1212f123b51f6..3386646a5856d39ed5eb4252cdb892f5c8057144 100644
--- a/app/models.py
+++ b/app/models.py
@@ -1,5 +1,6 @@
 import random
 import datetime
+import math
 
 from django.contrib.postgres import search
 from django.core.exceptions import ValidationError
@@ -25,9 +26,11 @@ def get_random_color():
     color = "%06x" % random.randint(0, 0xFFFFFF)
     return color
 
+
 def current_year():
     return datetime.date.today().year
 
+
 class Category(models.Model):
     name = models.CharField(max_length=20)
     description = models.TextField(blank=False, default="")
@@ -47,35 +50,47 @@ class MateriManager(models.Manager):
         if self.alive_only:
             return SoftDeletionQuerySet(self.model).filter(deleted_at=None)
         return SoftDeletionQuerySet(self.model)
-        
+
     def search(self, search_text):
-        search_vector = search.SearchVector("title", weight="A")
+        search_vector = None
+        for field, weight in Materi.SEARCH_INDEX:
+            if search_vector is None:
+                search_vector = search.SearchVector(field, weight=weight)
+            else:
+                search_vector += search.SearchVector(field, weight=weight)
+
         search_query = search.SearchQuery(search_text)
 
         search_rank = search.SearchRank(search_vector, search_query)
 
         search_result = (
-            self.get_queryset().filter(_search_vector=search_query).annotate(rank=search_rank).order_by("-rank")
+            self.get_queryset()
+            .filter(_search_vector=search_query)
+            .annotate(rank=search_rank)
+            .order_by("-rank")
         )
 
         return search_result
 
+
 class SoftDeletionQuerySet(models.query.QuerySet):
     def delete(self):
         return super(SoftDeletionQuerySet, self).update(deleted_at=timezone.now())
 
+
 class SoftDeleteModel(models.Model):
     deleted_at = models.DateTimeField(blank=True, null=True)
 
     all_objects = MateriManager(alive_only=False)
-    
+
     class Meta:
         abstract = True
-    
+
     def soft_delete(self):
         self.deleted_at = timezone.now()
         self.save()
 
+
 class Materi(SoftDeleteModel):
     cover = models.ImageField()
     content = models.FileField()
@@ -85,8 +100,9 @@ class Materi(SoftDeleteModel):
     publisher = models.CharField(max_length=30, default="Penerbit")
     release_year = models.IntegerField(default=current_year)
     pages = models.IntegerField(default=0)
-    descriptions = models.TextField(default="Deskripsi")    
-    status = models.CharField(max_length=30, choices=VERIFICATION_STATUS, default=VERIFICATION_STATUS[0][0])
+    descriptions = models.TextField(default="Deskripsi")
+    status = models.CharField(
+        max_length=30, choices=VERIFICATION_STATUS, default=VERIFICATION_STATUS[0][0])
     categories = models.ManyToManyField(Category)
     date_created = models.DateTimeField(default=timezone.now)
     date_modified = models.DateTimeField(auto_now=True)
@@ -94,13 +110,35 @@ class Materi(SoftDeleteModel):
 
     _search_vector = search.SearchVectorField(null=True, editable=False)
 
+    SEARCH_INDEX = (
+        ("title", "A"),
+        ("author", "A"),
+        ("publisher", "C"),
+        ("descriptions", "C"),
+        ("uploader", "D"),
+    )
+
     objects = MateriManager()
 
     def save(self, *args, **kwargs):
         super().save(*args, **kwargs)
+        search_index = {field: weight for (
+            field, weight) in Materi.SEARCH_INDEX}
+        if "update_fields" in kwargs:
+            is_search_index_updated = bool(
+                set(search_index.keys()) & set(kwargs["update_fields"])
+            )
+
+        if ("update_fields" not in kwargs) or (is_search_index_updated):
+            self._search_vector = None
+            for field, weight in search_index.items():
+                if self._search_vector is None:
+                    self._search_vector = search.SearchVector(
+                        field, weight=weight)
+                else:
+                    self._search_vector += search.SearchVector(
+                        field, weight=weight)
 
-        if "update_fields" not in kwargs or "_search_vector" not in kwargs["update_fields"]:
-            self._search_vector = search.SearchVector("title", weight="A")
             self.save(update_fields=["_search_vector"])
 
     @property
@@ -124,7 +162,7 @@ class Materi(SoftDeleteModel):
     def like_count(self):
         count = Like.objects.filter(materi=self).count()
         return count
-    
+
     @property
     def comment_count(self):
         count = Comment.objects.filter(materi=self).count()
@@ -135,6 +173,25 @@ class Materi(SoftDeleteModel):
         count = Review.objects.filter(materi=self).count()
         return count
 
+    @staticmethod
+    def earliest_materi_timestamp():
+        return Materi.objects.earliest('date_created').date_created.timestamp()
+
+    @property
+    def seconds_since_earliest_materi(self):
+        return self.date_created.timestamp() - Materi.earliest_materi_timestamp()
+
+    @property
+    def view_count(self):
+        count = ViewStatistics.objects.filter(materi=self).count()
+        return count
+
+    @property
+    def hot_score(self):
+        view_score = math.log(max(self.view_count, 1), 10)
+        time_score = self.seconds_since_earliest_materi / 604800  # 1 week
+        return round(view_score + time_score, 7)
+
     @property
     def is_like(self):
         like = False
@@ -148,7 +205,8 @@ class Comment(models.Model):
     profile = models.CharField(max_length=100, default=get_random_color)
     comment = models.CharField(max_length=240, default="comments")
     materi = models.ForeignKey(Materi, models.SET_NULL, null=True)
-    user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True)
+    user = models.ForeignKey(
+        User, on_delete=models.SET_NULL, blank=True, null=True)
     timestamp = models.DateTimeField(default=timezone.now)
 
     def __str__(self):
@@ -158,18 +216,20 @@ class Comment(models.Model):
     def like_count(self):
         count = LikeComment.objects.filter(comment=self).count()
         return count
-    
+
     @property
     def dislike_count(self):
         count = DislikeComment.objects.filter(comment=self).count()
         return count
 
+
 class Review(models.Model):
     username = models.CharField(max_length=100)
     profile = models.CharField(max_length=100, default=get_random_color)
     review = models.TextField(default="review")
     materi = models.ForeignKey(Materi, models.SET_NULL, null=True)
-    user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True)
+    user = models.ForeignKey(
+        User, on_delete=models.SET_NULL, blank=True, null=True)
     timestamp = models.DateTimeField(default=timezone.now)
 
     def __str__(self):
@@ -198,6 +258,7 @@ 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)
@@ -206,13 +267,16 @@ class SubmitVisitor(models.Model):
 
 
 class ViewStatistics(models.Model):
-    materi = models.ForeignKey(Materi, models.SET_NULL, null=True, related_name="baca")
+    materi = models.ForeignKey(
+        Materi, models.SET_NULL, null=True, related_name="baca")
     timestamp = models.DateTimeField(default=timezone.now)
 
 
 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")
+    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)
 
 
@@ -253,31 +317,57 @@ class Rating(models.Model):
 
 class RatingContributor(models.Model):
     timestamp = models.DateTimeField(auto_now=True)
-    score = models.PositiveIntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
-    contributor = models.ForeignKey(User, on_delete=models.CASCADE, related_name='contributor')
-    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user')
+    score = models.PositiveIntegerField(
+        validators=[MinValueValidator(1), MaxValueValidator(5)])
+    contributor = models.ForeignKey(
+        User, on_delete=models.CASCADE, related_name='contributor')
+    user = models.ForeignKey(
+        User, on_delete=models.CASCADE, related_name='user')
 
     def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
         if 1 <= self.score <= 5:
             super().save(force_insert, force_update, using, update_fields)
         else:
             raise ValidationError("Rating score must be integer between 1-5")
-    
+
     class Meta:
         unique_together = ["contributor", "user"]
 
 
 class LaporanMateri(models.Model):
-    materi = models.ForeignKey(Materi, on_delete=models.CASCADE, max_length=120)
+    materi = models.ForeignKey(
+        Materi, on_delete=models.CASCADE, max_length=120)
     user = models.ForeignKey(User, on_delete=models.CASCADE)
-    laporan = models.TextField(validators=[MinValueValidator(30), MaxValueValidator(120)], default="")
+    laporan = models.TextField(
+        validators=[MinValueValidator(30), MaxValueValidator(120)], default="")
     timestamp = models.DateTimeField(default=timezone.now)
     is_rejected = models.BooleanField(default=False)
 
+
+class GuestBook(models.Model):
+    name = models.TextField(default='')
+    job = models.TextField(default='')
+    gender = models.CharField(max_length=6, default="Male")
+
+
 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
+        unique_together = ["materi", "user"]
+
+
+class NotifikasiKontributor(models.Model):
+    materi = models.ForeignKey(
+        Materi, on_delete=models.CASCADE, max_length=120)
+    user = models.ForeignKey(User, on_delete=models.CASCADE)
+    feedback = models.CharField(max_length=20, default="")
+
+    def __str__(self):
+        return "Status: {}".format(self.feedback)
+
+
+class AdminNotification(models.Model):
+    materi = models.ForeignKey(Materi, on_delete=models.CASCADE)
diff --git a/app/services.py b/app/services.py
index 95cbb4a64bbb1d5920de6760245bff4d984bbfc9..57ffa31f66c27b3c71f9026a636b471406c62995 100644
--- a/app/services.py
+++ b/app/services.py
@@ -19,7 +19,7 @@ from pydrive.drive import GoogleDrive
 from app.forms import SuntingProfilForm
 from app.models import (Category, Comment, DislikeComment, DownloadStatistics,
                         Like, LikeComment, Materi, Rating, ReadLater,
-                        ViewStatistics)
+                        ViewStatistics, AdminNotification)
 from app.utils.fileManagementUtil import (get_random_filename,
                                           remove_image_exifdata)
 
@@ -29,16 +29,7 @@ class DafterKatalogService:
     @staticmethod
     def search_materi(get_search, lst_materi, url):
         url = url + "&search={0}".format(get_search)
-        lst_materi = (
-            lst_materi.search(get_search)
-                .filter(
-                Q(author__icontains=get_search)
-                | Q(uploader__name__icontains=get_search)
-                | Q(descriptions__icontains=get_search)
-                | Q(publisher__icontains=get_search)
-            )
-                .distinct()
-        )
+        lst_materi = lst_materi.search(get_search)
         return lst_materi, url
 
     @staticmethod
@@ -63,6 +54,9 @@ class DafterKatalogService:
             lst_materi = lst_materi.order_by('date_created')
         elif (get_sort == "terpopuler"):
             lst_materi = lst_materi.annotate(count=Count('like__id')).order_by('-count')
+        elif (get_sort == "terhangat"):
+            lst_materi = sorted(lst_materi, 
+                key=lambda t: (t.hot_score, t.date_created), reverse=True)
         elif (get_sort == "jumlah_unduh"):
             lst_materi = lst_materi.annotate(count=Count('unduh__id')).order_by('-count')
         elif (get_sort == "jumlah_tampilan"):
@@ -359,6 +353,7 @@ class UploadMateriService:
     @staticmethod
     def upload_materi(form, materi):
         materi.save()
+        AdminNotification.objects.create(materi=materi)
         kateg = form.cleaned_data["categories"]
         for i in kateg:
             materi.categories.add(i)
@@ -407,6 +402,7 @@ class UploadMateriService:
                     os.path.join(settings.MEDIA_ROOT,default_cover))
 
             materi.save()
+            AdminNotification.objects.create(materi=materi)
 
             for c in excel["Categories"][i].split(","):
                 materi.categories.add(categories.get(name=c))
@@ -432,6 +428,7 @@ class RevisiMateriService:
         materi.uploader = request.user
         materi.status = "REVISION"
         materi.save()
+        AdminNotification.objects.create(materi=materi)
         kateg = form.cleaned_data["categories"]
         for i in kateg:
             materi.categories.add(i)
diff --git a/app/static/app/css/guest_book.css b/app/static/app/css/guest_book.css
new file mode 100644
index 0000000000000000000000000000000000000000..681d4f1d307a052b538c5d576c5650f5362f0c09
--- /dev/null
+++ b/app/static/app/css/guest_book.css
@@ -0,0 +1,63 @@
+
+.form-style-6{
+	font: 95% Arial, Helvetica, sans-serif;
+	max-width: 500px;
+	margin: 25px auto;
+	padding: 16px;
+    background: #F7F7F7;
+    align-content: center;
+    justify-content: center;
+}
+.form-style-6 h1{
+	background: #43D1AF;
+	padding: 20px 0;
+	font-size: 140%;
+	font-weight: 300;
+	text-align: center;
+	color: #fff;
+	margin: -16px -16px 16px -16px;
+}
+.form-style-6 input[type="text"],
+.form-style-6 select 
+{
+	-webkit-transition: all 0.30s ease-in-out;
+	-moz-transition: all 0.30s ease-in-out;
+	-ms-transition: all 0.30s ease-in-out;
+	-o-transition: all 0.30s ease-in-out;
+	outline: none;
+	box-sizing: border-box;
+	-webkit-box-sizing: border-box;
+	-moz-box-sizing: border-box;
+	width: 100%;
+	background: #fff;
+	margin-bottom: 4%;
+	border: 1px solid #ccc;
+	color: #555;
+	font: 95% Arial, Helvetica, sans-serif;
+}
+.form-style-6 input[type="text"]:focus,
+.form-style-6 textarea:focus,
+.form-style-6 select:focus
+{
+	box-shadow: 0 0 5px #43D1AF;
+	padding: 3%;
+	border: 1px solid #43D1AF;
+}
+
+.form-style-6 button[type="submit"]{
+	box-sizing: border-box;
+	-webkit-box-sizing: border-box;
+	-moz-box-sizing: border-box;
+	width: 100%;
+	padding: 3%;
+	background: #1da1f2;
+	border-bottom: 2px solid #1da1f2;
+	border-top-style: none;
+	border-right-style: none;
+	border-left-style: none;	
+	color: #fff;
+}
+.form-style-6 input[type="submit"]:hover,
+.form-style-6 input[type="button"]:hover{
+	background: #2EBC99;
+}
\ No newline at end of file
diff --git a/app/templates/app/base_admin.html b/app/templates/app/base_admin.html
deleted file mode 100644
index ff080f96ee2d739e2d7837189a7f9df131341030..0000000000000000000000000000000000000000
--- a/app/templates/app/base_admin.html
+++ /dev/null
@@ -1,165 +0,0 @@
-{% load static %}
-
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-  <title>Digipus</title>
-  <meta charset="utf-8">
-  <meta http-equiv="X-UA-Compatible" content="IE=edge">
-  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
-  <meta name="description" content="">
-  <meta name="author" content="">
-
-  {% block title %}{% endblock %}
-
-  <!-- Custom fonts for this template -->
-  <link href="https://fonts.googleapis.com/css2?family=Poppins&display=swap" rel="stylesheet">
-
-  <!-- Custom styles for this template -->
-  <link rel="icon" type="image/png" href="{% static 'images/icons/logo.ico' %}" />
-  <link href="{% static 'css/sb-admin-2.min.css' %}" rel="stylesheet">
-  <link rel="stylesheet" href="{% static 'css/button.css' %}">
-
-  <!-- Custom styles for this page -->
-  <link href="{% static 'vendor/datatables/dataTables.bootstrap4.min.css' %}" rel="stylesheet">
-
-</head>
-
-<body id="page-top" style="font-family: 'Poppins', sans-serif;">
-
-  <!-- Page Wrapper -->
-  <div id="wrapper">
-
-    <!-- Sidebar -->
-    <ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
-
-      <!-- Sidebar - Brand -->
-      <a class="sidebar-brand d-flex align-items-center justify-content-center" href="{% url 'daftar_katalog' %}">
-        <div class="sidebar-brand-icon rotate-n-15">
-        </div>
-        <div class="sidebar-brand-text mx-3">Digipus</div>
-      </a>
-
-      <!-- Divider -->
-      <hr class="sidebar-divider my-0">
-
-      <!-- Nav Item - Dashboard -->
-      <li class="nav-item">
-        <a class="nav-link" href="/profil/">
-          <span>Halaman Profil</span></a>
-      </li>
-
-      <li class="nav-item">
-        <a class="nav-link" href="/sunting/">
-          <span>Sunting Profil</span></a>
-      </li>
-
-    </ul>
-    <!-- End of Sidebar -->
-
-    <!-- Content Wrapper -->
-    <div id="content-wrapper" class="d-flex flex-column">
-
-      <!-- Main Content -->
-      <div id="content">
-
-        <!-- Topbar -->
-        <nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
-            <!-- Sidebar Toggle (Topbar) -->
-            <button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
-              <em class="fa fa-bars"></em>
-            </button>
-            <div class="sidebar-brand-text mx-3">Diskominfo Kota Depok</div>
-            <!-- Topbar Navbar -->
-            <ul class="navbar-nav ml-auto">
-              <li class="nav-item">
-                  <a class="nav-link" href="/administration/">
-                      <span class="mr-2 d-none d-lg-inline text-gray-600 small">Administrasi</span>
-                  </a>
-              </li>
-              <li class="nav-item">
-                  <a class="nav-link" href="/profil/">
-                      <span class="mr-2 d-none d-lg-inline text-gray-600 small">Profil</span>
-                  </a>
-              </li>
-              <li class="nav-item">
-                <a class="nav-link" href="/logout/">
-                    <span class="mr-2 d-none d-lg-inline text-gray-600 small">Logout</span>
-                </a>
-              </li>
-              <div class="topbar-divider d-none d-sm-block"></div>
-              <!-- Nav Item - User Information -->
-              <li class="nav-item dropdown no-arrow">
-                <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-toggle="dropdown"
-                  aria-haspopup="true" aria-expanded="false">
-                  <span class="mr-2 d-none d-lg-inline text-gray-600 small">{{ user.name }}</span>
-                  {% if not user.default_profile_picture %}
-                  <img class="img-profile rounded-circle" src="https://i.ibb.co/9wgPzyZ/default-image.png" alt="User profile picture">
-                  {% else %}
-                  <img class="img-profile rounded-circle" src="{{ user.profile_picture.url }}" alt="User profile picture">
-                  {% endif %}
-                </a>
-              </li>
-            </ul>
-        </nav>
-        <!-- End of Topbar -->
-
-        <!-- Begin Page Content -->
-        <div class="container-fluid">
-            {% block content %}{% endblock %}
-        </div>
-        <!-- /.container-fluid -->
-
-      </div>
-      <!-- End of Main Content -->
-
-      <!-- Footer -->
-      <footer class="sticky-footer bg-white">
-        <div class="container my-auto">
-          <div class="copyright text-center my-auto">
-            <span>Copyright &copy; Diskominfo Kota Depok 2020</span>
-          </div>
-        </div>
-      </footer>
-      <!-- End of Footer -->
-
-    </div>
-    <!-- End of Content Wrapper -->
-
-  </div>
-  <!-- End of Page Wrapper -->
-
-  <!-- Scroll to Top Button-->
-  <a class="scroll-to-top rounded" href="#page-top">
-    <em class="fas fa-angle-up"></em>
-  </a>
-
-  <!-- Bootstrap core JavaScript-->
-  <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
-    integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
-    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>
-
-  <!-- Core plugin JavaScript-->
-  <script src="https://code.jquery.com/jquery-3.5.0.min.js"
-    integrity="sha256-xNzN2a4ltkB44Mc/Jz3pT4iU1cmeR0FkXs4pru/JxaQ=" crossorigin="anonymous"></script>
-
-  <!-- Custom scripts for all pages-->
-  <script src="{% static 'js/sb-admin-2.min.js' %}"></script>
-
-  <!-- Page level plugins -->
-  <script src="{% static 'vendor/datatables/jquery.dataTables.min.js' %}"></script>
-  <script src="{% static 'vendor/datatables/dataTables.bootstrap4.min.js' %}"></script>
-
-  <!-- Page level custom scripts -->
-  <script src="{% static 'js/demo/datatables-demo.js' %}"></script>
-
-</body>
-
-</html>
\ No newline at end of file
diff --git a/app/templates/app/base_dashboard.html b/app/templates/app/base_dashboard.html
index c74bd589eb745d75b89cb9f1560428d76b24e4fb..d7303650586965b4cbee33dabf2ca56c4b249556 100644
--- a/app/templates/app/base_dashboard.html
+++ b/app/templates/app/base_dashboard.html
@@ -23,6 +23,9 @@
 
     <!-- Custom styles for this page -->
     <link href="{% static 'vendor/datatables/dataTables.bootstrap4.min.css' %}" rel="stylesheet">
+
+    <!-- Bootstrap CSS -->
+
     {% block stylesheets %}{% endblock %}
     {% block scripts %}{% endblock %}
 </head>
@@ -69,6 +72,10 @@
         <em class="fas fa-angle-up"></em>
     </a>
 
+    <!-- Bootstrap scripts -->
+    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
+    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
 </body>
 
 </html>
\ No newline at end of file
diff --git a/app/templates/app/base_profile.html b/app/templates/app/base_profile.html
index 081dc2ef7b2c7c987d31d20c31301d3e81849d8b..f9975a5b732b99d096c2de0c44e19a9914156c02 100644
--- a/app/templates/app/base_profile.html
+++ b/app/templates/app/base_profile.html
@@ -71,6 +71,10 @@
         <em class="fas fa-angle-up"></em>
     </a>
 
+    <!-- Bootstrap scripts -->
+    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
+    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
 </body>
 
 </html>
\ No newline at end of file
diff --git a/app/templates/app/detail_materi.html b/app/templates/app/detail_materi.html
index c489843d40314a994c7a4e39dba192316ed30e46..e4a6562bfc0e9e5fb3220b783e0110e0a44e55cc 100644
--- a/app/templates/app/detail_materi.html
+++ b/app/templates/app/detail_materi.html
@@ -284,6 +284,11 @@ div.review {
                         <em class="align-self-center far fa-square" id="readLaterText"></em> Baca Nanti
                 </button>
                 {% endif %}
+                <button class="btn btn-book shadow-sm p-2 mr-2 bg-white rounded align-self-center"
+                    type="button" aria-haspopup="true" aria-expanded="false" data-toggle="modal"
+                    data-target="#reportModal">
+                    <em></em>Laporkan
+                </button>
             </div>
         </div>
     </div>
@@ -430,6 +435,12 @@ div.review {
                             {{ review.timestamp|naturaltime }}
                         </p>
                     </div>
+                    {% if user.is_admin %}
+                    <a class="ml-auto p-1 bd-highlight close"
+                        href="{% url 'delete-review' materi_data.id review.id %}">
+                        <span aria-hidden="true">&times;</span>
+                    </a>
+                    {% endif %}
                 </div>
                 <div  class = 'review'>
                 <p class="paragraph">{{review.review}}</p>
@@ -469,6 +480,51 @@ div.review {
         </div>
     </div>
 </div>
+
+<div class="modal fade" id="reportModal" tabindex="-1" role="dialog" aria-labelledby="reportModal"
+    aria-hidden="true">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="reportModal">
+                    {% if user.is_authenticated %}
+                        Laporkan Materi
+                    {% else %}
+                        Belum Login
+                    {% endif %}
+                </h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </div>
+            <form method="POST">
+            <div class="modal-body">
+                {% if user.is_authenticated %}
+                    <p>Judul materi: <b>{{ materi_data.title }}</b><br></p>
+                        {% csrf_token %}
+                        <div class="form-group">
+                            <textarea placeholder="Alasan laporan..." class="form-control" id="exampleFormControlTextarea1"
+                                rows="3" name="report" required></textarea>
+                        </div>
+                {% else %}
+                    Login untuk melaporkan materi
+                {% endif %}
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
+                    {% if user.is_authenticated %}
+                        <button type="submit" class="btn btn-primary">
+                            Laporkan Materi
+                    {% else %}
+                        <button type="button" class="btn btn-primary" onclick="window.location.href = '/login';">
+                        Login
+                    {% endif %}
+                </button>
+            </div>
+            </form>
+        </div>
+    </div>
+</div>
 {% endblock content %}
 {% block extra_scripts %}
 <script src="https://kit.fontawesome.com/bc2cedd6b2.js" crossorigin="anonymous"></script>
diff --git a/app/templates/app/includes/navbar_katalog_materi.html b/app/templates/app/includes/navbar_katalog_materi.html
index e96adc0a452cb125e17693498ff2cbe15609194c..75a4d36f5146998166720904d306a664a4e76f4a 100644
--- a/app/templates/app/includes/navbar_katalog_materi.html
+++ b/app/templates/app/includes/navbar_katalog_materi.html
@@ -1,15 +1,12 @@
   
-    <nav class="navbar navbar-expand-lg navbar-light  static-top shadow katalog-navbar"> <!-- Sidebar Toggle (Topbar) -->
-        <div class="sidebar-brand-text navbar-brand">Digipus</div>
+    <nav class="navbar navbar-expand-lg navbar-light sticky-top shadow katalog-navbar"> <!-- Sidebar Toggle (Topbar) -->
+        <a class="sidebar-brand-text navbar-brand" href="/">Digipus</a>
         <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarContent" aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation">
           <span class="navbar-toggler-icon"></span>
         </button>
-
+      
         <div class="collapse navbar-collapse" id="navbarContent">
           <ul class="navbar-nav ml-auto">
-            <li class="nav-item active">
-              <a class="nav-link" href="/">Home<span class="sr-only">(current)</span></a>
-            </li>
             <li class="nav-item">
               <a class="nav-link" href="/forum">Forum</a>
             </li>
@@ -19,18 +16,29 @@
             <li class="nav-item">
               <a class="nav-link" href="/most-contributor">Author</a>
             </li>
+            <li class="user-guide">
+              <a class="nav-link" href="/userguide/">User Guide</a>
+            </li>
             {% if not request.user.is_authenticated %}
               <li class="nav-item">
-                <a class="nav-link" href="/registrasi/umum">Registrasi Umum</a>
-              </li>
-              <li class="nav-item">
-                <a class="nav-link" href="/registrasi">Registrasi Kontributor</a>
-              </li>
-              <li class="nav-item">
-                <a class="nav-link" href="/login">Login</a>
+                <div class="dropdown">
+                  <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                  Registrasi</a>
+                  <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
+                    <a class="dropdown-item" href="/registrasi/umum">Umum</a>
+                    <a class="dropdown-item" href="/registrasi">Kontributor</a>
+                  </div>
+                </div>
               </li>
               <li class="nav-item">
-                <a class="nav-link" href="/login_admin">Login Admin</a>
+                <div class="dropdown">
+                  <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                  Login</a>
+                  <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
+                    <a class="dropdown-item" href="/login">Kontributor</a>
+                    <a class="dropdown-item" href="/login_admin">Admin</a>
+                  </div>
+                </div>
               </li>
             {% else %}
               {% if request.user.is_contributor %}
diff --git a/app/templates/app/includes/navigation.html b/app/templates/app/includes/navigation.html
index 956d34c1041f387dcea48b5b4febb479efe13637..d83b7829d02e5e243ca0730e6b6d9ff4d548af41 100644
--- a/app/templates/app/includes/navigation.html
+++ b/app/templates/app/includes/navigation.html
@@ -11,6 +11,28 @@
     <ul class="navbar-nav ml-auto">
 
         {% if request.user.is_contributor %}
+        <li class="nav-item">
+            <div class="dropdown">
+                {% if kontributor_notif %}
+                <a id="notifBtn" class="nav-link dropdown-toggle" data-toggle="dropdown" href="#"role="button" aria-haspopup="true" aria-expanded="false" style="color: #4e73df;">
+                Notifikasi</a>
+                <div class="dropdown-menu" aria-labelledby="notifBtn">
+                {% for notif in kontributor_notif %}
+                    <div class="dropdown-divider"></div>
+                    <a class="dropdown-item" style="white-space: break-spaces;" href="{% url 'dashboard' %}">Materi "{{notif.materi.title}}" <br>{{ notif }}</a>
+                    <div class="dropdown-divider"></div>
+                {% endfor %}
+                {% else %}
+                <a id="notifBtn" class="nav-link dropdown-toggle" data-toggle="dropdown" href="#"role="button" aria-haspopup="true" aria-expanded="false">
+                Notifikasi</a>
+                <div class="dropdown-menu" aria-labelledby="notifBtn">
+                    <a class="dropdown-item disabled" href="#" style="white-space: break-spaces;">Anda tidak memiliki notifikasi baru</a>
+                {% endif %}
+                    </div>
+                </a>
+            </div>
+        </li>
+        
         <li class="nav-item">
             <a class="nav-link" href="/dashboard/">
                 <span class="mr-2 d-none d-lg-inline text-gray-600 small">Dasbor</span>
diff --git a/app/templates/app/katalog_materi.html b/app/templates/app/katalog_materi.html
index 62736dfe989318cc88f0b122fb4ca01e737cc96a..703c9fe62fad1b77b69b87378af9e3f28be08eb5 100644
--- a/app/templates/app/katalog_materi.html
+++ b/app/templates/app/katalog_materi.html
@@ -66,6 +66,7 @@
                             </div>
 							<button type="submit" class="btn btn-cari">Cari</button>
                         </form>
+                        <p class="pageTitle">Tidak tahu cara menggunakan Digipus? Silahkan kunjungi <a href="/userguide/"> user guide ini</a></p>
                         <p class="pageTitle">Tidak menemukan materi yang kamu cari ? ajukan permintaan materi kami <a
                                 href="/req-materi">disini</a></p>
                         <p class="pageTitle">Ingin diskusi lebih mendalam? Silahkan kunjungi <a
@@ -98,16 +99,20 @@
                                 </h2>
                             </div>
 
-                            <div id="collapseOne" class="collapse" aria-labelledby="headingOne"
+                            <div id="collapseOne" class="collapse show" aria-labelledby="headingOne"
                                 data-parent="#accordionExample">
                                 <div class="card-body">
-                                    <ul>
-                                        {% for itemKategori in kategori_list %}
-                                        <li>
-                                            <a href="?kategori={{itemKategori.pk}}">{{itemKategori.name}}</a>
-                                        </li>
-                                        {% endfor %}
-                                    </ul>
+                                    {% if kategori_list.count != 0 %}
+                                        <ul>
+                                            {% for itemKategori in kategori_list %}
+                                            <li>
+                                                <a href="?kategori={{itemKategori.pk}}">{{itemKategori.name}}</a>
+                                            </li>
+                                            {% endfor %}
+                                        </ul>
+                                    {% else %}
+                                        <p class="text-center">Belum ada kategori</p>
+                                    {% endif %}
                                 </div>
                             </div>
                         </div>
@@ -134,6 +139,9 @@
                                         <li>
                                             <a href="?sort=terpopuler">terpopuler</a>
                                         </li>
+                                        <li>
+                                            <a href="?sort=terhangat">terhangat</a>
+                                        </li>
                                         <li>
                                             <a href="?sort=judul">judul</a>
                                         </li>
diff --git a/app/templates/guest_book.html b/app/templates/guest_book.html
new file mode 100644
index 0000000000000000000000000000000000000000..b2d32fce5267d65a487a12d6b0bc9612cee1a30c
--- /dev/null
+++ b/app/templates/guest_book.html
@@ -0,0 +1,31 @@
+{% 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, inital-scale=1">
+
+    <link rel="stylesheet" type="text/css" href="{% static 'app/css/guest_book.css' %}">
+    {% endblock header %}
+
+</head>
+
+<!-- Page Content -->
+{% block content %}
+<div class="form-group form-style-6" id="myForm">
+    <form method="POST">
+        <h3 style="text-align: center;">Buku Tamu Non Anggota</h3>
+        {% csrf_token %}
+        {{form}}
+        <button type="submit" class="btn btn-success">Submit</button>
+    </form>
+</div>
+{% endblock %}
+
+</html>
\ No newline at end of file
diff --git a/app/templates/sukses_admin.html b/app/templates/sukses_admin.html
index 40692fdb1abd598551b99f2a644c22352e9f63d1..b40fbd810a520ab9df96a05bbbb203c02a66419d 100644
--- a/app/templates/sukses_admin.html
+++ b/app/templates/sukses_admin.html
@@ -47,6 +47,23 @@
                     <span>
                       <a class="btn btn-primary main-content" href="/administration/">Halaman Administrasi</a>
                     </span>
+                    <div class="profile-margin"></div>
+                    {% if notifications%}
+                    <h3>Materi Baru</h3>
+                    {% endif %}
+                    <table class="table main-content">
+                      <tbody>
+                        {% for i in notifications %}
+                        <tr>
+                          <td>
+                            <a href="/administration/detail-verif/{{i.materi.id}}">
+                              Materi {{i.materi.title}} butuh verifikasi
+                            </a>
+                          </td>
+                        </tr>
+                        {% endfor %}
+                      </tbody>
+                    </table>
                 </div>
             </div>
         </div>
diff --git a/app/tests.py b/app/tests.py
index f428616f4b27e3277b84c471f51889e73dbddd05..bfcfcafa318de14089d70f7ac65a9c2a3586b3d9 100644
--- a/app/tests.py
+++ b/app/tests.py
@@ -1,3 +1,13 @@
+from webdriver_manager.chrome import ChromeDriverManager
+from selenium.webdriver.common.keys import Keys
+from selenium.webdriver.chrome.options import Options
+from selenium.common.exceptions import NoSuchElementException
+from selenium import webdriver
+from django.test import LiveServerTestCase
+import requests
+from statistics import mean
+import mock
+import pandas as pd
 import base64
 import datetime as dt
 import json
@@ -6,8 +16,10 @@ import random
 import re
 import tempfile
 import time
+import itertools
+from django.test import override_settings
 from datetime import datetime
-from io import StringIO
+from io import StringIO, BytesIO
 from time import sleep
 
 import mock
@@ -23,16 +35,17 @@ from django.contrib import messages as dj_messages
 from django.contrib.auth import get_user_model
 from django.core import mail, serializers
 from django.core.exceptions import PermissionDenied, ValidationError
+from django.core.files.uploadedfile import SimpleUploadedFile, InMemoryUploadedFile
 from django.core.files import File
-from django.core.files.uploadedfile import SimpleUploadedFile
 from django.core.management import call_command
 from django.db.utils import IntegrityError
+from pytz import timezone, UTC
 from django.test import (Client, RequestFactory, TestCase, TransactionTestCase,
                          override_settings)
 from django.urls import resolve, reverse
 from django.utils import timezone
 
-from app.forms import SuntingProfilForm, year_choices
+from app.forms import SuntingProfilForm, year_choices, GuestBookForm
 from app.utils.fileManagementUtil import (get_random_filename,
                                           remove_image_exifdata)
 from app.utils.PasswordValidator import PasswordPolicyValidator
@@ -40,27 +53,18 @@ from app.views import UploadMateriHTML, add_rating_materi
 
 from .models import (Category, Comment, DislikeComment, DownloadStatistics,
                      Like, LikeComment, Materi, Rating, RatingContributor,
-                     ReadLater, ReqMaterial, Review, ViewStatistics)
+                     ReadLater, ReqMaterial, Review, ViewStatistics, GuestBook, AdminNotification, NotifikasiKontributor)
 from .services import DetailMateriService
 from .views import (DaftarKatalog, DashboardKontributorView, DetailMateri,
                     KatalogPerKontributorView, MateriFavorite,
                     PasswordChangeViews, PostsView, ProfilView, ReqMateriView,
                     RevisiMateriView, SuksesLoginAdminView,
+                    LaporanMateri,
                     SuksesLoginKontributorView, SuntingProfilView,
                     UploadMateriExcelView, UploadMateriView, password_success)
 
 ERROR_403_MESSAGE = "Kamu harus login untuk mengakses halaman ini"
 
-from statistics import mean
-
-import requests
-from django.test import LiveServerTestCase
-from selenium import webdriver
-from selenium.common.exceptions import NoSuchElementException
-from selenium.webdriver.chrome.options import Options
-from selenium.webdriver.common.keys import Keys
-from webdriver_manager.chrome import ChromeDriverManager
-
 
 class DaftarKatalogTest(TestCase):
     def test_daftar_katalog_url_exist(self):
@@ -86,66 +90,20 @@ class DaftarKatalogTest(TestCase):
         resp = Materi.objects.get(id=materi.id)
         self.assertEqual(resp, materi)
 
-    def test_materi_model_generate_search_vector_after_save(self):
-        Materi(title="Eating book").save()
-
-        search_vector_new_materi = list(Materi.objects.values_list("_search_vector", flat=True))
-        expected_search_vector = ["'book':2A 'eat':1A"]
-
-        self.assertSequenceEqual(search_vector_new_materi, expected_search_vector)
-
-    def test_search_text_on_empty_database(self):
-        search_query = "test"
-
-        search_result = list(Materi.objects.search(search_query))
-        expected_search_result = []
-
-        self.assertSequenceEqual(search_result, expected_search_result)
-
-    def test_search_text_on_unmatched_data(self):
-        Materi(title="test satu sekarang").save()
-        Materi(title="test dua nanti").save()
-
-        search_query = "besok"
-
-        search_result = list(Materi.objects.search(search_query))
-        expected_search_result = []
-
-        self.assertSequenceEqual(search_result, expected_search_result)
-
-    def test_search_text_return_list_matched_by_rank(self):
-        materi_2 = Materi(title="ini lumayan cocok lumayan cocok")
-        materi_2.save()
-
-        materi_1 = Materi(title="ini sangat cocok sangat cocok sangat cocok")
-        materi_1.save()
-
-        materi_4 = Materi(title="ini tidak")
-        materi_4.save()
-
-        materi_3 = Materi(title="ini sedikit cocok")
-        materi_3.save()
-
-        search_query = "ini cocok"
-
-        search_result = list(Materi.objects.search(search_query))
-        expected_search_result = [materi_1, materi_2, materi_3]
-
-        self.assertSequenceEqual(search_result, expected_search_result)
 
 class DaftarKatalogSortingByJumlahUnduhTest(TestCase):
     def setUp(self):
         self.client = Client()
 
         self.contributor_credential = {
-        "email": "kontributor@gov.id",
-        "password": "passwordtest"
-            }
+            "email": "kontributor@gov.id",
+            "password": "passwordtest"
+        }
 
         self.contributor_credential_2 = {
-                "email": "kontributor2@gov.id",
-                "password": "passwordtest"
-            }
+            "email": "kontributor2@gov.id",
+            "password": "passwordtest"
+        }
 
         self.contributor = get_user_model().objects.create_user(
             **self.contributor_credential, name="Kontributor 1", is_contributor=True)
@@ -173,6 +131,7 @@ class DaftarKatalogSortingByJumlahUnduhTest(TestCase):
         response = self.client.get("/?sort=jumlah_unduh")
         self.assertRegex(str(response.content), rf'.*Materi 2.*Materi 1.*')
 
+
 class DaftarKatalogSortingByJumlahTampilanTest(TestCase):
     def setUp(self):
         self.contributor_credential = {
@@ -207,14 +166,171 @@ class DaftarKatalogSortingByJumlahTampilanTest(TestCase):
         response = self.client.get("/?sort=jumlah_tampilan")
         self.assertRegex(str(response.content), rf'.*Materi 2.*Materi 1.*')
 
+
+class DaftarKatalogSortingByTerhangatTest(TestCase):
+    @classmethod
+    def generate_view_materi(cls, materi, view_count):
+        for _ in itertools.repeat(None, view_count):
+            ViewStatistics.objects.create(materi=materi)
+
+    def get_displayed_materi_in_number(self):
+        response = self.client.get("/?sort=terhangat")
+        lst = [int(id) for id in re.findall(r"Materi\s(\d+)[^\d]",
+                                            response.content.decode())]
+        return lst
+
+    def setUp(self):
+        self.contributor_credential = {
+            "email": "kontributor@gov.id",
+            "password": id_generator()
+        }
+        self.contributor = get_user_model().objects.create_user(
+            name="Kontributor",
+            is_contributor=True,
+            **self.contributor_credential,
+        )
+        self.client = Client()
+        content = b"Test file"
+        self.cover = SimpleUploadedFile(
+            "cover.jpg",
+            content
+        )
+        self.content = SimpleUploadedFile(
+            "content.txt",
+            content
+        )
+
+        self.materi_data = {
+            "author": "Reyhan",
+            "uploader": self.contributor,
+            "publisher": "Publisher",
+            "descriptions": "Deskripsi Materi",
+            "status": "APPROVE",
+            "cover": self.cover,
+            "content": self.content,
+        }
+
+    def test_1_week_difference_give_1_hot_score_difference(self):
+        materi1 = Materi.objects.create(
+            title='Materi 1',
+            date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC),
+            **self.materi_data
+        )
+        materi2 = Materi.objects.create(
+            title='Materi 2',
+            date_created=datetime(2020, 10, 8, 7, 0, 0, tzinfo=UTC),
+            **self.materi_data
+        )
+        materi3 = Materi.objects.create(
+            title='Materi 3',
+            date_created=datetime(2020, 10, 15, 7, 0, 0, tzinfo=UTC),
+            **self.materi_data
+        )
+        self.generate_view_materi(materi1, 1)
+        self.generate_view_materi(materi2, 1)
+        self.generate_view_materi(materi3, 1)
+
+        self.assertAlmostEqual(materi3.hot_score - materi2.hot_score, 1)
+        self.assertAlmostEqual(materi2.hot_score - materi1.hot_score, 1)
+
+    def test_10_exponential_view_count_difference_give_1_hot_score_difference(self):
+        materi1 = Materi.objects.create(
+            title='Materi 1',
+            date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC),
+            **self.materi_data
+        )
+        materi2 = Materi.objects.create(
+            title='Materi 2',
+            date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC),
+            **self.materi_data
+        )
+        materi3 = Materi.objects.create(
+            title='Materi 3',
+            date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC),
+            **self.materi_data
+        )
+        self.generate_view_materi(materi1, 1)
+        self.generate_view_materi(materi2, 10)
+        self.generate_view_materi(materi3, 100)
+
+        self.assertAlmostEqual(materi3.hot_score - materi2.hot_score, 1)
+        self.assertAlmostEqual(materi2.hot_score - materi1.hot_score, 1)
+
+    def test_0_and_1_views_has_the_same_hot_score(self):
+        materi1 = Materi.objects.create(
+            title='Materi 1',
+            date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC),
+            **self.materi_data
+        )
+        materi2 = Materi.objects.create(
+            title='Materi 2',
+            date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC),
+            **self.materi_data
+        )
+        self.generate_view_materi(materi1, 0)
+        self.generate_view_materi(materi2, 1)
+
+        self.assertAlmostEqual(materi1.hot_score, materi2.hot_score)
+
+    def test_page_has_option_sort_by_hottest(self):
+        response = self.client.get("/")
+        self.assertIn("terhangat", response.content.decode())
+
+    def test_page_display_sort_by_hottest(self):
+        materi1 = Materi.objects.create(
+            title='Materi 1',
+            date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC),
+            **self.materi_data
+        )
+        materi2 = Materi.objects.create(
+            title='Materi 2',
+            date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC),
+            **self.materi_data
+        )
+        materi3 = Materi.objects.create(
+            title='Materi 3',
+            date_created=datetime(2020, 10, 8, 7, 0, 0, tzinfo=UTC),
+            **self.materi_data
+        )
+        materi4 = Materi.objects.create(
+            title='Materi 4',
+            date_created=datetime(2020, 10, 9, 7, 0, 0, tzinfo=UTC),
+            **self.materi_data
+        )
+        self.generate_view_materi(materi1, 11)
+        self.generate_view_materi(materi2, 10)
+        self.generate_view_materi(materi3, 1)
+        self.generate_view_materi(materi4, 1)
+
+        lst = self.get_displayed_materi_in_number()
+        self.assertEqual(lst, [4, 1, 3, 2])
+
+    def test_prefer_newest_materi_if_hot_score_is_same(self):
+        materi1 = Materi.objects.create(
+            title='Materi 1',
+            date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC),
+            **self.materi_data
+        )
+        materi2 = Materi.objects.create(
+            title='Materi 2',
+            date_created=datetime(2020, 10, 8, 7, 0, 0, tzinfo=UTC),
+            **self.materi_data
+        )
+        self.generate_view_materi(materi1, 10)
+        self.generate_view_materi(materi2, 1)
+
+        lst = self.get_displayed_materi_in_number()
+        self.assertEqual(lst, [2, 1])
+
+
 class DaftarKatalogSortingByJumlahKomentarTest(TestCase):
     def setUp(self):
         self.client = Client()
 
         self.contributor_credential = {
-        "email": "kontributor@gov.id",
-        "password": "passwordtest"
-            }
+            "email": "kontributor@gov.id",
+            "password": "passwordtest"
+        }
 
         self.contributor_credential_2 = {
             "email": "kontributor2@gov.id",
@@ -242,25 +358,27 @@ class DaftarKatalogSortingByJumlahKomentarTest(TestCase):
 
         url = f"/materi/{self.last_uploaded_material.id}/"
         self.client.login(**self.contributor_credential_2)
-        self.client.post(url, {"comment": "This is new comment by Kontributor 2"})
+        self.client.post(
+            url, {"comment": "This is new comment by Kontributor 2"})
 
     def test_sorting_by_jumlah_komentar(self):
         response = self.client.get("/?sort=jumlah_komentar")
         self.assertRegex(str(response.content), rf'.*Materi 2.*Materi 1.*')
 
+
 class DaftarKatalogPerKontributorTest(TestCase):
     def setUp(self):
         self.client = Client()
 
         self.contributor_credential = {
-                "email": "kontributor@gov.id",
-                "password": "passwordtest"
-            }
+            "email": "kontributor@gov.id",
+            "password": "passwordtest"
+        }
 
         self.contributor_credential_2 = {
-                "email": "kontributor2@gov.id",
-                "password": "passwordtest"
-            }
+            "email": "kontributor2@gov.id",
+            "password": "passwordtest"
+        }
 
         self.contributor = get_user_model().objects.create_user(
             **self.contributor_credential, name="Kontributor 1", is_contributor=True)
@@ -293,7 +411,8 @@ class DaftarKatalogPerKontributorTest(TestCase):
 
     def test_katalog_per_kontributor_using_katalog_per_kontributor_func(self):
         found = resolve(self.url)
-        self.assertEqual(found.func.__name__, KatalogPerKontributorView.as_view().__name__)
+        self.assertEqual(found.func.__name__,
+                         KatalogPerKontributorView.as_view().__name__)
 
     # def test_katalog_per_kontributor_show_daftar_materi_kontributor(self):
     #     response = self.client.get(self.url)
@@ -312,16 +431,17 @@ 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>'
+        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))
+        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))
+        self.assertNotIn(expected_content, re.sub(">\s*<", "><", html_content))
 
     def setUp(self):
         self.client = Client()
@@ -348,14 +468,14 @@ class DetailMateriTest(TestCase):
             "ExampleCover921.jpg", b"Test file")
         self.content = SimpleUploadedFile("ExampleFile921.pdf", b"Test file")
 
-        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.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) + "/"
@@ -365,14 +485,15 @@ class DetailMateriTest(TestCase):
         self.lcount_info_name = "Jumlah Like"
 
         self.materi_with_published_date = 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,
-                                                 date_modified=timezone.now(), date_created=timezone.now())
-        self.materi_with_published_date_url = "/materi/" + str(self.materi_with_published_date.id) + "/"
-        VerificationReport.objects.create(report='{"feedback": "Something", "kriteria": [{"title": "Kriteria 1", "status": true},' + \
-            ' {"title": "Kriteria 2", "status": true}, {"title": "Kriteria 3", "status": true}]}', \
-            timestamp="2020-10-09 06:21:33", status="Diterima", materi= self.materi_with_published_date, \
-            user=self.materi_with_published_date.uploader)
+                                                                publisher="Kelas SC", descriptions="Deskripsi Materi 1",
+                                                                status="APPROVE", cover=self.cover, content=self.content,
+                                                                date_modified=timezone.now(), date_created=timezone.now())
+        self.materi_with_published_date_url = "/materi/" + \
+            str(self.materi_with_published_date.id) + "/"
+        VerificationReport.objects.create(report='{"feedback": "Something", "kriteria": [{"title": "Kriteria 1", "status": true},' +
+                                          ' {"title": "Kriteria 2", "status": true}, {"title": "Kriteria 3", "status": true}]}',
+                                          timestamp="2020-10-09 06:21:33", status="Diterima", materi=self.materi_with_published_date,
+                                          user=self.materi_with_published_date.uploader)
 
     def test_detail_materi_url_exist(self):
         response = Client().get(self.url)
@@ -413,6 +534,14 @@ class DetailMateriTest(TestCase):
         self.assertIn("Anda belum menuliskan komentar",
                       response.context["error_message"])
 
+    def test_post_blank_report(self):
+        url = self.url
+        self.client.login(**self.anonymous_credential)
+        response = self.client.post(url, {"report": ""})
+        self.assertIn("error_message", response.context)
+        self.assertIn("Anda belum menuliskan komentar",
+                      response.context["error_message"])
+
     def test_comment_rendered_to_template(self):
         url = self.url
         self.client.login(**self.contributor_credential)
@@ -420,6 +549,13 @@ class DetailMateriTest(TestCase):
         response = Client().get(url)
         self.assertContains(response, "This is my new comment")
 
+    def test_report_rendered_to_template(self):
+        url = self.url
+        self.client.login(**self.contributor_credential)
+        self.client.post(url, {"report": "This is my new comment"})
+        response = Client().get(url)
+        self.assertEqual(response.status_code, 200)
+
     def test_comment_by_admin(self):
         url = self.url
         self.client.login(**self.admin_credential)
@@ -448,14 +584,17 @@ class DetailMateriTest(TestCase):
         self.client.get("/logout/")
         self.client.login(**self.anonymous_credential)
 
-        self.client.post(url_materi, {"comment": "This is new comment by Anonymous"})
-        comment = Comment.objects.get(comment="This is new comment by Anonymous").id
+        self.client.post(
+            url_materi, {"comment": "This is new comment by Anonymous"})
+        comment = Comment.objects.get(
+            comment="This is new comment by Anonymous").id
         response = self.client.get(url_materi)
         session_id = response.context["session_id"]
 
         payload = {"comment": comment, "session_id": session_id}
         self.client.post("/comment/dislike/", payload)
-        num_of_comment_dislikes = DislikeComment.objects.filter(comment=comment).count()
+        num_of_comment_dislikes = DislikeComment.objects.filter(
+            comment=comment).count()
         self.assertEqual(num_of_comment_dislikes, 0)
 
     def test_comment_liked_by_anonymous(self):
@@ -463,14 +602,17 @@ class DetailMateriTest(TestCase):
         self.client.get("/logout/")
         self.client.login(**self.anonymous_credential)
 
-        self.client.post(url_materi, {"comment": "This is new comment by Anonymous"})
-        comment = Comment.objects.get(comment="This is new comment by Anonymous").id
+        self.client.post(
+            url_materi, {"comment": "This is new comment by Anonymous"})
+        comment = Comment.objects.get(
+            comment="This is new comment by Anonymous").id
         response = self.client.get(url_materi)
         session_id = response.context["session_id"]
 
         payload = {"comment": comment, "session_id": session_id}
         self.client.post("/comment/like/", payload)
-        num_of_comment_likes = LikeComment.objects.filter(comment=comment).count()
+        num_of_comment_likes = LikeComment.objects.filter(
+            comment=comment).count()
         self.assertEqual(num_of_comment_likes, 0)
 
     def test_comment_undisliked_by_anonymous(self):
@@ -478,16 +620,19 @@ class DetailMateriTest(TestCase):
         self.client.get("/logout/")
         self.client.login(**self.anonymous_credential)
 
-        self.client.post(url_materi, {"comment": "This is new comment by Anonymous"})
-        comment = Comment.objects.get(comment="This is new comment by Anonymous")
+        self.client.post(
+            url_materi, {"comment": "This is new comment by Anonymous"})
+        comment = Comment.objects.get(
+            comment="This is new comment by Anonymous")
         response = self.client.get(url_materi)
         session_id = response.context["session_id"]
-        
+
         payload = {"comment": comment, "session_id": session_id}
         self.client.post("/comment/dislike/", payload)
-        
+
         self.client.post("/comment/dislike/", payload)
-        num_of_comment_dislikes = DislikeComment.objects.filter(comment=comment, session_id=session_id).count()
+        num_of_comment_dislikes = DislikeComment.objects.filter(
+            comment=comment, session_id=session_id).count()
         self.assertEqual(num_of_comment_dislikes, 0)
 
     def test_comment_unliked_by_anonymous(self):
@@ -495,16 +640,19 @@ class DetailMateriTest(TestCase):
         self.client.get("/logout/")
         self.client.login(**self.anonymous_credential)
 
-        self.client.post(url_materi, {"comment": "This is new comment by Anonymous"})
-        comment = Comment.objects.get(comment="This is new comment by Anonymous")
+        self.client.post(
+            url_materi, {"comment": "This is new comment by Anonymous"})
+        comment = Comment.objects.get(
+            comment="This is new comment by Anonymous")
         response = self.client.get(url_materi)
         session_id = response.context["session_id"]
-        
+
         payload = {"comment": comment, "session_id": session_id}
         self.client.post("/comment/like/", payload)
-        
+
         self.client.post("/comment/like/", payload)
-        num_of_comment_likes = LikeComment.objects.filter(comment=comment, session_id=session_id).count()
+        num_of_comment_likes = LikeComment.objects.filter(
+            comment=comment, session_id=session_id).count()
         self.assertEqual(num_of_comment_likes, 0)
 
     def test_comment_new_does_not_have_dislike(self):
@@ -514,10 +662,13 @@ class DetailMateriTest(TestCase):
 
         response = self.client.get(url_materi)
         session_id = response.context["session_id"]
-        self.client.post(url_materi, {"comment": "This is new comment by Anonymous"})
+        self.client.post(
+            url_materi, {"comment": "This is new comment by Anonymous"})
 
-        comment = Comment.objects.get(comment="This is new comment by Anonymous")
-        comment_dislike_counter = DislikeComment.objects.filter(comment=comment, session_id=session_id).count()
+        comment = Comment.objects.get(
+            comment="This is new comment by Anonymous")
+        comment_dislike_counter = DislikeComment.objects.filter(
+            comment=comment, session_id=session_id).count()
         self.assertEqual(comment_dislike_counter, 0)
 
     def test_comment_new_does_not_have_like(self):
@@ -527,10 +678,13 @@ class DetailMateriTest(TestCase):
 
         response = self.client.get(url_materi)
         session_id = response.context["session_id"]
-        self.client.post(url_materi, {"comment": "This is new comment by Anonymous"})
+        self.client.post(
+            url_materi, {"comment": "This is new comment by Anonymous"})
 
-        comment = Comment.objects.get(comment="This is new comment by Anonymous")
-        comment_like_counter = LikeComment.objects.filter(comment=comment, session_id=session_id).count()
+        comment = Comment.objects.get(
+            comment="This is new comment by Anonymous")
+        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):
@@ -542,12 +696,13 @@ class DetailMateriTest(TestCase):
             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)
+        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
+        # comment with other user
+        self.client.login(**self.contributor_credential)
 
         prev_outbox_count = len(mail.outbox)
 
@@ -569,12 +724,13 @@ class DetailMateriTest(TestCase):
             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)
+        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
+        # comment with other user
+        self.client.login(**self.contributor_credential)
 
         prev_outbox_count = len(mail.outbox)
 
@@ -599,12 +755,13 @@ class DetailMateriTest(TestCase):
             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)
+        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
+        # comment with the same user
+        self.client.login(**contributor_subscribed_credentials)
 
         prev_outbox_count = len(mail.outbox)
 
@@ -639,7 +796,8 @@ class DetailMateriTest(TestCase):
     def test_detail_materi_not_contains_form_comment(self):
         response = self.client.get(self.url)
         self.assertNotContains(response, "Beri komentar...")
-        self.assertContains(response, "Login terlebih dahulu untuk berkomentar")
+        self.assertContains(
+            response, "Login terlebih dahulu untuk berkomentar")
 
     def create_and_delete_comment(self, is_admin=False, is_contributor=False):
         url = self.url
@@ -658,7 +816,8 @@ class DetailMateriTest(TestCase):
 
     def test_delete_comments_by_admin(self):
         self.create_and_delete_comment(is_admin=True)
-        count = Comment.objects.all().filter(comment="This is new comment by Anonymous").count()
+        count = Comment.objects.all().filter(
+            comment="This is new comment by Anonymous").count()
         self.assertEqual(count, 0)
 
     def test_delete_comments_by_contributor(self):
@@ -667,7 +826,8 @@ class DetailMateriTest(TestCase):
         self.assertRaises(PermissionDenied)
         self.assertEqual(response.status_code, 403)
 
-        count = Comment.objects.all().filter(comment="This is new comment by Anonymous").count()
+        count = Comment.objects.all().filter(
+            comment="This is new comment by Anonymous").count()
         self.assertEqual(count, 1)
 
     def test_delete_comments_by_anonymous(self):
@@ -676,10 +836,10 @@ class DetailMateriTest(TestCase):
         self.assertRaises(PermissionDenied)
         self.assertEqual(response.status_code, 403)
 
-        count = Comment.objects.all().filter(comment="This is new comment by Anonymous").count()
+        count = Comment.objects.all().filter(
+            comment="This is new comment by Anonymous").count()
         self.assertEqual(count, 1)
 
-    
     def test_review_models_can_create_new_object(self):
         test = Review.objects.create(
             username="saul", profile="121212", review="232323")
@@ -726,6 +886,44 @@ class DetailMateriTest(TestCase):
         self.client.post(url, {"review": "This is new review by Anonymous"})
         response = self.client.get(url)
         self.assertContains(response, "Anonymous")
+
+    def create_and_delete_review(self, is_admin=False, is_contributor=False):
+        url = self.url
+        self.client.login(**self.admin_credential)
+        self.client.post(url, {"review": "A review by Anonymous"})
+        delete_url = "/review/delete/" + str(self.materi1.id) + "/" + str(
+            Review.objects.get(review="A review by Anonymous").id)
+        if is_admin:
+            self.client.login(**self.admin_credential)
+        if is_contributor:
+            self.client.login(**self.contributor_credential)
+        if not is_admin and not is_contributor:
+            self.client.login(**self.anonymous_credential)
+        response = self.client.get(delete_url)
+        return response
+    
+    def test_delete_review_by_admin(self):
+        self.create_and_delete_review(is_admin=True)
+        count = Review.objects.all().filter(review="A review by Anonymous").count()
+        self.assertEqual(count, 0)
+
+    def test_delete_review_by_contributor(self):
+        response = self.create_and_delete_review(is_contributor=True)
+
+        self.assertRaises(PermissionDenied)
+        self.assertEqual(response.status_code, 403)
+
+        count = Review.objects.all().filter(review="A review by Anonymous").count()
+        self.assertEqual(count, 1)
+
+    def test_delete_review_by_regular_user(self):
+        response = self.create_and_delete_review()
+
+        self.assertRaises(PermissionDenied)
+        self.assertEqual(response.status_code, 403)
+
+        count = Review.objects.all().filter(review="A review by Anonymous").count()
+        self.assertEqual(count, 1)
     
     def test_detail_materi_contains_review_count(self):
         url = self.url
@@ -735,7 +933,7 @@ class DetailMateriTest(TestCase):
         self.assertContains(response, "Review (0)")
 
         self.client.post(
-            url, {"review": review })
+            url, {"review": review})
         self.client.post(
             url, {"review": review})
         response = self.client.get(url)
@@ -749,7 +947,8 @@ class DetailMateriTest(TestCase):
     def test_detail_materi_not_contains_form_review(self):
         response = self.client.get(self.url)
         self.assertNotContains(response, "Beri Review")
-        self.assertContains(response, "Login terlebih dahulu untuk Membuat review")
+        self.assertContains(
+            response, "Login terlebih dahulu untuk Membuat review")
 
     def test_tombol_citasiAPA(self):
         response = self.client.get(self.url)
@@ -757,25 +956,27 @@ class DetailMateriTest(TestCase):
 
     def test_hasil_citasi_APA_materi_has_no_published_date(self):
         response = self.client.get(self.url)
-        expected = self.materi1.author + " . (n.d) . " + self.materi1.title + " . " + self.materi1.publisher	
+        expected = self.materi1.author + \
+            " . (n.d) . " + self.materi1.title + " . " + self.materi1.publisher
         self.assertIn(expected, response.context["citationAPA"])
         self.assertIn(expected,
                       response.context["citationAPA"])
 
     def test_hasil_citasi_APA_materi_has_published_date(self):
         response = self.client.get(self.materi_with_published_date_url)
-        published_date = self.materi_with_published_date.published_date.strftime("%Y-%m-%d %H:%M")	
-        expected = (	
-            self.materi_with_published_date.author	
-            + " . ("	
-            + published_date	
-            + ") . "	
-            + self.materi_with_published_date.title	
-            + " . "	
-            + self.materi_with_published_date.publisher	
-        )	
+        published_date = self.materi_with_published_date.published_date.strftime(
+            "%Y-%m-%d %H:%M")
+        expected = (
+            self.materi_with_published_date.author
+            + " . ("
+            + published_date
+            + ") . "
+            + self.materi_with_published_date.title
+            + " . "
+            + self.materi_with_published_date.publisher
+        )
         self.assertIn(expected, response.context["citationAPA"])
-    
+
     def test_citation_IEEE_button(self):
         response = self.client.get(self.url)
         self.assertContains(response, "Citate IEEE")
@@ -787,19 +988,19 @@ class DetailMateriTest(TestCase):
         current_month = current_date.strftime("%b")
         current_year = str(current_date.year)
 
-        expected = (	
-            "Agas, "	
-            + "Materi 1. "	
-            + "Kelas SC, n.d. "	
-            + "Accessed on: "	
-            + current_month	
-            + ". "	
-            + current_day	
-            + ", "	
-            + current_year	
-            + ". [Online]. "	
-            + "Available: http://testserver"	
-            + self.url	
+        expected = (
+            "Agas, "
+            + "Materi 1. "
+            + "Kelas SC, n.d. "
+            + "Accessed on: "
+            + current_month
+            + ". "
+            + current_day
+            + ", "
+            + current_year
+            + ". [Online]. "
+            + "Available: http://testserver"
+            + self.url
         )
         self.assertIn(expected, response.context["citationIEEE"])
 
@@ -809,7 +1010,8 @@ class DetailMateriTest(TestCase):
         current_day = str(current_date.day)
         current_month = current_date.strftime("%b")
         current_year = str(current_date.year)
-        published_date = self.materi_with_published_date.published_date.strftime('%Y')
+        published_date = self.materi_with_published_date.published_date.strftime(
+            '%Y')
 
         expected = "Agas, " + \
                    "Materi 1. " + \
@@ -879,10 +1081,11 @@ class DetailMateriTest(TestCase):
         context2 = {}
         DetailMateriService.init_materi_download_count(context1, self.materi1)
         DetailMateriService.init_materi_download_count(context2, self.materi2)
-        self.assertNotEqual(context1['materi_download_count'], context2['materi_download_count'])
+        self.assertNotEqual(
+            context1['materi_download_count'], context2['materi_download_count'])
         self.assertEqual(context1['materi_download_count'], 3)
         self.assertEqual(context2['materi_download_count'], 2)
-    
+
     def test_download_count_displayed_on_template_when_no_download(self):
         response = self.client.get(self.url)
         html = response.content.decode("utf-8")
@@ -923,16 +1126,20 @@ class DetailMateriTest(TestCase):
         dcount_materi1 = self.materi1.unduh.all().count()
         dcount_materi2 = self.materi2.unduh.all().count()
 
-        self.check_materi_info_in_html(self.dcount_info_name, dcount_materi1, html)
-        self.check_materi_info_not_in_html(self.dcount_info_name, dcount_materi2, html)
-        self.check_materi_info_in_html(self.dcount_info_name, dcount_materi2, html2)
-        self.check_materi_info_not_in_html(self.dcount_info_name, dcount_materi1, html2)
-    
+        self.check_materi_info_in_html(
+            self.dcount_info_name, dcount_materi1, html)
+        self.check_materi_info_not_in_html(
+            self.dcount_info_name, dcount_materi2, html)
+        self.check_materi_info_in_html(
+            self.dcount_info_name, dcount_materi2, html2)
+        self.check_materi_info_not_in_html(
+            self.dcount_info_name, dcount_materi1, html2)
+
     def test_like_count_displayed_on_template_when_no_like(self):
         response = self.client.get(self.url)
         html = response.content.decode("utf-8")
         self.check_materi_info_in_html(self.lcount_info_name, 0, html)
-    
+
     def test_like_count_displayed_on_template_when_single_like(self):
         payload = {
             'materi_id': self.materi1.id,
@@ -942,8 +1149,8 @@ class DetailMateriTest(TestCase):
 
         response = self.client.get(self.url)
         html = response.content.decode("utf-8")
-        self.check_materi_info_in_html(self.lcount_info_name, 1, html)        
-        
+        self.check_materi_info_in_html(self.lcount_info_name, 1, html)
+
     def test_like_count_displayed_on_template_when_multiple_like(self):
         payload1 = {
             'materi_id': self.materi1.id,
@@ -955,17 +1162,17 @@ class DetailMateriTest(TestCase):
         }
         self.client.post(self.like_url, payload1)
         self.client.post(self.like_url, payload2)
-        
+
         response = self.client.get(self.url)
         html = response.content.decode("utf-8")
         self.check_materi_info_in_html(self.lcount_info_name, 2, html)
-    
+
     def test_like_count_displayed_on_template_when_unlike(self):
         payload = {
             'materi_id': self.materi1.id,
             'session_id': "dummysession"
         }
-        
+
         # Like materi once
         self.client.post(self.like_url, payload)
         response = self.client.get(self.url)
@@ -978,6 +1185,7 @@ class DetailMateriTest(TestCase):
         html = response.content.decode("utf-8")
         self.check_materi_info_in_html(self.lcount_info_name, 0, html)
 
+
 class PostsViewTest(TestCase):
 
     @classmethod
@@ -1005,14 +1213,14 @@ class PostsViewTest(TestCase):
                 "comments": [],
             }
 
-            for j in range (LIKES_COUNT_PER_POST[i]):
+            for j in range(LIKES_COUNT_PER_POST[i]):
                 Like.objects.create(
-                    timestamp=timezone.now(), 
-                    materi=post, 
+                    timestamp=timezone.now(),
+                    materi=post,
                     session_id=("dummysession-" + str(i) + '-' + str(j))
                 )
                 time.sleep(0.1)
-            
+
         for i, post_id in enumerate(post_comment_group_dict):
             post = post_comment_group_dict[post_id]["data"]
 
@@ -1032,7 +1240,8 @@ class PostsViewTest(TestCase):
             "email": "user@email.com",
             "password": "justpass"
         }
-        cls.user = User.objects.create_user(**cls.user_credentials, is_contributor=True)
+        cls.user = User.objects.create_user(
+            **cls.user_credentials, is_contributor=True)
         cls.data = cls.generate_posts_data(cls.user)
 
     def _request_as_user(self):
@@ -1082,12 +1291,12 @@ class PostsViewTest(TestCase):
 
         self.assertRegex(
             str(response.content),
-            rf'.*(<div id="post-{posts[2]}">)' + \
-            rf'.*(<div id="post-{posts[2]}-comment-{comments[2][0]}">)' + \
-            rf'.*(<div id="post-{posts[2]}-comment-{comments[2][1]}">)' + \
-            rf'.*(<div id="post-{posts[2]}-comment-{comments[2][2]}">)' + \
-            rf'.*(<div id="post-{posts[1]}">)' + \
-            rf'.*(<div id="post-{posts[0]}">)' + \
+            rf'.*(<div id="post-{posts[2]}">)' +
+            rf'.*(<div id="post-{posts[2]}-comment-{comments[2][0]}">)' +
+            rf'.*(<div id="post-{posts[2]}-comment-{comments[2][1]}">)' +
+            rf'.*(<div id="post-{posts[2]}-comment-{comments[2][2]}">)' +
+            rf'.*(<div id="post-{posts[1]}">)' +
+            rf'.*(<div id="post-{posts[0]}">)' +
             rf'.*(<div id="post-{posts[0]}-comment-{comments[0][0]}">)'
         )
 
@@ -1099,9 +1308,11 @@ class PostsViewTest(TestCase):
 
             self.assertContains(
                 response,
-                '<span id="post-like-count-' + str(post.id) + '">' +  str(post.like_count) + '</span>',
+                '<span id="post-like-count-' +
+                str(post.id) + '">' + str(post.like_count) + '</span>',
             )
 
+
 class TemplateLoaderTest(TestCase):
     def test_template_loader_url_exist(self):
         url = "/test-page.html"
@@ -1154,13 +1365,13 @@ class UploadPageTest(TestCase):
         response = Client().get("/fake/")
         self.assertEqual(response.status_code, 404)
 
-    def test_upload_page_url_admin_doesnt_exist(self):        
+    def test_upload_page_url_admin_doesnt_exist(self):
         self.client.login(email="admin@gov.id",
                           password="admin")
         response = self.client.get("/unggah/")
         self.assertEqual(response.status_code, 403)
 
-    def test_upload_page_url_admin_cant_upload(self):        
+    def test_upload_page_url_admin_cant_upload(self):
         self.client.login(email="admin@gov.id",
                           password="admin")
         response = self.client.post("/unggah/")
@@ -1280,7 +1491,9 @@ class UploadExcelPageTest(TestCase):
 
         file_path = os.path.join(settings.MEDIA_ROOT, 'dummy.xlsx')
 
-        writer = pd.ExcelWriter(file_path, engine='xlsxwriter') #pylint: disable=abstract-class-instantiated
+        # pylint: disable=abstract-class-instantiated
+        writer = pd.ExcelWriter(
+            file_path, engine='xlsxwriter')
         data_frame.to_excel(writer, index=0)
         writer.save()
 
@@ -1291,14 +1504,15 @@ class UploadExcelPageTest(TestCase):
                           password="kontributor")
 
         field_lengths = {
-            'author':30,
-            'publisher':30,
+            'author': 30,
+            'publisher': 30,
         }
-        file_name, data_frame = self.create_dummy_excel(field_lengths=field_lengths)
+        file_name, data_frame = self.create_dummy_excel(
+            field_lengths=field_lengths)
 
         with open(file_name, 'rb') as fp:
             response = self.client.post("/unggah_excel/", {'excel': fp})
-        
+
         messages = list(dj_messages.get_messages(response.wsgi_request))
         msg_text = messages[0].message
 
@@ -1309,33 +1523,34 @@ class UploadExcelPageTest(TestCase):
                           password="kontributor")
 
         field_lengths = {
-            'title':50,
-            'publisher':30,
+            'title': 50,
+            'publisher': 30,
         }
-        file_name, data_frame = self.create_dummy_excel(field_lengths=field_lengths)
+        file_name, data_frame = self.create_dummy_excel(
+            field_lengths=field_lengths)
 
         with open(file_name, 'rb') as fp:
             response = self.client.post("/unggah_excel/", {'excel': fp})
-        
+
         messages = list(dj_messages.get_messages(response.wsgi_request))
         msg_text = messages[0].message
 
         self.assertIn('Author', msg_text)
 
-
     def test_upload_excel_upload_file_publisher_error(self):
         self.client.login(email="kontributor@gov.id",
                           password="kontributor")
 
         field_lengths = {
-            'title':50,
-            'author':30,
+            'title': 50,
+            'author': 30,
         }
-        file_name, data_frame = self.create_dummy_excel(field_lengths=field_lengths)
+        file_name, data_frame = self.create_dummy_excel(
+            field_lengths=field_lengths)
 
         with open(file_name, 'rb') as fp:
             response = self.client.post("/unggah_excel/", {'excel': fp})
-        
+
         messages = list(dj_messages.get_messages(response.wsgi_request))
         msg_text = messages[0].message
 
@@ -1346,15 +1561,16 @@ class UploadExcelPageTest(TestCase):
                           password="kontributor")
 
         field_lengths = {
-            'title':50,
-            'author':30,
-            'publisher':30,
+            'title': 50,
+            'author': 30,
+            'publisher': 30,
         }
-        file_name, data_frame = self.create_dummy_excel(field_lengths=field_lengths)
+        file_name, data_frame = self.create_dummy_excel(
+            field_lengths=field_lengths)
 
         with open(file_name, 'rb') as fp:
             response = self.client.post("/unggah_excel/", {'excel': fp})
-        
+
         messages = list(dj_messages.get_messages(response.wsgi_request))
         msg_text = messages[0].message
 
@@ -1369,37 +1585,38 @@ class UploadExcelPageTest(TestCase):
         Category(name='Deep Learning').save()
 
         field_lengths = {
-            'title':50,
-            'author':30,
-            'publisher':30,
+            'title': 50,
+            'author': 30,
+            'publisher': 30,
         }
 
-        categories = ['Computer Science','Machine Learning','Deep Learning']
-        
-        file_name, data_frame = self.create_dummy_excel(field_lengths=field_lengths, categories=categories)
+        categories = ['Computer Science', 'Machine Learning', 'Deep Learning']
+
+        file_name, data_frame = self.create_dummy_excel(
+            field_lengths=field_lengths, categories=categories)
 
         with open(file_name, 'rb') as fp:
             self.client.post("/unggah_excel/", {'excel': fp})
-        
+
         title = data_frame['Title'][0]
         materi = Materi.objects.get(title=title)
         default_path = 'book-cover-placeholder.png'
 
         self.assertTrue(materi)
         self.assertEquals(materi.cover.name, default_path)
-        self.assertTrue(os.path.exists(os.path.join(settings.MEDIA_ROOT, default_path)))
+        self.assertTrue(os.path.exists(
+            os.path.join(settings.MEDIA_ROOT, default_path)))
 
     def test_upload_excel_download_template(self):
         self.client.login(email="kontributor@gov.id",
                           password="kontributor")
 
         response = self.client.get("/unggah_excel/?template=1")
-        
-        self.assertEquals(response['Content-Type'],'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
-        self.assertEquals(response['Content-Disposition'],'attachment; filename=template.xlsx')
-
-        
 
+        self.assertEquals(
+            response['Content-Type'], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+        self.assertEquals(response['Content-Disposition'],
+                          'attachment; filename=template.xlsx')
 
 
 class DashboardKontributorViewTest(TestCase):
@@ -1469,6 +1686,7 @@ class DashboardKontributorViewTest(TestCase):
         html = response.content.decode("utf-8")
         self.assertIn(ERROR_403_MESSAGE, html)
 
+
 class DeleteMateriTest(TestCase):
     def setUp(self):
         self.client = Client()
@@ -1494,7 +1712,8 @@ class DeleteMateriTest(TestCase):
             "content.txt", b"Test")
         self.cover = SimpleUploadedFile(
             "flower.jpg", b"Test file")
-        self.contributor = User.objects.create_contributor(**self.contributor_credential)
+        self.contributor = User.objects.create_contributor(
+            **self.contributor_credential)
         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()
@@ -1505,14 +1724,14 @@ class DeleteMateriTest(TestCase):
         self.client.login(**self.contributor_credential)
         response = self.client.get(self.url)
         self.assertEqual(response.status_code, 302)
-    
+
     def test_url_soft_delete_materi_is_success_as_admin(self):
         self.client.login(**self.admin_credential)
         response = self.client.get(self.url)
-        self.assertEqual(response.status_code, 302) 
+        self.assertEqual(response.status_code, 302)
         self.materi1.refresh_from_db()
         self.assertNotEqual(self.materi1.deleted_at, None)
-    
+
     def test_url_soft_delete_materi_is_success_as_superuser(self):
         self.client.login(**self.superuser_credential)
         response = self.client.get(self.url)
@@ -1520,6 +1739,7 @@ class DeleteMateriTest(TestCase):
         self.materi1.refresh_from_db()
         self.assertNotEqual(self.materi1.deleted_at, None)
 
+
 class ProfilViewTest(TestCase):
     @classmethod
     def setUpTestData(cls):
@@ -1528,10 +1748,13 @@ class ProfilViewTest(TestCase):
         cls.template_name = "profil.html"
         cls.view = ProfilView
 
-        cls.contributor_credentials = {"email": "contributor@gov.id", "password": "justpass"}
-        cls.contributor = User.objects.create_contributor(**cls.contributor_credentials)
+        cls.contributor_credentials = {
+            "email": "contributor@gov.id", "password": "justpass"}
+        cls.contributor = User.objects.create_contributor(
+            **cls.contributor_credentials)
 
-        cls.admin_credentials = {"email": "admin@gov.id", "password": "justpass"}
+        cls.admin_credentials = {
+            "email": "admin@gov.id", "password": "justpass"}
         cls.admin = User.objects.create_admin(**cls.admin_credentials)
 
     def test_returns_correct_profile_view(self):
@@ -1539,7 +1762,7 @@ class ProfilViewTest(TestCase):
         self.assertEqual(found.func.__name__, self.view.as_view().__name__)
 
     def _request_as_user(self, credentials):
-        self.client = Client() 
+        self.client = Client()
         self.client.login(**credentials)
         return self.client.get(self.url)
 
@@ -1570,10 +1793,13 @@ class SuntingProfilViewTest(TestCase):
         cls.template_name = "sunting.html"
         cls.view = SuntingProfilView
 
-        cls.contributor_credentials = {"email": "contributor@gov.id", "password": "justpass"}
-        cls.contributor = User.objects.create_contributor(**cls.contributor_credentials)
+        cls.contributor_credentials = {
+            "email": "contributor@gov.id", "password": "justpass"}
+        cls.contributor = User.objects.create_contributor(
+            **cls.contributor_credentials)
 
-        cls.admin_credentials = {"email": "admin@gov.id", "password": "justpass"}
+        cls.admin_credentials = {
+            "email": "admin@gov.id", "password": "justpass"}
         cls.admin = User.objects.create_admin(**cls.admin_credentials)
 
     def test_sunting_profile_view(self):
@@ -1581,7 +1807,7 @@ class SuntingProfilViewTest(TestCase):
         self.assertEqual(found.func.__name__, self.view.as_view().__name__)
 
     def _request_as_user(self, credentials):
-        self.client = Client() 
+        self.client = Client()
         self.client.login(**credentials)
         return self.client.get(self.url)
 
@@ -1616,8 +1842,10 @@ class SuntingProfilViewTest(TestCase):
         }
         form = SuntingProfilForm(data=form_data)
 
-        self.assertEqual(form.fields["twitter"].widget.attrs.get("autofocus"), "")
-        self.assertEqual(form.fields["instagram"].widget.attrs.get("autofocus"), None)
+        self.assertEqual(
+            form.fields["twitter"].widget.attrs.get("autofocus"), "")
+        self.assertEqual(
+            form.fields["instagram"].widget.attrs.get("autofocus"), None)
 
 
 class SuksesLoginKontributorTest(TestCase):
@@ -1962,7 +2190,7 @@ class RevisiMateriTest(TestCase):
         # Logout
         self.client.logout()
 
-    
+
 class GenerateDummyCommandTest(TestCase):
 
     def setUp(self):
@@ -1998,7 +2226,8 @@ class RemoveDummyCommandTest(TestCase):
 
         call_command("removedummy", stdout=stdout)
 
-        self.assertIn("Successfully remove all dummy object", stdout.getvalue())
+        self.assertIn("Successfully remove all dummy object",
+                      stdout.getvalue())
         self.assertEqual(User.objects.count(), 0)
         self.assertEqual(Category.objects.count(), 0)
         self.assertEqual(Materi.objects.count(), 0)
@@ -2023,8 +2252,10 @@ class RatingMateriTest(TestCase):
         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.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"
@@ -2050,17 +2281,20 @@ class RatingMateriTest(TestCase):
         self.assertEqual(rating.materi, self.materi1)
         self.assertEqual(rating.user, self.user_one)
         self.assertTrue(0 < rating.score < 6)
-        self.assertEqual(rating.__str__(), "Material:Materi 1 | User:User One | Rating:5")
+        self.assertEqual(rating.__str__(),
+                         "Material:Materi 1 | User:User One | Rating:5")
 
     def test_rating_model_should_not_be_created_with_rating_more_than_5(self):
         with self.assertRaises(ValidationError) as context:
             Rating(materi=self.materi1, user=self.user_one, score=6).save()
-        self.assertTrue('Rating score must be integer between 1-5' in str(context.exception))
+        self.assertTrue(
+            'Rating score must be integer between 1-5' in str(context.exception))
 
     def test_rating_model_should_not_be_created_with_rating_less_than_1(self):
         with self.assertRaises(ValidationError) as context:
             Rating(materi=self.materi1, user=self.user_one, score=0).save()
-        self.assertTrue('Rating score must be integer between 1-5' in str(context.exception))
+        self.assertTrue(
+            'Rating score must be integer between 1-5' in str(context.exception))
 
     def test_one_materi_should_be_able_to_be_related_to_multiple_rating(self):
         Rating(materi=self.materi1, user=self.user_one, score=1).save()
@@ -2112,7 +2346,8 @@ class RatingMateriTest(TestCase):
         self.assertEqual(response.status_code, 403)
 
     def test_rating_materi_post_not_authenticated_should_return_403_forbidden(self):
-        response = self.client.post(self.url_rate, {'materi_id': 1, 'rating_score': 5})
+        response = self.client.post(
+            self.url_rate, {'materi_id': 1, 'rating_score': 5})
         response_json = json.loads(response.content)
         self.assertEqual(response_json.get("success", None), False)
         self.assertEqual(response_json.get("msg", None), "Forbidden")
@@ -2137,52 +2372,65 @@ class RatingMateriTest(TestCase):
 
     def test_rating_materi_authenticated_materi_id_doesnt_exist_should_return_422(self):
         self.client.login(**self.user_one_credential)
-        response = self.client.post(self.url_rate, {'materi_id': 123456, 'rating_score': 5})
+        response = self.client.post(
+            self.url_rate, {'materi_id': 123456, 'rating_score': 5})
         response_json = json.loads(response.content)
         self.assertEqual(response_json.get("success", None), False)
-        self.assertEqual(response_json.get("msg", None), "Materi does not exist")
+        self.assertEqual(response_json.get(
+            "msg", None), "Materi does not exist")
         self.assertEqual(response.status_code, 422)
 
     def test_rating_materi_authenticated_param_wrong_data_type_should_return_422(self):
         self.client.login(**self.user_one_credential)
-        response = self.client.post(self.url_rate, {'materi_id': "STRING", 'rating_score': 5})
+        response = self.client.post(
+            self.url_rate, {'materi_id': "STRING", 'rating_score': 5})
         response_json = json.loads(response.content)
         self.assertEqual(response_json.get("success", None), False)
-        self.assertEqual(response_json.get("msg", None), "materi_id must be an integer")
+        self.assertEqual(response_json.get("msg", None),
+                         "materi_id must be an integer")
         self.assertEqual(response.status_code, 422)
 
-        response = self.client.post(self.url_rate, {'materi_id': 1, 'rating_score': "STRING"})
+        response = self.client.post(
+            self.url_rate, {'materi_id': 1, 'rating_score': "STRING"})
         response_json = json.loads(response.content)
         self.assertEqual(response_json.get("success", None), False)
-        self.assertEqual(response_json.get("msg", None), "rating_score must be an integer")
+        self.assertEqual(response_json.get("msg", None),
+                         "rating_score must be an integer")
         self.assertEqual(response.status_code, 422)
 
     def test_rating_score_should_be_between_1_and_5(self):
         self.client.login(**self.user_one_credential)
         for i in range(1, 6):
             Rating.objects.all().delete()
-            response = self.client.post(self.url_rate, {'materi_id': self.materi1.id, 'rating_score': i})
+            response = self.client.post(
+                self.url_rate, {'materi_id': self.materi1.id, 'rating_score': i})
             response_json = json.loads(response.content)
             # self.assertEqual(response_json.get("success", None), True)
-            self.assertEqual(response_json.get("msg", None), "Rating successfully created")
+            self.assertEqual(response_json.get("msg", None),
+                             "Rating successfully created")
             self.assertEqual(response.status_code, 201)
 
         for i in [-100, -7, -6, -1, 0, 6, 7, 100]:
             Rating.objects.all().delete()
-            response = self.client.post(self.url_rate, {'materi_id': self.materi1.id, 'rating_score': i})
+            response = self.client.post(
+                self.url_rate, {'materi_id': self.materi1.id, 'rating_score': i})
             response_json = json.loads(response.content)
             # self.assertEqual(response_json.get("success", None), False)
-            self.assertEqual(response_json.get("msg", None), "Rating must be an integer from 1 to 5")
+            self.assertEqual(response_json.get("msg", None),
+                             "Rating must be an integer from 1 to 5")
             self.assertEqual(response.status_code, 422)
 
     def test_user_should_not_able_to_rate_materi_twice(self):
         self.client.login(**self.user_one_credential)
         Rating.objects.all().delete()
-        self.client.post(self.url_rate, {'materi_id': self.materi1.id, 'rating_score': 1})
-        response = self.client.post(self.url_rate, {'materi_id': self.materi1.id, 'rating_score': 2})
+        self.client.post(
+            self.url_rate, {'materi_id': self.materi1.id, 'rating_score': 1})
+        response = self.client.post(
+            self.url_rate, {'materi_id': self.materi1.id, 'rating_score': 2})
         response_json = json.loads(response.content)
         # self.assertEqual(response_json.get("success", None), False)
-        self.assertEqual(response_json.get("msg", None), "Rating already exist")
+        self.assertEqual(response_json.get(
+            "msg", None), "Rating already exist")
         self.assertEqual(response.status_code, 409)
 
     def test_user_authenticated_visit_unrated_should_get_0_materi_rating_score_context(self):
@@ -2200,11 +2448,12 @@ class RatingMateriTest(TestCase):
         response = self.client.get(self.url_materi)
         self.assertEqual(1, response.context.get('materi_rating_score'))
 
+
 class FileManagementUtilTest(TestCase):
     def setUp(self):
         self.filename = "image_with_exif_data.gif"
         self.file_content = open(settings.BASE_DIR + "/app/test_files/" +
-                self.filename, "rb").read()
+                                 self.filename, "rb").read()
 
     def test_get_random_filename_isCorrect(self):
         generated_name = get_random_filename(self.filename)
@@ -2227,6 +2476,7 @@ class FileManagementUtilTest(TestCase):
             self.assertTrue(len(sanitized_img) < len(self.file_content))
             self.assertTrue(b'<exif:' not in sanitized_img)
 
+
 class RequestMateriTest(TestCase):
     def setUp(self):
         self.client = Client()
@@ -2278,15 +2528,16 @@ class RequestMateriTest(TestCase):
         self.client.login(**self.contributor_credential)
 
         response = self.client.post(self.url,
-                         data={
-                             'title': 'Requested Material'
-                         })
+                                    data={
+                                        'title': 'Requested Material'
+                                    })
         self.assertEqual(ReqMaterial.objects.count(), 1)
 
         new_material_request = ReqMaterial.objects.first()
         self.assertEqual(new_material_request.title, 'Requested Material')
 
-        self.assertIn('Permintaan materi berhasil dikirimkan', response.content.decode())
+        self.assertIn('Permintaan materi berhasil dikirimkan',
+                      response.content.decode())
         self.client.logout()
 
     def test_given_no_title_should_not_save_request_and_return_correct_response_message(self):
@@ -2333,62 +2584,77 @@ class RatingContributorTest(TransactionTestCase):
         self.url = f"/profil/{self.contributor.email}/"
 
     def test_add_rating_contributor(self):
-        RatingContributor.objects.create(score=3, contributor=self.contributor, user=self.anonymous)
+        RatingContributor.objects.create(
+            score=3, contributor=self.contributor, user=self.anonymous)
         self.assertEqual(1, RatingContributor.objects.count())
 
     def test_add_rating_contributor_should_failed_when_negative(self):
         with self.assertRaises(ValidationError):
-            RatingContributor.objects.create(score=-1, contributor=self.contributor, user=self.anonymous)
+            RatingContributor.objects.create(
+                score=-1, contributor=self.contributor, user=self.anonymous)
         self.assertEqual(0, RatingContributor.objects.count())
 
     def test_add_rating_contributor_should_failed_when_bigger_than_five(self):
         with self.assertRaises(ValidationError):
-            RatingContributor.objects.create(score=6, contributor=self.contributor, user=self.anonymous)
+            RatingContributor.objects.create(
+                score=6, contributor=self.contributor, user=self.anonymous)
         self.assertEqual(0, RatingContributor.objects.count())
 
     def test_submit_form_correct_rating_contributor_should_added(self):
         self.client.login(**self.anonymous_credential)
-        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.anonymous.id, "score": 5})
-        self.assertEqual(1, RatingContributor.objects.filter(contributor=self.contributor.id).count())
+        self.client.post(self.url, data={
+                         "contributor": self.contributor.id, "user": self.anonymous.id, "score": 5})
+        self.assertEqual(1, RatingContributor.objects.filter(
+            contributor=self.contributor.id).count())
         self.client.logout()
 
         self.client.login(**self.admin_credential)
-        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.admin.id,  "score": 1})
-        self.assertEqual(2, RatingContributor.objects.filter(contributor=self.contributor.id).count())
+        self.client.post(self.url, data={
+                         "contributor": self.contributor.id, "user": self.admin.id,  "score": 1})
+        self.assertEqual(2, RatingContributor.objects.filter(
+            contributor=self.contributor.id).count())
 
     def test_submit_form_not_correct_rating_contributor_should__not_added(self):
         self.client.login(**self.anonymous_credential)
-        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.anonymous.id, "score": 6})
-        self.assertEqual(0, RatingContributor.objects.filter(contributor=self.contributor.id).count())
+        self.client.post(self.url, data={
+                         "contributor": self.contributor.id, "user": self.anonymous.id, "score": 6})
+        self.assertEqual(0, RatingContributor.objects.filter(
+            contributor=self.contributor.id).count())
         self.client.logout()
 
         self.client.login(**self.admin_credential)
-        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.admin.id, "score": 0})
-        self.assertEqual(0, RatingContributor.objects.filter(contributor=self.contributor.id).count())
+        self.client.post(self.url, data={
+                         "contributor": self.contributor.id, "user": self.admin.id, "score": 0})
+        self.assertEqual(0, RatingContributor.objects.filter(
+            contributor=self.contributor.id).count())
 
     def test_average_rating_score_empty(self):
         response = self.client.get(self.url)
-        self.assertTemplateUsed(response=response, template_name="app/katalog_kontri.html")
+        self.assertTemplateUsed(
+            response=response, template_name="app/katalog_kontri.html")
         self.assertContains(response=response, text="Rating: 0", count=1)
         self.assertContains(response=response, text="oleh: 0 orang", count=1)
 
     def test_average_rating_correct(self):
         self.client.login(**self.anonymous_credential)
-        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.anonymous.id, "score": 5})
+        self.client.post(self.url, data={
+                         "contributor": self.contributor.id, "user": self.anonymous.id, "score": 5})
         response = self.client.get(self.url)
         self.assertContains(response=response, text="Rating: 5", count=1)
         self.assertContains(response=response, text="oleh: 1 orang", count=1)
 
     def test_average_rating_form_incorrect_correct(self):
         self.client.login(**self.anonymous_credential)
-        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.anonymous.id, "score": 6})
+        self.client.post(self.url, data={
+                         "contributor": self.contributor.id, "user": self.anonymous.id, "score": 6})
         response = self.client.get(self.url)
         self.assertContains(response=response, text="Rating: 0", count=1)
         self.assertContains(response=response, text="oleh: 0 orang", count=1)
         self.client.logout()
 
         self.client.login(**self.admin_credential)
-        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.admin.id, "score": -1})
+        self.client.post(self.url, data={
+                         "contributor": self.contributor.id, "user": self.admin.id, "score": -1})
         response = self.client.get(self.url)
         self.assertContains(response=response, text="Rating: 0", count=1)
         self.assertContains(response=response, text="oleh: 0 orang", count=1)
@@ -2397,10 +2663,13 @@ class RatingContributorTest(TransactionTestCase):
         score = 5
         avg = [score]
         self.client.login(**self.anonymous_credential)
-        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.anonymous.id, "score": score})
+        self.client.post(self.url, data={
+                         "contributor": self.contributor.id, "user": self.anonymous.id, "score": score})
         response = self.client.get(self.url)
-        self.assertContains(response=response, text=f"Rating: {mean(avg)}", count=1)
-        self.assertContains(response=response, text=f"oleh: {len(avg)} orang", count=1)
+        self.assertContains(response=response,
+                            text=f"Rating: {mean(avg)}", count=1)
+        self.assertContains(response=response,
+                            text=f"oleh: {len(avg)} orang", count=1)
         self.client.logout()
 
         self.anonymous2 = get_user_model().objects.create_user(
@@ -2409,10 +2678,13 @@ class RatingContributorTest(TransactionTestCase):
         score = 4
         avg.append(score)
         self.client.login(email=self.anonymous2.email, password="test")
-        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.anonymous2.id, "score": score})
+        self.client.post(self.url, data={
+                         "contributor": self.contributor.id, "user": self.anonymous2.id, "score": score})
         response = self.client.get(self.url)
-        self.assertContains(response=response, text=f"Rating: {mean(avg)}", count=1)
-        self.assertContains(response=response, text=f"oleh: {len(avg)} orang", count=1)
+        self.assertContains(response=response,
+                            text=f"Rating: {mean(avg)}", count=1)
+        self.assertContains(response=response,
+                            text=f"oleh: {len(avg)} orang", count=1)
         self.client.logout()
 
         self.anonymous3 = get_user_model().objects.create_user(
@@ -2421,10 +2693,13 @@ class RatingContributorTest(TransactionTestCase):
         score = 3
         avg.append(score)
         self.client.login(email=self.anonymous3.email, password="test")
-        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.anonymous3.id, "score": score})
+        self.client.post(self.url, data={
+                         "contributor": self.contributor.id, "user": self.anonymous3.id, "score": score})
         response = self.client.get(self.url)
-        self.assertContains(response=response, text=f"Rating: {mean(avg)}", count=1)
-        self.assertContains(response=response, text=f"oleh: {len(avg)} orang", count=1)
+        self.assertContains(response=response,
+                            text=f"Rating: {mean(avg)}", count=1)
+        self.assertContains(response=response,
+                            text=f"oleh: {len(avg)} orang", count=1)
         self.client.logout()
 
         self.anonymous4 = get_user_model().objects.create_user(
@@ -2433,120 +2708,131 @@ class RatingContributorTest(TransactionTestCase):
         score = 2
         avg.append(score)
         self.client.login(email=self.anonymous4.email, password="test")
-        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.anonymous4.id, "score": score})
+        self.client.post(self.url, data={
+                         "contributor": self.contributor.id, "user": self.anonymous4.id, "score": score})
         response = self.client.get(self.url)
-        self.assertContains(response=response, text=f"Rating: {mean(avg)}", count=1)
-        self.assertContains(response=response, text=f"oleh: {len(avg)} orang", count=1)
+        self.assertContains(response=response,
+                            text=f"Rating: {mean(avg)}", count=1)
+        self.assertContains(response=response,
+                            text=f"oleh: {len(avg)} orang", count=1)
         self.client.logout()
 
         score = 1
         avg.append(score)
         self.client.login(**self.admin_credential)
-        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.admin.id, "score": score})
+        self.client.post(self.url, data={
+                         "contributor": self.contributor.id, "user": self.admin.id, "score": score})
         response = self.client.get(self.url)
-        self.assertContains(response=response, text=f"Rating: {mean(avg)}", count=1)
-        self.assertContains(response=response, text=f"oleh: {len(avg)} orang", count=1)
-    
+        self.assertContains(response=response,
+                            text=f"Rating: {mean(avg)}", count=1)
+        self.assertContains(response=response,
+                            text=f"oleh: {len(avg)} orang", count=1)
+
     def test_not_authenticated_user_should_not_able_to_add_rating(self):
         response_post = self.client.post(self.url, data={
-            "contributor":self.contributor.id,
-            "user":self.anonymous.id,
-            "score":3
+            "contributor": self.contributor.id,
+            "user": self.anonymous.id,
+            "score": 3
         })
         response_get = self.client.get(self.url)
         self.assertEqual(response_post.status_code, 403)
-        self.assertContains(response_get, "Kamu harus login untuk memberi rating")
-    
+        self.assertContains(
+            response_get, "Kamu harus login untuk memberi rating")
+
     def test_not_authenticated_user_should_not_able_to_delete_rating(self):
-        response_post = self.client.post(self.url, data={"delete":True})
+        response_post = self.client.post(self.url, data={"delete": True})
         self.assertEqual(response_post.status_code, 403)
-    
+
     def test_authenticated_user_should_not_delete_rating_if_has_not_been_rated(self):
         self.client.login(**self.admin_credential)
         self.client.post(self.url, data={
-            "contributor":self.contributor.id,
-            "user":self.admin.id,
-            "score":3
+            "contributor": self.contributor.id,
+            "user": self.admin.id,
+            "score": 3
         })
-        
-        self.assertEqual(1, RatingContributor.objects.filter(contributor=self.contributor).count())
+
+        self.assertEqual(1, RatingContributor.objects.filter(
+            contributor=self.contributor).count())
         self.client.logout()
 
         self.client.login(**self.anonymous_credential)
-        self.client.post(self.url, data={"delete":True})
-        
-        self.assertEqual(1, RatingContributor.objects.filter(contributor=self.contributor).count())
-    
+        self.client.post(self.url, data={"delete": True})
+
+        self.assertEqual(1, RatingContributor.objects.filter(
+            contributor=self.contributor).count())
+
     def test_authenticated_user_should_delete_rating_if_has_been_rated(self):
         self.client.login(**self.admin_credential)
         self.client.post(self.url, data={
-            "contributor":self.contributor.id,
-            "user":self.admin.id,
-            "score":3
+            "contributor": self.contributor.id,
+            "user": self.admin.id,
+            "score": 3
         })
         response = self.client.get(self.url)
         self.assertContains(response, 'Rating: 3')
         self.assertContains(response, 'oleh: 1 orang')
-        self.assertEqual(1, RatingContributor.objects.filter(contributor=self.contributor).count())
+        self.assertEqual(1, RatingContributor.objects.filter(
+            contributor=self.contributor).count())
 
-        self.client.post(self.url, data={"delete":True})
+        self.client.post(self.url, data={"delete": True})
         response = self.client.get(self.url)
         self.assertContains(response, 'Rating: 0')
         self.assertContains(response, 'oleh: 0 orang')
-        self.assertEqual(0, RatingContributor.objects.filter(contributor=self.contributor).count())
-    
+        self.assertEqual(0, RatingContributor.objects.filter(
+            contributor=self.contributor).count())
+
     def test_average_still_be_correct_when_rating_was_deleted(self):
-        scores = [2,4]
+        scores = [2, 4]
         self.client.login(**self.admin_credential)
         self.client.post(self.url, data={
-            "contributor":self.contributor.id,
-            "user":self.admin.id,
-            "score":scores[0]
+            "contributor": self.contributor.id,
+            "user": self.admin.id,
+            "score": scores[0]
         })
         self.client.logout()
 
         self.client.login(**self.anonymous_credential)
         self.client.post(self.url, data={
-            "contributor":self.contributor.id,
-            "user":self.anonymous.id,
-            "score":scores[1]
+            "contributor": self.contributor.id,
+            "user": self.anonymous.id,
+            "score": scores[1]
         })
         response = self.client.get(self.url)
         self.assertContains(response, f'Rating: {mean(scores)}')
         self.assertContains(response, 'oleh: 2 orang')
 
-        self.client.post(self.url, data={"delete":True})
+        self.client.post(self.url, data={"delete": True})
         response = self.client.get(self.url)
         self.assertContains(response, f'Rating: {scores[0]}')
         self.assertContains(response, 'oleh: 1 orang')
-    
+
     def test_authenticated_user_should_update_rating_if_has_been_rated(self):
         self.client.login(**self.anonymous_credential)
         self.client.post(self.url, data={
-            "contributor":self.contributor.id,
-            "user":self.anonymous.id,
-            "score":5
+            "contributor": self.contributor.id,
+            "user": self.anonymous.id,
+            "score": 5
         })
         response = self.client.get(self.url)
         self.assertContains(response, 'Rating: 5')
         self.assertContains(response, 'oleh: 1 orang')
 
-        self.client.post(self.url, data={"update":True, "score":3})
+        self.client.post(self.url, data={"update": True, "score": 3})
         response = self.client.get(self.url)
         self.assertContains(response, 'Rating: 3')
         self.assertContains(response, 'oleh: 1 orang')
-    
+
     def test_authenticated_user_should_not_update_rating_if_has_not_been_rated(self):
         self.client.login(**self.anonymous_credential)
         response = self.client.get(self.url)
         self.assertContains(response, 'Rating: 0')
         self.assertContains(response, 'oleh: 0 orang')
 
-        self.client.post(self.url, data={"update":True, "score":3})
+        self.client.post(self.url, data={"update": True, "score": 3})
         response = self.client.get(self.url)
         self.assertContains(response, 'Rating: 0')
         self.assertContains(response, 'oleh: 0 orang')
-    
+
 
 class UserDownloadHistoryTest(TestCase):
     def setUp(self):
@@ -2585,26 +2871,30 @@ class UserDownloadHistoryTest(TestCase):
         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()
+        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()
+
+        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()
+        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()
+
+        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)
@@ -2613,19 +2903,19 @@ class UserDownloadHistoryTest(TestCase):
     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()
+            pk__in=downloaded_materi).count()
         self.assertEqual(num_of_downloads, 1)
 
     def test_registered_user_multiple_download(self):
@@ -2638,53 +2928,53 @@ class UserDownloadHistoryTest(TestCase):
         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()
+            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()
+            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
+
+        # 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()
+            pk__in=downloaded_materi).count()
         self.assertEqual(user1_num_of_downloads, 1)
         self.assertEqual(guest_num_of_downloads, 1)
-		
-		# Login Anonym 2
+
+        # 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()
@@ -2692,10 +2982,11 @@ class UserDownloadHistoryTest(TestCase):
         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 = {
@@ -2712,42 +3003,42 @@ class DownloadHistoryViewTest(TestCase):
             **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.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)
+                                             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)
-			   
+                                             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
+        # Forced Logout
         self.client.logout()
-		
+
         response = self.client.get(self.history_url)
         self.assertEqual(response.status_code, 200)
 
@@ -2759,14 +3050,14 @@ class DownloadHistoryViewTest(TestCase):
         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 = pytz.timezone(TIME_ZONE)
-		
+
         download_history = self.user_anonim.riwayat_unduh.all()
         response = self.client.get(self.history_url)
         resp_html = response.content.decode('utf8')
@@ -2774,19 +3065,20 @@ class DownloadHistoryViewTest(TestCase):
             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)
-			
+            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 = pytz.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']:
@@ -2794,19 +3086,20 @@ class DownloadHistoryViewTest(TestCase):
             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)
-			
+            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 = pytz.timezone(TIME_ZONE)
-		
+
         download_history = self.user_anonim.riwayat_unduh.all()
         response = self.client.get(self.history_url)
         resp_html = response.content.decode('utf8')
@@ -2814,25 +3107,26 @@ class DownloadHistoryViewTest(TestCase):
             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)
-			
+            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
+        # download with 1 second interval to differ download time
         self.client.get(self.download_url1)
         sleep(1)
         self.client.get(self.download_url2)
@@ -2840,23 +3134,25 @@ class DownloadHistoryViewTest(TestCase):
         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>"
+
+        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)
+            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)
@@ -2865,9 +3161,9 @@ class DownloadHistoryViewTest(TestCase):
 
 
 class MateriModelTest(TestCase):
-    
+
     def setUp(self):
-        self.contributor = User.objects.create( 
+        self.contributor = User.objects.create(
             email="kontributor@gov.id",
             password="passwordtest",
             name="kontributor",
@@ -2887,12 +3183,15 @@ class MateriModelTest(TestCase):
         self.assertEqual(0, self.materi.like_count)
 
     def test_like_count_return_right_value_when_there_is_like(self):
-        Like.objects.create(timestamp=timezone.now(), materi=self.materi, session_id="dummysessionid1")
+        Like.objects.create(timestamp=timezone.now(),
+                            materi=self.materi, session_id="dummysessionid1")
         self.assertEqual(1, self.materi.like_count)
 
-        Like.objects.create(timestamp=timezone.now(), materi=self.materi, session_id="dummysessionid2")
+        Like.objects.create(timestamp=timezone.now(),
+                            materi=self.materi, session_id="dummysessionid2")
         self.assertEqual(2, self.materi.like_count)
 
+
 class MateriFavoriteTest(TestCase):
     @classmethod
     def setUpTestData(cls):
@@ -2901,7 +3200,8 @@ class MateriFavoriteTest(TestCase):
             "email": "user@email.com",
             "password": "justpass"
         }
-        cls.user = User.objects.create_user(**cls.user_credentials, is_contributor=True)
+        cls.user = User.objects.create_user(
+            **cls.user_credentials, is_contributor=True)
 
     def _request_as_user(self):
         self.client.login(**self.user_credentials)
@@ -2909,7 +3209,9 @@ class MateriFavoriteTest(TestCase):
 
     def test_url_resolves_to_favorite_view(self):
         found = resolve(self.url)
-        self.assertEqual(found.func.__name__, MateriFavorite.as_view().__name__)
+        self.assertEqual(found.func.__name__,
+                         MateriFavorite.as_view().__name__)
+
 
 class RandomizedMateriTest(TestCase):
     def setUp(self):
@@ -2954,7 +3256,8 @@ class RandomizedMateriTest(TestCase):
         response = Client().get("/?random=1")
         self.assertIn("Materi 1", response.content.decode())
         self.assertIn("Materi 2", response.content.decode())
-    
+
+
 class YearChoicesTest(TestCase):
     def test_release_year_contains_the_right_current_year(self):
         now = dt.date.today().year
@@ -2968,6 +3271,7 @@ class YearChoicesTest(TestCase):
 
         self.assertEqual((2000, 2000), choices[0])
 
+
 TEST_IMAGE = '''
 iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI
 WXMAAABIAAAASABGyWs+AAAACXZwQWcAAAAQAAAAEABcxq3DAAABfElEQVQ4y52TvUuCURTGf5Zg
@@ -3007,25 +3311,26 @@ cpnR0WOUSiVEhLVKhbXXa7xcXqHyaoV6o0Hqd1MxUjqu7XYLMFkaNXtXYC09+R5UwbkYEcVaizFm
 P/LWGsLJydMs3VvCWkP3gzxK7OKu7Bl81/tEhKmpKVhYWNCJiQkNglDDMKdhLpf1/0AQhDo+Pq5z
 c3NKmqa6uLios7MXtFgsahRFGhUKHUS7KBQ0iiIdGhrS8+dndH5+XpMk0X8AMTVx/inpU4cAAAAl
 dEVYdGNyZWF0ZS1kYXRlADIwMTAtMTItMjZUMTQ6NDk6MjErMDk6MDAHHBB1AAAAJXRFWHRtb2Rp
-ZnktZGF0ZQAyMDEwLTEyLTI2VDE0OjQ5OjIxKzA5OjAwWK1mQQAAAABJRU5ErkJggg==
+ZnktZGF0ZQAyMDEwLTEyLTI2VDE0OjQ5OjIxKzA5OjAwWK1mQQAAAABJRU5ErkJggg ==
 '''.strip()
+
+
 class YTUrlVideoTest(TestCase):
     def setUp(self):
         self.client = Client()
-        self.contributor_credential = {"email": "kontributor@gov.id", "password": "passwordtest"}
+        self.contributor_credential = {
+            "email": "kontributor@gov.id", "password": "passwordtest"}
         self.contributor = get_user_model().objects.create_user(
             **self.contributor_credential, name="Kontributor", is_contributor=True
         )
 
         self.setUpImage()
         self.content = SimpleUploadedFile("ExampleFile221.pdf", b"Test file")
-        self.category = Category.objects.create(id="1", name="medis", description="kategori medis")
-    
+        self.category = Category.objects.create(
+            id="1", name="medis", description="kategori medis")
+
     @override_settings(MEDIA_ROOT=tempfile.gettempdir())
     def setUpImage(self):
-        from io import BytesIO
-
-        from django.core.files.uploadedfile import InMemoryUploadedFile
         self.cover = InMemoryUploadedFile(
             BytesIO(base64.b64decode(TEST_IMAGE)),
             field_name='tempfile',
@@ -3048,23 +3353,23 @@ class YTUrlVideoTest(TestCase):
     def test_upload_materi_with_valid_yt_video_id(self):
         self.client.login(**self.contributor_credential)
         self.client.post(
-            "/unggah/", data={"title":"Materi 1", "author":"Agas", "publisher":"Kelas SC", "release_year":"2000",
-                                "descriptions":"Deskripsi Materi 1", 'categories':"1",
-                                "cover":self.cover, "content":self.content,
-                                "yt_video_id":"jNwz4L9MGVY"}
+            "/unggah/", data={"title": "Materi 1", "author": "Agas", "publisher": "Kelas SC", "release_year": "2000",
+                              "descriptions": "Deskripsi Materi 1", 'categories': "1",
+                              "cover": self.cover, "content": self.content,
+                              "yt_video_id": "jNwz4L9MGVY"}
         )
         self.assertTrue(Materi.objects.get(title="Materi 1"))
-    
+
     def test_upload_materi_with_invalid_yt_video_id(self):
         self.client.login(**self.contributor_credential)
         self.client.post(
-            "/unggah/", data={"title":"Materi 2", "author":"Agas", "publisher":"Kelas SC", "release_year":"2000",
-                                "descriptions":"Deskripsi Materi 1", 'categories':"1",
-                                "cover":self.cover, "content":self.content,
-                                "yt_video_id":"randomId"}
+            "/unggah/", data={"title": "Materi 2", "author": "Agas", "publisher": "Kelas SC", "release_year": "2000",
+                              "descriptions": "Deskripsi Materi 1", 'categories': "1",
+                              "cover": self.cover, "content": self.content,
+                              "yt_video_id": "randomId"}
         )
         self.assertEqual(Materi.objects.filter(title="Materi 2").count(), 0)
-    
+
     def test_detail_materi_has_video_if_yt_video_id_not_empty(self):
         self.test_upload_materi_with_valid_yt_video_id()
         pk = Materi.objects.get(title="Materi 1").pk
@@ -3075,20 +3380,23 @@ class YTUrlVideoTest(TestCase):
     def test_detail_materi_has_no_video_if_yt_video_id_empty(self):
         self.client.login(**self.contributor_credential)
         self.client.post(
-            "/unggah/", data={"title":"Materi 2", "author":"Agas", "publisher":"Kelas SC", "release_year":"2000",
-                                "descriptions":"Deskripsi Materi 1", 'categories':"1",
-                                "cover":self.cover, "content":self.content,}
+            "/unggah/", data={"title": "Materi 2", "author": "Agas", "publisher": "Kelas SC", "release_year": "2000",
+                              "descriptions": "Deskripsi Materi 1", 'categories': "1",
+                              "cover": self.cover, "content": self.content, }
         )
         pk = Materi.objects.get(title="Materi 2").pk
         response = self.client.get("/materi/"+str(pk)+"/")
         html = response.content.decode("utf-8")
         self.assertNotIn("Video", html)
 
+
 class ChangePasswordTest(TestCase):
     def setUp(self):
         self.client = Client()
-        self.kontributor = User.objects.create_contributor(email="kontributor@gov.id", password="kontributor")
-        self.admin = User.objects.create_admin(email="admin@gov.id", password="admin")
+        self.kontributor = User.objects.create_contributor(
+            email="kontributor@gov.id", password="kontributor")
+        self.admin = User.objects.create_admin(
+            email="admin@gov.id", password="admin")
         self.url = "/change-password/"
         self.view = PasswordChangeViews
         self.template_name = "change-password.html"
@@ -3115,7 +3423,7 @@ class ChangePasswordTest(TestCase):
         # Logout
         self.client.logout()
 
-    
+
 class SeeRatedMateriByUser(TestCase):
     def setUp(self):
         self.client = Client()
@@ -3127,7 +3435,8 @@ class SeeRatedMateriByUser(TestCase):
             "email": "admin@gov.id",
             "password": id_generator()
         }
-        self.kontributor = User.objects.create_contributor(**self.contributor_credential)
+        self.kontributor = User.objects.create_contributor(
+            **self.contributor_credential)
         self.admin = User.objects.create_admin(**self.admin_credential)
         self.given_rating_url = "/given-rating/"
 
@@ -3152,13 +3461,17 @@ class SeeRatedMateriByUser(TestCase):
         self.materi2 = Materi.objects.all()[1]
         self.materi3 = Materi.objects.all()[2]
         time.sleep(5)
-        self.rating_test_1 = Rating(materi=self.materi1, user=self.kontributor, score=5)
+        self.rating_test_1 = Rating(
+            materi=self.materi1, user=self.kontributor, score=5)
         time.sleep(5)
-        self.rating_test_2 = Rating(materi=self.materi2, user=self.kontributor, score=4)
+        self.rating_test_2 = Rating(
+            materi=self.materi2, user=self.kontributor, score=4)
         time.sleep(5)
-        self.rating_test_3 = Rating(materi=self.materi3, user=self.kontributor, score=3)
+        self.rating_test_3 = Rating(
+            materi=self.materi3, user=self.kontributor, score=3)
         time.sleep(5)
-        self.rating_test_4 = Rating(materi=self.materi3, user=self.admin, score=3)
+        self.rating_test_4 = Rating(
+            materi=self.materi3, user=self.admin, score=3)
 
         self.rating_test_1.save()
         self.rating_test_2.save()
@@ -3198,12 +3511,14 @@ class SeeRatedMateriByUser(TestCase):
         self.assertNotIn(self.rating_test_4, response.context['rating_list'])
 
     def test_given_rating_page_no_rating_should_display_message(self):
-        user_credential = {"email": "user@mail.com", "password": id_generator()}
+        user_credential = {"email": "user@mail.com",
+                           "password": id_generator()}
         User.objects.create_contributor(**user_credential)
         self.client.login(**user_credential)
         response = self.client.get(self.given_rating_url)
         self.assertEqual(len(response.context['rating_list']), 0)
-        self.assertIn("Anda belum pernah memberikan rating ke materi", response.content.decode("utf-8"))
+        self.assertIn("Anda belum pernah memberikan rating ke materi",
+                      response.content.decode("utf-8"))
         self.assertNotIn(self.materi1.title, response.content.decode("utf-8"))
 
     def test_given_rating_page_order_should_give_default_parameter(self):
@@ -3219,24 +3534,28 @@ class SeeRatedMateriByUser(TestCase):
         self.assertEqual(response.context['order_by_key'], 'timestamp')
 
         # No order_by parameter, should default to asc
-        response = self.client.get(self.given_rating_url + '?order_by_key=score')
+        response = self.client.get(
+            self.given_rating_url + '?order_by_key=score')
         self.assertEqual(response.context['order_by'], 'dsc')
         self.assertEqual(response.context['order_by_key'], 'score')
 
         # INVALID PARAMETERS
         # Starts with negative
-        response = self.client.get(self.given_rating_url + '?order_by_key=-score')
+        response = self.client.get(
+            self.given_rating_url + '?order_by_key=-score')
         self.assertEqual(response.context['order_by'], 'dsc')
         self.assertEqual(response.context['order_by_key'], 'score')
 
         # Invalid params
-        response = self.client.get(self.given_rating_url + '?order_by_key=halohalohalo&order=haihaihaihai')
+        response = self.client.get(
+            self.given_rating_url + '?order_by_key=halohalohalo&order=haihaihaihai')
         self.assertEqual(response.context['order_by'], 'dsc')
         self.assertEqual(response.context['order_by_key'], 'timestamp')
 
     def test_given_rating_page_order_ascending_should_be_correct(self):
         self.client.login(**self.contributor_credential)
-        response = self.client.get(self.given_rating_url + '?order_by_key=score&order=asc')
+        response = self.client.get(
+            self.given_rating_url + '?order_by_key=score&order=asc')
 
         # From Low to High
         # order key score
@@ -3245,13 +3564,15 @@ class SeeRatedMateriByUser(TestCase):
         self.assertEqual(list(response.context['rating_list']),
                          [self.rating_test_3, self.rating_test_2, self.rating_test_1])
         # order key timestamp
-        response = self.client.get(self.given_rating_url + '?order_by_key=timestamp&order=asc')
+        response = self.client.get(
+            self.given_rating_url + '?order_by_key=timestamp&order=asc')
         self.assertEqual(response.context['order_by'], 'asc')
         self.assertEqual(response.context['order_by_key'], 'timestamp')
         self.assertEqual(list(response.context['rating_list']),
                          [self.rating_test_1, self.rating_test_2, self.rating_test_3])
         # order key materi title
-        response = self.client.get(self.given_rating_url + '?order_by_key=materi__title&order=asc')
+        response = self.client.get(
+            self.given_rating_url + '?order_by_key=materi__title&order=asc')
         self.assertEqual(response.context['order_by'], 'asc')
         self.assertEqual(response.context['order_by_key'], 'materi__title')
         self.assertEqual(list(response.context['rating_list']),
@@ -3261,26 +3582,30 @@ class SeeRatedMateriByUser(TestCase):
         self.client.login(**self.contributor_credential)
         # From High to Low
         # order key score
-        response = self.client.get(self.given_rating_url + '?order_by_key=score&order=dsc')
+        response = self.client.get(
+            self.given_rating_url + '?order_by_key=score&order=dsc')
         self.assertEqual(response.context['order_by'], 'dsc')
         self.assertEqual(response.context['order_by_key'], 'score')
         self.assertEqual(list(response.context['rating_list']),
                          [self.rating_test_1, self.rating_test_2, self.rating_test_3])
 
         # order key timestamp
-        response = self.client.get(self.given_rating_url + '?order_by_key=timestamp&order=dsc')
+        response = self.client.get(
+            self.given_rating_url + '?order_by_key=timestamp&order=dsc')
         self.assertEqual(response.context['order_by'], 'dsc')
         self.assertEqual(response.context['order_by_key'], 'timestamp')
         self.assertEqual(list(response.context['rating_list']),
                          [self.rating_test_3, self.rating_test_2, self.rating_test_1])
 
         # order key materi title
-        response = self.client.get(self.given_rating_url + '?order_by_key=materi__title&order=dsc')
+        response = self.client.get(
+            self.given_rating_url + '?order_by_key=materi__title&order=dsc')
         self.assertEqual(response.context['order_by'], 'dsc')
         self.assertEqual(response.context['order_by_key'], 'materi__title')
         self.assertEqual(list(response.context['rating_list']),
                          [self.rating_test_2, self.rating_test_1, self.rating_test_3])
 
+
 class PasswordValidatorPolicyTest(TestCase):
     def setUp(self):
         self.password_no_lowercase = "PASSW0RD!"
@@ -3290,28 +3615,34 @@ class PasswordValidatorPolicyTest(TestCase):
         self.password_length_lower_than_8 = "P4ss!"
         self.password_enforcing_policy = "Passw0rd!"
         self.validator = PasswordPolicyValidator()
-    
+
     def test_using_password_no_lowercase(self):
-        self.assertRaises(ValidationError, self.validator.validate, self.password_no_lowercase)
+        self.assertRaises(
+            ValidationError, self.validator.validate, self.password_no_lowercase)
 
     def test_using_password_no_upprcase(self):
-        self.assertRaises(ValidationError, self.validator.validate, self.password_no_uppercase)
-    
+        self.assertRaises(
+            ValidationError, self.validator.validate, self.password_no_uppercase)
+
     def test_using_password_no_digit(self):
-        self.assertRaises(ValidationError, self.validator.validate, self.password_no_digit)
+        self.assertRaises(
+            ValidationError, self.validator.validate, self.password_no_digit)
 
     def test_using_password_no_special_char(self):
-        self.assertRaises(ValidationError, self.validator.validate, self.password_no_special_char)
-    
+        self.assertRaises(ValidationError, self.validator.validate,
+                          self.password_no_special_char)
+
     def test_using_password_with_length_less_than_8(self):
-        self.assertRaises(ValidationError, self.validator.validate, self.password_length_lower_than_8)
-    
+        self.assertRaises(ValidationError, self.validator.validate,
+                          self.password_length_lower_than_8)
+
     def test_using_password_using_correct_policy(self):
-        self.assertEquals(self.validator.validate(self.password_enforcing_policy), None)
-    
+        self.assertEquals(self.validator.validate(
+            self.password_enforcing_policy), None)
+
 
 class LandingPageNavbarTest(TestCase):
-    
+
     def setUp(self):
         self.client = Client()
         self.contributor_credential = {
@@ -3326,52 +3657,95 @@ class LandingPageNavbarTest(TestCase):
             "email": "public@gov.id",
             "password": id_generator()
         }
-        self.kontributor = User.objects.create_contributor(**self.contributor_credential)
+        self.kontributor = User.objects.create_contributor(
+            **self.contributor_credential)
         self.admin = User.objects.create_admin(**self.admin_credential)
         self.public = User.objects.create_user(**self.public_credential)
 
     def test_navbar_admin(self):
         self.client.login(**self.admin_credential)
         response = self.client.get('/')
-        self.assertContains(response, '<a class="nav-link" href="/">Home<span class="sr-only">(current)</span></a>')
-        self.assertContains(response, '<a class="nav-link" href="/forum">Forum</a>')
-        self.assertContains(response, '<a class="nav-link" href="/news/all">Berita</a>')
-        self.assertContains(response, '<a class="nav-link" href="/administration">Administrasi</a>')
-        self.assertContains(response, '<a class="nav-link" href="/profil">Profil</a>')
-        self.assertContains(response, '<a class="nav-link" href="/logout">Logout</a>')
-        self.assertNotContains(response, '<a class="nav-link" href="/logout">LogoutX</a>')
+        self.assertContains(
+            response, '<a class="sidebar-brand-text navbar-brand" href="/">Digipus</a>')
+        self.assertContains(
+            response, '<a class="nav-link" href="/forum">Forum</a>')
+        self.assertContains(
+            response, '<a class="nav-link" href="/news/all">Berita</a>')
+        self.assertContains(
+            response, '<a class="nav-link" href="/profil">Profil</a>')
+        self.assertContains(
+            response, '<a class="nav-link" href="/logout">Logout</a>')
+        self.assertContains(
+            response, '<a class="nav-link" href="/administration">Administrasi</a>')
+        self.assertNotContains(
+            response, '<a class="nav-link" href="/dashboard">Dasbor</a>')
 
     def test_navbar_contributor(self):
         self.client.login(**self.contributor_credential)
         response = self.client.get('/')
-        self.assertContains(response, '<a class="nav-link" href="/">Home<span class="sr-only">(current)</span></a>')
-        self.assertContains(response, '<a class="nav-link" href="/forum">Forum</a>')
-        self.assertContains(response, '<a class="nav-link" href="/news/all">Berita</a>')
-        self.assertContains(response, '<a class="nav-link" href="/dashboard">Dasbor</a>')
-        self.assertContains(response, '<a class="nav-link" href="/profil">Profil</a>')
-        self.assertContains(response, '<a class="nav-link" href="/logout">Logout</a>')
-        self.assertNotContains(response, '<a class="nav-link" href="/logout">LogoutX</a>')
+        self.assertContains(
+            response, '<a class="sidebar-brand-text navbar-brand" href="/">Digipus</a>')
+        self.assertContains(
+            response, '<a class="nav-link" href="/forum">Forum</a>')
+        self.assertContains(
+            response, '<a class="nav-link" href="/news/all">Berita</a>')
+        self.assertContains(
+            response, '<a class="nav-link" href="/profil">Profil</a>')
+        self.assertContains(
+            response, '<a class="nav-link" href="/logout">Logout</a>')
+        self.assertContains(
+            response, '<a class="nav-link" href="/dashboard">Dasbor</a>')
+        self.assertNotContains(
+            response, '<a class="nav-link" href="/administration">Administrasi</a>')
 
     def test_navbar_public(self):
         self.client.login(**self.public_credential)
         response = self.client.get('/')
-        self.assertContains(response, '<a class="nav-link" href="/">Home<span class="sr-only">(current)</span></a>')
-        self.assertContains(response, '<a class="nav-link" href="/forum">Forum</a>')
-        self.assertContains(response, '<a class="nav-link" href="/news/all">Berita</a>')
-        self.assertContains(response, '<a class="nav-link" href="/profil">Profil</a>')
-        self.assertContains(response, '<a class="nav-link" href="/logout">Logout</a>')
-        self.assertNotContains(response, '<a class="nav-link" href="/logout">LogoutX</a>')
+        self.assertContains(
+            response, '<a class="sidebar-brand-text navbar-brand" href="/">Digipus</a>')
+        self.assertContains(
+            response, '<a class="nav-link" href="/forum">Forum</a>')
+        self.assertContains(
+            response, '<a class="nav-link" href="/news/all">Berita</a>')
+        self.assertContains(
+            response, '<a class="nav-link" href="/profil">Profil</a>')
+        self.assertContains(
+            response, '<a class="nav-link" href="/logout">Logout</a>')
+        self.assertNotContains(
+            response, '<a class="nav-link" href="/dashboard">Dasbor</a>')
+        self.assertNotContains(
+            response, '<a class="nav-link" href="/administration">Administrasi</a>')
 
     def test_navbar_anonymous(self):
         response = self.client.get('/')
-        self.assertContains(response, '<a class="nav-link" href="/">Home<span class="sr-only">(current)</span></a>')
-        self.assertContains(response, '<a class="nav-link" href="/forum">Forum</a>')
-        self.assertContains(response, '<a class="nav-link" href="/news/all">Berita</a>')
-        self.assertContains(response, '<a class="nav-link" href="/registrasi/umum">Registrasi Umum</a>')
-        self.assertContains(response, '<a class="nav-link" href="/registrasi">Registrasi Kontributor</a>')
-        self.assertContains(response, '<a class="nav-link" href="/login">Login</a>')
-        self.assertContains(response, '<a class="nav-link" href="/login_admin">Login Admin</a>')
-        self.assertNotContains(response, '<a class="nav-link" href="/logout">LogoutX</a>')
+        self.assertContains(
+            response, '<a class="sidebar-brand-text navbar-brand" href="/">Digipus</a>')
+        self.assertContains(
+            response, '<a class="nav-link" href="/forum">Forum</a>')
+        self.assertContains(
+            response, '<a class="nav-link" href="/news/all">Berita</a>')
+        self.assertContains(
+            response,
+            '<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">'
+        )
+        self.assertContains(response, 'Registrasi</a>')
+        self.assertContains(
+            response, '<a class="dropdown-item" href="/registrasi/umum">Umum</a>')
+        self.assertContains(
+            response, '<a class="dropdown-item" href="/registrasi">Kontributor</a>')
+        self.assertContains(response, 'Login</a>')
+        self.assertContains(
+            response, '<a class="dropdown-item" href="/login">Kontributor</a>')
+        self.assertContains(
+            response, '<a class="dropdown-item" href="/login_admin">Admin</a>')
+        self.assertNotContains(
+            response, '<a class="nav-link" href="/profil">Profil</a>')
+        self.assertNotContains(
+            response, '<a class="nav-link" href="/logout">Logout</a>')
+        self.assertNotContains(
+            response, '<a class="nav-link" href="/dashboard">Dasbor</a>')
+        self.assertNotContains(
+            response, '<a class="nav-link" href="/administration">Administrasi</a>')
 
 
 class MateriRecommendationTest(TestCase):
@@ -3431,7 +3805,8 @@ class MateriRecommendationTest(TestCase):
         )
 
         response = Client().get("/?recommendation=1")
-        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, [2, 1, 3])
 
     def test_set_date_as_tiebreak_if_like_counts_is_same(self):
@@ -3468,9 +3843,201 @@ class MateriRecommendationTest(TestCase):
         Like.objects.create(materi=materi1)
 
         response = Client().get("/?recommendation=1")
-        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])
-    
+
+
+class GuestBookTest(TestCase):
+    def test_form_name_input_has_placeholder_and_css_classes(self):
+        form = GuestBookForm()
+        self.assertIn('placeholder="Input your name"', form.as_p())
+        self.assertIn('class="form-control input-lg"', form.as_p())
+
+    def test_form_job_input_has_placeholder_and_css_classes(self):
+        form = GuestBookForm()
+        self.assertIn('placeholder="Input your job"', form.as_p())
+        self.assertIn('class="form-control input-lg"', form.as_p())
+
+    def test_form_gender_input_has_choices_and_css_classes(self):
+        form = GuestBookForm()
+        self.assertIn('option value="Male"', form.as_p())
+        self.assertIn('option value="Female">', form.as_p())
+        self.assertIn('class="form-control input-lg"', form.as_p())
+
+    def test_form_validation_for_blank_items(self):
+        form = GuestBookForm(data={'name': '', 'job': '', 'gender': ''})
+        self.assertFalse(form.is_valid())
+        self.assertEqual(
+            form.errors['name'],
+            ['Name is required']
+        )
+        self.assertEqual(
+            form.errors['job'],
+            ['Job is required']
+        )
+        self.assertEqual(
+            form.errors['gender'],
+            ['Gender is required']
+        )
+
+    def test_can_create_guest_book_instance(self):
+        guestBook = GuestBook(name='Selvy', job='Student', gender='Female')
+        guestBook.save()
+        self.assertEqual(GuestBook.objects.count(), 1)
+
+
+class MateriSearchVectorTest(TestCase):
+    def setUp(self):
+        Materi.SEARCH_INDEX = (("title", "A"), ("author", "B"))
+
+    def test_search_vector_constructed_on_create(self):
+        materi = Materi(title="Buku 1", author="Pembuat 1")
+        materi.save()
+
+        search_vector_string = list(
+            Materi.objects.values_list("_search_vector", flat=True)
+        )[0]
+
+        self.assertGreaterEqual(len(search_vector_string.split(",")), 1)
+
+    def test_search_vector_based_on_indexed_attribute(self):
+        materi = Materi(title="Buku 1", author="Pembuat 1",
+                        descriptions="Deskripsi 1")
+        materi.save()
+
+        search_vector_string = list(
+            Materi.objects.values_list("_search_vector", flat=True)
+        )[0]
+        self.assertIn("buku", search_vector_string)
+        self.assertIn("pembuat", search_vector_string)
+
+    def test_search_vector_not_based_on_unindexed_attribute(self):
+        materi = Materi(title="Buku 1", author="Pembuat 1",
+                        descriptions="Deskripsi 1")
+        materi.save()
+
+        search_vector_string = list(
+            Materi.objects.values_list("_search_vector", flat=True)
+        )[0]
+
+        self.assertNotIn("deskripsi", search_vector_string)
+
+    def test_search_vector_reconstructed_on_update_indexed_field(self):
+        materi = Materi(title="Sebelum reconstruct")
+        materi.save()
+
+        search_vector = list(Materi.objects.values_list(
+            "_search_vector", flat=True))[0]
+
+        materi.title = "Setelah reconstruct"
+        materi.save()
+
+        search_vector = list(Materi.objects.values_list(
+            "_search_vector", flat=True))[0]
+
+        self.assertIn("setelah", search_vector)
+
+    def test_search_vector_not_reconstructed_on_update_unindexed_field(self):
+        materi = Materi(descriptions="sebelum reconstruct")
+        materi.save()
+
+        search_vector = list(Materi.objects.values_list(
+            "_search_vector", flat=True))[0]
+
+        materi.descriptions = "sebelum reconstruct"
+        materi.save()
+
+        search_vector = list(Materi.objects.values_list(
+            "_search_vector", flat=True))[0]
+
+        self.assertNotIn("setelah", search_vector)
+
+
+class MateriSearchTest(TestCase):
+    def test_empty_result_on_empty_table(self):
+        search_query = "test"
+
+        search_result = list(Materi.objects.search(search_query))
+        expected_search_result = []
+
+        self.assertSequenceEqual(search_result, expected_search_result)
+
+    def test_empty_result_on_unmatched_data(self):
+        Materi.SEARCH_INDEX = (("title", "A"), ("author", "B"))
+
+        Materi(title="buku 1", author="bapak 1").save()
+        Materi(title="artikel 2", author="ibu 1").save()
+
+        search_query = "majalah"
+
+        search_result = list(Materi.objects.search(search_query))
+        expected_search_result = []
+
+        self.assertSequenceEqual(search_result, expected_search_result)
+
+        search_query = "kakak"
+
+        search_result = list(Materi.objects.search(search_query))
+        expected_search_result = []
+
+        self.assertSequenceEqual(search_result, expected_search_result)
+
+    def test_correct_rank_on_result_tested_by_similiarity_words(self):
+        Materi.SEARCH_INDEX = (("descriptions", "A"),)
+        materi_2 = Materi(descriptions="ini lumayan cocok lumayan cocok")
+        materi_2.save()
+
+        materi_1 = Materi(
+            descriptions="ini sangat cocok sangat cocok sangat cocok")
+        materi_1.save()
+
+        materi_4 = Materi(descriptions="ini tidak")
+        materi_4.save()
+
+        materi_3 = Materi(descriptions="ini sedikit cocok")
+        materi_3.save()
+
+        search_query = "ini cocok"
+
+        search_result = list(Materi.objects.search(search_query))
+        expected_search_result = [materi_1, materi_2, materi_3]
+
+        self.assertSequenceEqual(search_result, expected_search_result)
+
+    def test_correct_rank_on_result_tested_by_weight(self):
+        Materi.SEARCH_INDEX = (
+            ("title", "A"),
+            ("author", "C"),
+            ("descriptions", "B"),
+            ("publisher", "D"),
+        )
+
+        materi_title = Materi(title="cocok")
+        materi_title.save()
+
+        materi_author = Materi(author="cocok cocok cocok")
+        materi_author.save()
+
+        materi_descriptions = Materi(descriptions="cocok cocok")
+        materi_descriptions.save()
+
+        materi_publisher = Materi(publisher="cocok cocok cocok cocok")
+        materi_publisher.save()
+
+        search_query = "cocok"
+
+        search_result = list(Materi.objects.search(search_query))
+        expected_search_result = [
+            materi_title,
+            materi_descriptions,
+            materi_author,
+            materi_publisher,
+        ]
+
+        self.assertSequenceEqual(search_result, expected_search_result)
+
+
 class BacaNantiTest(TestCase):
     def setUp(self):
         self.contributor_credential = {
@@ -3488,8 +4055,10 @@ class BacaNantiTest(TestCase):
         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.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"
@@ -3525,7 +4094,7 @@ class BacaNantiTest(TestCase):
         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()
@@ -3549,7 +4118,7 @@ class BacaNantiTest(TestCase):
     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)
@@ -3558,30 +4127,35 @@ class BacaNantiTest(TestCase):
     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")
-    
+        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})
+        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()
+        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()
+        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})
+        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}
@@ -3591,20 +4165,22 @@ class BacaNantiTest(TestCase):
         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})
+        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})
+        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)
@@ -3613,19 +4189,21 @@ class BacaNantiTest(TestCase):
             {"success": False, "msg": "Missing parameter"}
         )
 
+
 class MateriStatsTest(TestCase):
 
     def setUp(self):
         self.credential = {
-            'email':"kontributor@gov.id", 
-            'password':"P@ssw0rd", 
+            'email': "kontributor@gov.id",
+            'password': "P@ssw0rd",
         }
 
         self.path = '/stats/'
         self.path_json = '/stats/?data=json'
         self.header = 'Summary Materi per Kategori'
 
-        self.contributor = User.objects.create_contributor(**self.credential, name="kontributor")
+        self.contributor = User.objects.create_contributor(
+            **self.credential, name="kontributor")
         self.client = Client()
 
         categories = []
@@ -3641,7 +4219,6 @@ class MateriStatsTest(TestCase):
                 m.categories.add(categories[i])
                 m.save()
 
-
     def test_stats_has_correct_template(self):
         self.client.login(**self.credential)
         response = self.client.get(self.path)
@@ -3654,12 +4231,199 @@ class MateriStatsTest(TestCase):
 
     def test_stats_as_anonymous(self):
         response = self.client.get(self.path)
-        self.assertEqual(response.status_code, 302) #redirect
+        self.assertEqual(response.status_code, 302)  # redirect
         response = self.client.get(self.path_json)
-        self.assertEqual(response.status_code, 302) #redirect
+        self.assertEqual(response.status_code, 302)  # redirect
 
     def test_stats_api_correct_data(self):
         self.client.login(**self.credential)
         response = self.client.get(self.path_json)
         jobj = json.loads(response.content)
         self.assertEqual(len(jobj['labels']), 6)
+
+class UploadMateriTest(TestCase):
+    def setUp(self):
+        self.client = Client()
+        self.user = User.objects._create_user(email="kontributor@gov.id",
+                                              password="kontributor", is_contributor=True)
+        self.setUpImage()
+        self.content = SimpleUploadedFile("ExampleFile221.pdf", b"Test file")
+        self.category = Category.objects.create(
+            id="1", name="sains", description="kategori sains")
+
+    @override_settings(MEDIA_ROOT=tempfile.gettempdir())
+    def setUpImage(self):
+        self.cover = InMemoryUploadedFile(
+            BytesIO(base64.b64decode(TEST_IMAGE)),
+            field_name='tempfile',
+            name='tempfile.png',
+            content_type='image/png',
+            size=len(TEST_IMAGE),
+            charset='utf-8',
+        )
+
+    def test_for_invalid_input_shows_validation_error(self):
+        self.client.login(email="kontributor@gov.id",
+                          password="kontributor")
+        response = self.client.post("/unggah/", data={"title": ""})
+        self.assertContains(response, "This field is required.")
+
+    def test_data_form_upload_materi_success_save_to_db(self):
+        self.client.login(email="kontributor@gov.id",
+                          password="kontributor")
+
+        data = {"title": "Dunia Binatang", "author": "Parzival", "publisher": "Buku Asyik",
+                "release_year": "2015", "descriptions": "Buku dunia binatang seri 1",
+                'categories': "1", "cover": self.cover, "content": self.content}
+
+        self.client.post("/unggah/", data=data)
+
+        self.assertEqual(Materi.objects.count(), 1)
+
+class NotifikasiKontributorTest(TestCase):
+    def setUp(self):
+        self.client = Client()
+        self.contributor_credential = {"email": "kontributor@gov.id", "password": "passwordtest"}
+        self.contributor = get_user_model().objects.create_user(
+            **self.contributor_credential, name="Kontributor", is_contributor=True
+        )
+
+        self.admin_credential = {
+            "email": "admin@gov.id",
+            "password": "passwordtest"
+        }
+        self.admin = get_user_model().objects.create_user(
+            **self.admin_credential, name="Admin", is_admin=True)
+
+        self.setUpImage()
+        self.content = SimpleUploadedFile("ExampleFile221.pdf", b"Test file")
+        self.category = Category.objects.create(id="1", name="medis", description="kategori medis")
+        VerificationSetting.objects.create(title="Kriteria 1", description="memenuhi kriteria 1", archived=False, pk=1)
+        self.make_materi()
+    
+    @override_settings(MEDIA_ROOT=tempfile.gettempdir())
+    def setUpImage(self):
+        self.cover = InMemoryUploadedFile(
+            BytesIO(base64.b64decode(TEST_IMAGE)),
+            field_name='tempfile',
+            name='tempfile.png',
+            content_type='image/png',
+            size=len(TEST_IMAGE),
+            charset='utf-8',
+        )
+    
+    def make_materi(self):
+        self.client.login(**self.contributor_credential)
+        self.client.post(
+            "/unggah/", data={"title":"Materi 1", "author":"Agas", "publisher":"Kelas SC", "release_year":"2000",
+                                "descriptions":"Deskripsi Materi 1", 'categories':"1",
+                                "cover":self.cover, "content":self.content,
+                                "yt_video_id":"jNwz4L9MGVY"}
+        )
+        self.client.logout()
+
+    def admin_disapprove_materi(self, materi, pk_new_materi):
+        self.client.login(**self.admin_credential)
+        self.client.post(
+            "/administration/detail-verif/"+str(pk_new_materi)+"/", data={
+                'kriteria-1':'0','feedback':"a", 'action':"disapprove"
+            }
+        )
+        self.client.logout()
+    
+    def test_no_notification(self):
+        self.client.login(**self.contributor_credential)
+        notifs = NotifikasiKontributor.objects.filter(user=self.contributor)
+        self.assertEqual(notifs.count(), 0)
+        self.client.logout()
+
+    def test_notification(self):
+        materi = Materi.objects.get(title="Materi 1", uploader=self.contributor)
+        pk_new_materi = materi.id
+        self.admin_disapprove_materi(materi, pk_new_materi)
+        self.assertEqual(NotifikasiKontributor.objects.filter(materi=materi, user=self.contributor).count(), 1)
+        
+        # Client as Kontributor get notifications
+        self.client.login(**self.contributor_credential)
+        notif = NotifikasiKontributor.objects.get(materi=materi, user=self.contributor)
+        html = self.client.get("/dashboard/").content.decode("utf-8")
+        self.assertIn(str(notif), html)
+
+    def test_notifications_reset_when_it_is_revised(self):
+        materi = Materi.objects.get(title="Materi 1", uploader=self.contributor)
+        pk_new_materi = materi.id
+        self.admin_disapprove_materi(materi, pk_new_materi)
+        self.assertEqual(NotifikasiKontributor.objects.filter(materi=materi, user=self.contributor).count(), 1)
+
+        self.client.login(**self.contributor_credential)
+        notif = NotifikasiKontributor.objects.get(materi=materi, user=self.contributor)
+
+        # Client as Kontributor check Materi detail, and the notification should be gone
+        response = self.client.post(
+            "/revisi/materi/"+str(pk_new_materi)+"/",
+            data={"title":"Materi 2", "author":"Agas", "publisher":"Kelas SC", "release_year":"2000",
+            "descriptions":"Deskripsi Materi 2", 'categories':"1", "cover":self.cover, "content":self.content,
+            "yt_video_id":"jNwz4L9MGVY"}
+        )
+        self.assertEqual(NotifikasiKontributor.objects.filter(materi=materi, user=self.contributor).count(), 0)
+    
+    def test_notifications_reset_when_it_is_opened_in_detail(self):
+        materi = Materi.objects.get(title="Materi 1", uploader=self.contributor)
+        pk_new_materi = materi.id
+        self.admin_disapprove_materi(materi, pk_new_materi)
+        self.assertEqual(NotifikasiKontributor.objects.filter(materi=materi, user=self.contributor).count(), 1)
+
+        self.client.login(**self.contributor_credential)
+        notif = NotifikasiKontributor.objects.get(materi=materi, user=self.contributor)
+
+        # Client as Kontributor check Materi detail, and the notification should be gone
+        self.client.get("/materi/"+str(materi.id)+"/")
+        self.assertEqual(NotifikasiKontributor.objects.filter(materi=materi, user=self.contributor).count(), 0)
+
+class AdminNotificationTest(TestCase):
+        
+    def setUp(self):
+        self.client = Client()
+        self.contributor_credential = {
+            "email": "kontributor@gov.id",
+            "password": id_generator()
+        }
+        self.admin_credential = {
+            "email": "admin@gov.id",
+            "password": id_generator()
+        }
+        self.public_credential = {
+            "email": "public@gov.id",
+            "password": id_generator()
+        }
+        self.kontributor = User.objects.create_contributor(**self.contributor_credential)
+        self.admin = User.objects.create_admin(**self.admin_credential)
+        self.public = User.objects.create_user(**self.public_credential)
+        self.setUpImage()
+        self.content = SimpleUploadedFile("ExampleFile221.pdf", b"Test file")
+        self.category = Category.objects.create(id="1", name="medis", description="kategori medis")
+
+    @override_settings(MEDIA_ROOT=tempfile.gettempdir())
+    def setUpImage(self):
+        self.cover = InMemoryUploadedFile(
+            BytesIO(base64.b64decode(TEST_IMAGE)),
+            field_name='tempfile',
+            name='tempfile.png',
+            content_type='image/png',
+            size=len(TEST_IMAGE),
+            charset='utf-8',
+        )
+
+    def test_notification_admin(self):
+        self.client.login(**self.contributor_credential)
+        self.client.post(
+            "/unggah/", data={"title":"Materi 1", "author":"Agas", "publisher":"Kelas SC", "release_year":"2000",
+                                "descriptions":"Deskripsi Materi 1", 'categories':"1",
+                                "cover":self.cover, "content":self.content,
+                                "yt_video_id":"jNwz4L9MGVY"}
+        )
+        materi = Materi.objects.get(title="Materi 1")
+        self.assertTrue(AdminNotification.objects.get(materi=materi.id))
+
+
+
diff --git a/app/urls.py b/app/urls.py
index b48be941b1a77fcacd0cae3571b7360b26199c2f..fb1e0e927cfe413948fbf74502277c0c6d6859fe 100644
--- a/app/urls.py
+++ b/app/urls.py
@@ -5,8 +5,7 @@ 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, ReadLaterView, MostContributor)
+                       ReqMateriView, KatalogPerKontributorView, MateriFavorite, PasswordChangeViews, password_success, SubmitVisitorView, GuestBookView, ReadLaterView, MostContributor)
 
 
 urlpatterns = [
@@ -15,14 +14,19 @@ urlpatterns = [
     path("materi/like/", views.toggle_like, name="PostLikeToggle"),
     path("delete/<int:pk_materi>/<int:pk_comment>",
          views.delete_comment, name="delete-comment"),
+    path("review/delete/<int:pk_materi>/<int:pk_review>",
+        views.delete_review, name="delete-review"),
     path("comment/like/", views.toggle_like_comment, name="comment-like-toggle"),
-    path("comment/dislike/", views.toggle_dislike_comment, name="comment-dislike-toggle"),
+    path("comment/dislike/", views.toggle_dislike_comment,
+         name="comment-dislike-toggle"),
     path("materi/<int:pk>/delete", views.delete_materi, name="detele-materi"),
     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("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"),
     path("profil/", ProfilView.as_view(), name="profil"),
@@ -36,26 +40,33 @@ urlpatterns = [
     path("profil/<str:email>/", KatalogPerKontributorView.as_view(),
          name="katalog-per-kontributor"),
     path("materi/rate/", views.add_rating_materi, name="rate-materi"),
-    path("materi/<int:pk>/save-to-gdrive/", views.save_to_gdrive, name="save-to-gdrive"),
+    path("materi/<int:pk>/save-to-gdrive/",
+         views.save_to_gdrive, name="save-to-gdrive"),
     path("favorite/", MateriFavorite.as_view(), name="favorite"),
-    path("change-password/", PasswordChangeViews.as_view(template_name='change-password.html')),
+    path("change-password/",
+         PasswordChangeViews.as_view(template_name='change-password.html')),
     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"),
-    path("reset_password/", 
-        auth_views.PasswordResetView.as_view(template_name="password_reset.html"), 
-        name="reset_password"),
-    path("reset_password_sent/", 
-        auth_views.PasswordResetDoneView.as_view(template_name="password_reset_sent.html"), 
-        name="password_reset_done"),
-    path("reset/<uidb64>/<token>/", 
-        auth_views.PasswordResetConfirmView.as_view(template_name="password_reset_form.html"), 
-        name="password_reset_confirm"),
-    path("reset_password_complete/", 
-        auth_views.PasswordResetCompleteView.as_view(template_name="password_reset_done.html"), 
-        name="password_reset_complete"),
-    path("most-contributor/", MostContributor.as_view(), name="most-contributor")
+    path("reset_password/",
+         auth_views.PasswordResetView.as_view(
+             template_name="password_reset.html"),
+         name="reset_password"),
+    path("reset_password_sent/",
+         auth_views.PasswordResetDoneView.as_view(
+             template_name="password_reset_sent.html"),
+         name="password_reset_done"),
+    path("reset/<uidb64>/<token>/",
+         auth_views.PasswordResetConfirmView.as_view(
+             template_name="password_reset_form.html"),
+         name="password_reset_confirm"),
+    path("reset_password_complete/",
+         auth_views.PasswordResetCompleteView.as_view(
+             template_name="password_reset_done.html"),
+         name="password_reset_complete"),
+    path("most-contributor/", MostContributor.as_view(), name="most-contributor"),
+    path("guest-book/", GuestBookView.as_view(), name="guest-book")
 ]
diff --git a/app/views.py b/app/views.py
index cd6be9c991ea2acdaaece401b16d1959f7a2a8db..63664613c38290ca9c5521d7f56ac4c8c4e89e73 100644
--- a/app/views.py
+++ b/app/views.py
@@ -24,16 +24,20 @@ from django.urls import reverse_lazy
 from django.views import defaults
 from django.views.generic import TemplateView
 
-from app.forms import SuntingProfilForm, UploadMateriForm, RatingContributorForm
+from app.forms import SuntingProfilForm, UploadMateriForm, RatingContributorForm, GuestBookForm
 from app.models import (
     Category,
     Comment,
     Review,
     Materi,
+    LaporanMateri,
     ReqMaterial,
     Rating, RatingContributor,
     SubmitVisitor,
-    ReadLater
+    GuestBook,
+    ReadLater,
+    NotifikasiKontributor,
+    AdminNotification
 )
 from authentication.models import User
 from .services import DafterKatalogService, DetailMateriService, LikeDislikeService, MateriFieldValidationHelperService, \
@@ -48,6 +52,7 @@ UNGGAH_URL = "/unggah/"
 UNGGAH_EXCEL_URL = "/unggah_excel/"
 LOGIN_URL = "/login/"
 
+
 def permission_denied(request, exception, template_name="error_403.html"):
     return defaults.permission_denied(request, exception, template_name)
 
@@ -63,11 +68,12 @@ class DaftarKatalog(TemplateView):
         context = self.get_context_data(**kwargs)
         context["kategori_list"] = Category.objects.all()
 
-        lst_materi = Materi.objects.filter(status="APPROVE").order_by("date_modified")
+        lst_materi = Materi.objects.filter(
+            status="APPROVE").order_by("date_modified")
         url = ""
 
-        lst_materi, url = DafterKatalogService.apply_options(lst_materi, request, url)
-
+        lst_materi, url = DafterKatalogService.apply_options(
+            lst_materi, request, url)
 
         context["materi_list"] = lst_materi
         paginator = Paginator(context["materi_list"], 15)
@@ -102,14 +108,16 @@ class KatalogPerKontributorView(TemplateView):
         contributor = context["contributor"]
         context["form_rating"] = RatingContributorForm(initial={
             "contributor": contributor,
-            "user":request.user
+            "user": request.user
         })
         context["avg_rating"] = User.objects.filter(email=kwargs["email"]) \
             .annotate(avg_rating=Avg("contributor__score"))[0]
-        context["count_rating"] = RatingContributor.objects.filter(contributor=contributor).count()
+        context["count_rating"] = RatingContributor.objects.filter(
+            contributor=contributor).count()
 
         if request.user.is_authenticated:
-            has_rated = RatingContributor.objects.filter(user=request.user, contributor=contributor).exists()
+            has_rated = RatingContributor.objects.filter(
+                user=request.user, contributor=contributor).exists()
             context["has_rated"] = has_rated
 
         return self.render_to_response(context=context)
@@ -123,7 +131,7 @@ class KatalogPerKontributorView(TemplateView):
         is_update = request.POST.get('update', None)
         if is_delete:
             rating_contributor = get_object_or_404(
-                RatingContributor, 
+                RatingContributor,
                 user=request.user,
                 contributor=context["contributor"]
             )
@@ -134,7 +142,7 @@ class KatalogPerKontributorView(TemplateView):
                 user=request.user,
                 contributor=context["contributor"]
             ).first()
-            
+
             if rating and score:
                 rating.score = int(score)
                 rating.save()
@@ -142,9 +150,10 @@ class KatalogPerKontributorView(TemplateView):
             data = RatingContributorForm(request.POST)
             if data.is_valid():
                 data.save()
-                
+
         return redirect("katalog-per-kontributor", email=kwargs["email"])
 
+
 class DetailMateri(TemplateView):
     template_name = "app/detail_materi.html"
 
@@ -153,17 +162,21 @@ class DetailMateri(TemplateView):
         if not self.request.session or not self.request.session.session_key:
             self.request.session.save()
         materi = get_object_or_404(Materi, pk=kwargs["pk"])
-        DetailMateriService.init_context_data(context, materi, self.request.session)
+        DetailMateriService.init_context_data(
+            context, materi, self.request.session)
         published_date = DetailMateriService.set_published_date(materi)
-        DetailMateriService.init_citation_and_materi_rating(context, materi, published_date, self.request)
+        DetailMateriService.init_citation_and_materi_rating(
+            context, materi, published_date, self.request)
         DetailMateriService.init_materi_download_count(context, materi)
 
         if self.request.user.is_authenticated:
-            materi_rating = Rating.objects.filter(materi=materi, user=self.request.user).first()
+            materi_rating = Rating.objects.filter(
+                materi=materi, user=self.request.user).first()
             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()
+            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:
@@ -173,59 +186,72 @@ class DetailMateri(TemplateView):
 
         return context
 
-
     def get(self, request, *args, **kwargs):
         context = self.get_context_data(**kwargs)
-        query_set_for_comment = Comment.objects.filter(materi=context["materi_data"])
-        query_set_for_review = Review.objects.filter(materi=context["materi_data"])
-        has_disliked, has_liked = DetailMateriService.find_comment_like_dislike(query_set_for_comment, self.request.session)
+        query_set_for_comment = Comment.objects.filter(
+            materi=context["materi_data"])
+        query_set_for_review = Review.objects.filter(
+            materi=context["materi_data"])
+        has_disliked, has_liked = DetailMateriService.find_comment_like_dislike(
+            query_set_for_comment, self.request.session)
         context["comment_data"] = query_set_for_comment
         context["review_data"] = query_set_for_review
         context["has_liked_comment"] = has_liked
         context["has_disliked_comment"] = has_disliked
+        if request.user.is_authenticated and request.user.is_contributor:
+            opened_notif = NotifikasiKontributor.objects.filter(user=request.user, materi=context["materi_data"])
+            opened_notif.delete()
         return self.render_to_response(context=context)
 
-
-
     def post(self, request, *args, **kwargs):
         comment_text = request.POST.get("comment", None)
-        review_text  = request.POST.get("review", None)
-        if ((comment_text == None or comment_text == "" )and (review_text == None or review_text == "")):
+        review_text = request.POST.get("review", None)
+        report_text = request.POST.get("report", None)
+        if ((comment_text == None or comment_text == "") and
+            (review_text == None or review_text == "") and
+                (report_text == None or report_text == "")):
             context = self.get_context_data(*args, **kwargs)
             context["error_message"] = "Anda belum menuliskan komentar"
             context["materi_data"] = get_object_or_404(Materi, pk=kwargs["pk"])
-            query_set_for_comment = Comment.objects.filter(materi=context["materi_data"])
+            query_set_for_comment = Comment.objects.filter(
+                materi=context["materi_data"])
             context["comment_data"] = query_set_for_comment
-            query_set_for_review = Review.objects.filter(materi=context["materi_data"])
+            query_set_for_review = Review.objects.filter(
+                materi=context["materi_data"])
             context["review_data"] = query_set_for_review
             return self.render_to_response(context=context)
 
         materi = get_object_or_404(Materi, pk=kwargs["pk"])
         user_obj = request.user if request.user.is_authenticated else None
         if user_obj:
-            if (comment_text != None ):
+            if (comment_text != None):
                 comment = Comment.objects.create(
                     comment=comment_text, username=DetailMateriService.get_user_name(request), materi=materi, user=user_obj
                 )
                 comment.save()
                 materi_uploader = materi.uploader
                 if materi_uploader.is_subscribing_to_material_comments and user_obj.email != materi_uploader.email:
-                    email_content =  f'User dengan email {user_obj.email} ' + \
+                    email_content = f'User dengan email {user_obj.email} ' + \
                         f'menambahkan komentar pada materi Anda dengan judul "{materi.title}".' + \
                         f'\nKomentar: "{comment.comment}".\n' + \
                         f'Silahkan akses halaman detail materi untuk berinteraksi lebih lanjut.'
 
                     MailService.send(
-                        subject = 'DIGIPUS: Komentar Baru pada Materi Anda',
-                        message = email_content,
-                        from_email = getattr(settings, 'EMAIL_HOST_USER'),
-                        recipient_list = [materi_uploader.email,],
-                    ) 
+                        subject='DIGIPUS: Komentar Baru pada Materi Anda',
+                        message=email_content,
+                        from_email=getattr(settings, 'EMAIL_HOST_USER'),
+                        recipient_list=[materi_uploader.email, ],
+                    )
             elif (review_text != None):
                 review = Review.objects.create(
                     review=review_text, username=DetailMateriService.get_user_name(request), materi=materi, user=user_obj
                 )
                 review.save()
+            elif (report_text != None):
+                laporan_materi = LaporanMateri.objects.create(
+                    materi=materi, laporan=report_text, user=user_obj
+                )
+                laporan_materi.save()
         return HttpResponseRedirect(request.path)
 
 
@@ -248,6 +274,14 @@ def delete_comment(request, pk_materi, pk_comment):
     comment.delete()
     return HttpResponseRedirect(url)
 
+def delete_review(request, pk_materi, pk_review):
+    if not request.user.is_authenticated or not request.user.is_admin:
+        raise PermissionDenied(request)
+    review = get_object_or_404(Review, pk=pk_review)
+    url_materi = "/materi/" + str(pk_materi) + "/"
+    review.delete()
+    return HttpResponseRedirect(url_materi)
+
 def toggle_like_comment(request):
     comment_id = 0
     if request.method == "POST":
@@ -271,24 +305,22 @@ def toggle_dislike_comment(request):
     else:
         return JsonResponse({"success": False, "msg": UNSUPPORTED_MESSAGE, "comment_id": comment_id})
 
-
 def add_rating_materi(request):
     if request.method == "POST" and request.user.is_authenticated:
 
         materi_id = request.POST.get("materi_id", None)
         rating_score = request.POST.get("rating_score", None)
 
-
         is_valid_params, materi_id, \
-        rating_score, response, \
-        status_code = MateriFieldValidationHelperService.\
-            validate_materi_rating_params(materi_id,rating_score)
+            rating_score, response, \
+            status_code = MateriFieldValidationHelperService.\
+            validate_materi_rating_params(materi_id, rating_score)
 
         if not is_valid_params:
             return JsonResponse(response, status=status_code)
 
         is_valid_rating, materi, \
-        response, status_code = MateriFieldValidationHelperService.\
+            response, status_code = MateriFieldValidationHelperService.\
             validate_materi_rating(materi_id, request.user)
 
         if not is_valid_rating:
@@ -301,7 +333,6 @@ def add_rating_materi(request):
     return JsonResponse({"success": False, "msg": "Forbidden"}, status=403)
 
 
-
 def download_materi(request, pk):
     materi = get_object_or_404(Materi, pk=pk)
     path = materi.content.path
@@ -328,7 +359,8 @@ def view_materi(request, pk):
         try:
             with open(file_path, "rb") as fh:
                 response = HttpResponse(fh.read(), content_type=mimetype[0])
-                DownloadViewMateriHelperService.build_view_materi_response(file_path, materi, response)
+                DownloadViewMateriHelperService.build_view_materi_response(
+                    file_path, materi, response)
                 return response
         except Exception:
             raise Http404(FILE_NOT_FOUND_MESSAGE)
@@ -344,6 +376,7 @@ def delete_materi(request, pk):
     materi.delete()
     return HttpResponseRedirect("/dashboard/")
 
+
 class UploadMateriView(TemplateView):
     template_name = UNGGAH_HTML
     context = {}
@@ -365,7 +398,8 @@ class UploadMateriView(TemplateView):
             if not UploadMateriService.validate_file_extension(konten, request, yt_url_id):
                 return HttpResponseRedirect(UNGGAH_URL)
             UploadMateriService.upload_materi(form, materi)
-            messages.success(request, "Materi berhasil diunggah, periksa riwayat unggah anda")
+            messages.success(
+                request, "Materi berhasil diunggah, periksa riwayat unggah anda")
             return HttpResponseRedirect(UNGGAH_URL)
         else:
             context = self.get_context_data(**kwargs)
@@ -373,8 +407,6 @@ class UploadMateriView(TemplateView):
             messages.error(request, "Terjadi kesalahan pada pengisian data")
             return self.render_to_response(context)
 
-
-
     def get(self, request, *args, **kwargs):
         if request.user.is_authenticated == False or not request.user.is_contributor:
             raise PermissionDenied(request)
@@ -403,11 +435,14 @@ class UploadMateriExcelView(TemplateView):
         if "template" in self.request.GET:
 
             data_frame = pd.DataFrame(
-                {"Title": [], "Author": [], "Publisher": [], "Categories": [], "Description": [],}
+                {"Title": [], "Author": [], "Publisher": [],
+                    "Categories": [], "Description": [], }
             )
 
+            # pylint: disable=abstract-class-instantiated
             with BytesIO() as b:
-                writer = pd.ExcelWriter(b, engine="xlsxwriter")  # pylint: disable=abstract-class-instantiated
+                writer = pd.ExcelWriter(
+                    b, engine="xlsxwriter")
                 data_frame.to_excel(writer, index=0)
                 writer.save()
                 response = HttpResponse(
@@ -441,9 +476,11 @@ class UploadMateriExcelView(TemplateView):
         for i in range(row):
 
             # Validate Categories
-            message = UploadMateriService.validate_excel_categories(categories, excel, i, message)
+            message = UploadMateriService.validate_excel_categories(
+                categories, excel, i, message)
 
-            message = UploadMateriService.validate_excel_field_length(excel, field_length, i, message)
+            message = UploadMateriService.validate_excel_field_length(
+                excel, field_length, i, message)
 
             if message != None:
                 break
@@ -454,15 +491,14 @@ class UploadMateriExcelView(TemplateView):
 
         # Second pass, save data
         with django.db.transaction.atomic():
-            UploadMateriService.upload_materi_excel(categories, excel, request, row)
+            UploadMateriService.upload_materi_excel(
+                categories, excel, request, row)
 
         messages.success(request, "Materi berhasil diunggah")
 
         return HttpResponseRedirect(UNGGAH_EXCEL_URL)
 
 
-
-
 class DashboardKontributorView(TemplateView):
     template_name = "dashboard.html"
 
@@ -472,7 +508,8 @@ class DashboardKontributorView(TemplateView):
         return super(DashboardKontributorView, self).dispatch(request, *args, **kwargs)
 
     def get_context_data(self, **kwargs):
-        context = super(DashboardKontributorView, self).get_context_data(**kwargs)
+        context = super(DashboardKontributorView,
+                        self).get_context_data(**kwargs)
         return context
 
     def get(self, request, *args, **kwargs):
@@ -519,13 +556,15 @@ class SuntingProfilView(TemplateView):
 
         current_user = self.request.user
 
-        form = SuntingProfilForm(request.POST, request.FILES, instance=current_user)
+        form = SuntingProfilForm(
+            request.POST, request.FILES, instance=current_user)
         if form.is_valid():
             current_user.default_profile_picture = True
 
             # Removing exifdata from profile picture on upload
             if request.FILES:
-                EditProfileService.update_profile_picture(current_user, request)
+                EditProfileService.update_profile_picture(
+                    current_user, request)
             else:
                 form.save()
             return HttpResponseRedirect("/profil/")
@@ -569,7 +608,8 @@ class SuksesLoginKontributorView(TemplateView):
         return super(SuksesLoginKontributorView, self).dispatch(request, *args, **kwargs)
 
     def get_context_data(self, **kwargs):
-        context = super(SuksesLoginKontributorView, self).get_context_data(**kwargs)
+        context = super(SuksesLoginKontributorView,
+                        self).get_context_data(**kwargs)
         return context
 
     def get(self, request, *args, **kwargs):
@@ -584,6 +624,10 @@ class SuksesLoginKontributorView(TemplateView):
 class SuksesLoginAdminView(TemplateView):
     template_name = "sukses_admin.html"
 
+    def getAdminNotification(self):
+        notifications = AdminNotification.objects.all()
+        return notifications
+
     def dispatch(self, request, *args, **kwargs):
         if not request.user.is_admin:
             raise PermissionDenied(request)
@@ -599,8 +643,10 @@ class SuksesLoginAdminView(TemplateView):
         current_user = self.request.user
         context["user"] = current_user
 
-        return self.render_to_response(context)
+        notifications = self.getAdminNotification()
+        context["notifications"] = notifications
 
+        return self.render_to_response(context)
 
 class PostsView(TemplateView):
 
@@ -616,9 +662,11 @@ class PostsView(TemplateView):
         user = self.request.user
 
         posts = Materi.objects.filter(uploader=user).order_by("-date_created")
-        posts_data = {post.id: {"data": post, "comments": []} for post in posts}
+        posts_data = {post.id: {"data": post, "comments": []}
+                      for post in posts}
 
-        comments = Comment.objects.filter(materi__id__in=posts_data.keys()).order_by("-timestamp")
+        comments = Comment.objects.filter(
+            materi__id__in=posts_data.keys()).order_by("-timestamp")
 
         for comment in comments:
             posts_data[comment.materi.id]["comments"].append(comment)
@@ -653,7 +701,13 @@ class RevisiMateriView(TemplateView):
             raise PermissionDenied(request)
 
         current_materi = get_object_or_404(Materi, pk=kwargs["pk"])
-        form = UploadMateriForm(request.POST, request.FILES, instance=current_materi)
+
+        if request.user.is_authenticated and request.user.is_contributor:
+            opened_notif = NotifikasiKontributor.objects.filter(user=request.user, materi=current_materi)
+            opened_notif.delete()
+        
+        form = UploadMateriForm(
+            request.POST, request.FILES, instance=current_materi)
         if form.is_valid():
             RevisiMateriService.revisi_materi(form, request)
             return HttpResponseRedirect("/dashboard/")
@@ -690,7 +744,8 @@ class DownloadHistoryView(TemplateView):
         context = self.get_context_data(**kwargs)
         if request.user.is_authenticated:
             current_user = self.request.user
-            DownloadHistoryService.init_data_authenticated_user(context, current_user)
+            DownloadHistoryService.init_data_authenticated_user(
+                context, current_user)
         else:
             DownloadHistoryService.init_data_guest_user(context, request)
         return self.render_to_response(context)
@@ -707,6 +762,7 @@ def save_to_gdrive(request, pk):
 
     return HttpResponseRedirect(reverse('detail-materi', kwargs={'pk': pk}))
 
+
 class MateriFavorite(TemplateView):
 
     template_name = "user_favorite_materi.html"
@@ -721,7 +777,7 @@ class MateriFavorite(TemplateView):
         user = self.request.user
 
         materi = Materi.objects.filter(like=True)
-        likes_data = { mat.id: { "data": mat, "comments": [] } for mat in materi }
+        likes_data = {mat.id: {"data": mat, "comments": []} for mat in materi}
 
         comments = Comment.objects \
             .filter(materi__id__in=likes_data.keys()) \
@@ -735,11 +791,13 @@ class MateriFavorite(TemplateView):
 
         return self.render_to_response(context=context)
 
+
 class PasswordChangeViews(PasswordChangeView):
 
     from_class = PasswordChangeForm
     success_url = reverse_lazy('password_success')
 
+
 def password_success(request):
     return render(request, 'password_success.html', {})
 
@@ -758,15 +816,18 @@ def see_given_rating(request):
         try:
             if order_by_key[0] == '-':
                 order_by_key = order_by_key[1:]
-            rating_list = Rating.objects.filter(user=request.user).order_by(query_order + order_by_key)
+            rating_list = Rating.objects.filter(
+                user=request.user).order_by(query_order + order_by_key)
         except FieldError:
             order_by_key = 'timestamp'
-            rating_list = Rating.objects.filter(user=request.user).order_by(query_order + order_by_key)
+            rating_list = Rating.objects.filter(
+                user=request.user).order_by(query_order + order_by_key)
 
         return render(request, 'given-rating.html',
                       context={'rating_list': rating_list, 'order_by_key': order_by_key, 'order_by': order_by})
     return permission_denied(request, exception=None)
 
+
 class SubmitVisitorView(TemplateView):
     template_name = "submit_visitor.html"
 
@@ -792,6 +853,21 @@ class SubmitVisitorView(TemplateView):
         SubmitVisitor(msg=title, user_id=user_id, email=email).save()
         return JsonResponse({"success": True, "msg": "Buku tamu berhasil ditambahkan"})
 
+
+class GuestBookView(TemplateView):
+    def get(self, request, *args, **kwargs):
+        form = GuestBookForm()
+        return render(request, 'guest_book.html', {'form': form})
+
+    def post(self, request, *args, **kwargs):
+        name = request.POST.get('name')
+        job = request.POST.get('job')
+        gender = request.POST.get('gender')
+        guestBook = GuestBook(name=name, job=job, gender=gender)
+        guestBook.save()
+        return redirect("daftar_katalog")
+
+
 class ReadLaterView(TemplateView):
     template_name = 'baca-nanti.html'
 
@@ -807,19 +883,22 @@ class ReadLaterView(TemplateView):
     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')
+        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_MESSAGE})
-        
+
         return JsonResponse(ReadLaterService.toggle_read_later(materi_id, request.user))
     else:
         return JsonResponse({"success": False, "msg": UNSUPPORTED_MESSAGE})
 
+
 class StatisticsView(TemplateView):
     template_name = "statistik.html"
 
@@ -840,29 +919,29 @@ class StatisticsView(TemplateView):
                 result.append(e)
 
         chart_data = {
-                'labels': [e.name for e in result],
-                'datasets': [{
-                    'label': 'Jumlah Materi per Kategori',
-                    'data': [e.num for e in result],
-                    'backgroundColor': [
-                        'rgba(255, 99, 132, 0.2)',
-                        'rgba(54, 162, 235, 0.2)',
-                        'rgba(255, 206, 86, 0.2)',
-                        'rgba(75, 192, 192, 0.2)',
-                        'rgba(153, 102, 255, 0.2)',
-                        'rgba(255, 159, 64, 0.2)'
-                    ],
-                    'borderColor': [
-                        'rgba(255, 99, 132, 1)',
-                        'rgba(54, 162, 235, 1)',
-                        'rgba(255, 206, 86, 1)',
-                        'rgba(75, 192, 192, 1)',
-                        'rgba(153, 102, 255, 1)',
-                        'rgba(255, 159, 64, 1)'
-                    ],
-                    'borderWidth': 1
-                }]
-            }
+            'labels': [e.name for e in result],
+            'datasets': [{
+                'label': 'Jumlah Materi per Kategori',
+                'data': [e.num for e in result],
+                'backgroundColor': [
+                    'rgba(255, 99, 132, 0.2)',
+                    'rgba(54, 162, 235, 0.2)',
+                    'rgba(255, 206, 86, 0.2)',
+                    'rgba(75, 192, 192, 0.2)',
+                    'rgba(153, 102, 255, 0.2)',
+                    'rgba(255, 159, 64, 0.2)'
+                ],
+                'borderColor': [
+                    'rgba(255, 99, 132, 1)',
+                    'rgba(54, 162, 235, 1)',
+                    'rgba(255, 206, 86, 1)',
+                    'rgba(75, 192, 192, 1)',
+                    'rgba(153, 102, 255, 1)',
+                    'rgba(255, 159, 64, 1)'
+                ],
+                'borderWidth': 1
+            }]
+        }
         return chart_data
 
     def get(self, request, *args, **kwargs):
@@ -887,6 +966,6 @@ class MostContributor(TemplateView):
 
     def count_contributor(self):
         count_materi = Materi.objects.all.count()
-        content = {'count_materi':count_materi}
+        content = {'count_materi': count_materi}
         print(content)
         return content
diff --git a/authentication/templates/login.html b/authentication/templates/login.html
index e577a9969a799bef368c0deedc0146951c4a0bee..201288988c0b43337af6e000b8824353ebff8819 100644
--- a/authentication/templates/login.html
+++ b/authentication/templates/login.html
@@ -72,6 +72,10 @@
                             <a href="/registrasi" class="txt1">
                                 Ingin jadi kontributor? klik di sini
                             </a>
+                            <br>
+                            <a href="/" class="txt1">
+                                Kembali ke halaman beranda
+                            </a>
                         </div>
                     </div>
 
diff --git a/authentication/templates/login_admin.html b/authentication/templates/login_admin.html
index 15a8d0f482d509c91edaa661e0484084720d29e1..501f7be80ae93ef39cc027e57de6b829708b0f29 100644
--- a/authentication/templates/login_admin.html
+++ b/authentication/templates/login_admin.html
@@ -81,6 +81,10 @@
                                     <a href="/registrasi/admin/" class="txt1">
                                         Belum mendaftar? klik di sini
                                     </a>
+                                    <br>
+                                    <a href="/" class="txt1">
+                                        Kembali ke halaman beranda
+                                    </a>
                                 </div>
         
                             </div>
diff --git a/authentication/views.py b/authentication/views.py
index c33e983e6562b3e40b242cdd54a447a0db57d7ff..852894bf99a187ed02f54d75d4014ef0929c09e9 100644
--- a/authentication/views.py
+++ b/authentication/views.py
@@ -77,3 +77,12 @@ class Login(TemplateView):
 
             return self.do_login(context, email, password, request, result)
 
+from app.models import NotifikasiKontributor
+def kontributor_notif_context(request):
+    if request.user.is_authenticated and request.user.is_contributor:
+        notifs = NotifikasiKontributor.objects.filter(user=request.user)
+        return {
+            'kontributor_notif':notifs
+        }
+    else:
+        return {}
\ No newline at end of file
diff --git a/digipus/__pycache__/settings.cpython-36.pyc b/digipus/__pycache__/settings.cpython-36.pyc
index 43d2d1c4169e7113ebfa287197b9840fd637a652..d738b3ddbb6740940b2b34c9c7651fa292ceac82 100644
Binary files a/digipus/__pycache__/settings.cpython-36.pyc and b/digipus/__pycache__/settings.cpython-36.pyc differ
diff --git a/digipus/settings.py b/digipus/settings.py
index 884c3e67cf7924e4165161d8d50a3ce04361a306..149fd47d1e7cffe34bb483d82aa92296322a6614 100644
--- a/digipus/settings.py
+++ b/digipus/settings.py
@@ -51,6 +51,7 @@ INSTALLED_APPS = [
     "traffic_statistics",
     "forum",
     "rest_framework",
+    'userguide'
 ]
 
 MIDDLEWARE = [
@@ -80,6 +81,8 @@ TEMPLATES = [
                 "django.template.context_processors.request",
                 "django.contrib.auth.context_processors.auth",
                 "django.contrib.messages.context_processors.messages",
+                
+                "authentication.views.kontributor_notif_context"
             ],
         },
     },
@@ -189,4 +192,4 @@ EMAIL_PORT = config('EMAIL_PORT', default=587) # use Google Mail SMTP as default
 EMAIL_HOST_USER = config('EMAIL_HOST_USER', default="pmplclass2020@gmail.com")
 EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default="pmpldigipusemail")
 EMAIL_USE_TLS = True
-EMAIL_USE_SSL = False
\ No newline at end of file
+EMAIL_USE_SSL = False
diff --git a/digipus/urls.py b/digipus/urls.py
index a58c0fb1b8eb9156a99cf3b5c069d84ad380e40a..835b6c47cde1af45963d9f2f8cb79101b5520344 100644
--- a/digipus/urls.py
+++ b/digipus/urls.py
@@ -27,6 +27,7 @@ urlpatterns = [
     path("", include("news.urls"), name="news"),
     path("statistics/", include("traffic_statistics.urls")),
     path("forum/", include("forum.urls")),
+    path("userguide/", include(("userguide.urls")))
 ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
 
 handler403 = 'app.views.permission_denied'
diff --git a/register/templates/register_base.html b/register/templates/register_base.html
index 7befc8c4076485d785f3929f37ca764cffd90bde..f1b5701151b88979105febef60c05658539ce12c 100644
--- a/register/templates/register_base.html
+++ b/register/templates/register_base.html
@@ -1,4 +1,8 @@
-{% load static %}
+
+                                <br>
+                                <a href="/" class="txt1">
+                                    Kembali ke halaman beranda
+                                </a>{% load static %}
 
 <!DOCTYPE html>
 <html lang="en">
@@ -103,6 +107,10 @@
                                 <a href="/login/" class="txt1">
                                     Kembali ke halaman login
                                 </a>
+                                <br>
+                                <a href="/" class="txt1">
+                                    Kembali ke halaman beranda
+                                </a>
                             </div>
                         </div>
 
diff --git a/requirements.txt b/requirements.txt
index 30fd8032d8b3be87ddcb85a8443175668af3dc5f..43d0d26f750678927c647708539ffbe7f4bd9589 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -101,4 +101,4 @@ whitenoise==5.0.1
 wrapt==1.11.2
 xlrd==1.2.0
 XlsxWriter==1.3.6
-zipp==3.1.0
+zipp==3.1.0
\ No newline at end of file
diff --git a/userguide/__init__.py b/userguide/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/userguide/admin.py b/userguide/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e
--- /dev/null
+++ b/userguide/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/userguide/apps.py b/userguide/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..986e41e1270a1f01b590414bfc87a1421f812d06
--- /dev/null
+++ b/userguide/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class UserguideConfig(AppConfig):
+    name = 'userguide'
diff --git a/userguide/migrations/__init__.py b/userguide/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/userguide/models.py b/userguide/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..71a836239075aa6e6e4ecb700e9c42c95c022d91
--- /dev/null
+++ b/userguide/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/userguide/static/css/home.css b/userguide/static/css/home.css
new file mode 100644
index 0000000000000000000000000000000000000000..3961d49bc6c9edfbdf855ca87e198dcc3d8c23e9
--- /dev/null
+++ b/userguide/static/css/home.css
@@ -0,0 +1,37 @@
+.purple {
+    background-color : #615CFD !important;
+    color : #ffffff !important;
+}
+
+.rounded {
+    border-radius: 5px;
+}
+
+.padded {
+    height: 64px;
+    padding: 20px;
+}
+
+li {
+    padding-top: 5px;
+    padding-bottom: 5px;
+}
+
+#userGuideText {
+    font-size: 80px;
+    color: #615CFD;
+    padding-left: 10px;
+}
+
+body {
+    background-color: #F8F8F8;
+}
+
+#changedDiv {
+    background-color: #FFFFFF;
+    padding-top: 10px;
+}
+
+h1 {
+    color: #615CFD;
+}
\ No newline at end of file
diff --git a/userguide/static/js/home.js b/userguide/static/js/home.js
new file mode 100644
index 0000000000000000000000000000000000000000..3cb314307d806922eb4272053ee9e086de6666dc
--- /dev/null
+++ b/userguide/static/js/home.js
@@ -0,0 +1,85 @@
+$("#loginAdmin").click(function(e) {
+    e.preventDefault();
+    ajaxCallToChangeTemplateBasedOnState(10);
+})
+
+$("#approveRejectAdmin").click(function(e) {
+    e.preventDefault();
+    ajaxCallToChangeTemplateBasedOnState(11);
+})
+
+$("#configCategoryAdmin").click(function(e) {
+    e.preventDefault();
+    ajaxCallToChangeTemplateBasedOnState(12);
+})
+
+$("#manageContributorAdmin").click(function(e) {
+    e.preventDefault();
+    ajaxCallToChangeTemplateBasedOnState(13);
+})
+
+$("#manageAdminAdmin").click(function(e) {
+    e.preventDefault();
+    ajaxCallToChangeTemplateBasedOnState(14);
+})
+
+$("#editProfileAdmin, #editProfileContributor, #editProfileUser").click(function(e) {
+    e.preventDefault();
+    ajaxCallToChangeTemplateBasedOnState(15);
+})
+
+$("#loginContributor").click(function(e) {
+    e.preventDefault();
+    ajaxCallToChangeTemplateBasedOnState(21);
+})
+
+$("#uploadMaterialContributor").click(function(e) {
+    e.preventDefault();
+    ajaxCallToChangeTemplateBasedOnState(22);
+})
+
+$("#historyUploadsContributor").click(function(e) {
+    e.preventDefault();
+    ajaxCallToChangeTemplateBasedOnState(23);
+})
+
+$("#lookStatisticCommentsContributor").click(function(e) {
+    e.preventDefault();
+    ajaxCallToChangeTemplateBasedOnState(25);
+})
+
+$("#lookMaterialUser").click(function(e) {
+    e.preventDefault();
+    ajaxCallToChangeTemplateBasedOnState(31);
+})
+
+$("#giveCommentUser").click(function(e) {
+    e.preventDefault();
+    ajaxCallToChangeTemplateBasedOnState(32);
+})
+
+$("#downloadReadMaterials").click(function(e) {
+    e.preventDefault();
+    ajaxCallToChangeTemplateBasedOnState(33);
+})
+
+
+function ajaxCallToChangeTemplateBasedOnState(state){
+    $.ajax({
+        url: '/userguide/',
+        headers: {'X-CSRFToken': csrftoken},
+        data :{
+            'state' : state
+        },
+        dataType : 'html',
+        type : 'POST',
+        success : function(data) {
+            console.log(data)
+            $('#changedDiv').html(data)
+        },
+        error: function(xhr, status, error) {
+            var err = eval("(" + xhr.responseText + ")");
+            alert(err.Message);
+          }
+    })
+}
\ No newline at end of file
diff --git a/userguide/templates/changed.html b/userguide/templates/changed.html
new file mode 100644
index 0000000000000000000000000000000000000000..43e5dc1294ac6036c17c589b79d66270e923834e
--- /dev/null
+++ b/userguide/templates/changed.html
@@ -0,0 +1,108 @@
+{% if state == 0 %}
+Digipus is a system application that can accommodate archiving educational material from regional
+apparatus, academics, and practitioners, and organizing it properly so that it becomes material for
+increasing knowledge and skills for the community at large.
+{% elif state == 10 %}
+    <h1>Login as an admin</h1>
+    <ol>
+        <li>Click on the Login Admin button on the dashboard.</li>
+        <li>Type your credentials and solve the captcha problem.</li>
+        <li>Click “Login”</li>
+    </ol>
+    <h1>Register as an admin</h1>
+    <ol>
+        <li>Click on the Login Admin button on the dashboard.</li>
+        <li>Click on the “Belum mendaftar? klik di sini” Text</li>
+        <li>Type your registration details and click “Daftar”</li>
+        <li>Wait for our internal team to accept your admin account</li>
+    </ol>
+{% elif state == 11 %}
+    <h1>Approve/Reject a Material</h1>
+    <ol>
+        <li>Click on "Administrasi" on the navigation bar</li>
+        <li>Click on "Verifikasi Materi" on the top left</li>
+        <li>Assuming that a material has been submitted, you can click on "Approve" or "Reject" on that particular material</li>
+    </ol>
+{% elif state == 12 %}
+    <h1>Configuration Category and Criteria of Material</h1>
+    <ol>
+        <li>Click on "Administrasi" on the navigation bar</li>
+        <li>Click on "Pengaturan Kategori"</li>
+        <li>To add category, fill in the form and click "Tambahkan"</li>
+        <li>On the List of Categories, you can either edit or delete the category by clicking on “Edit” and “Hapus” respectively</li>
+        <li>You can view the list of categories and erased categories on the table on that page.</li>
+    </ol>
+{% elif state == 13 %}
+    <h1>Manage contributor</h1>
+    <ol>
+        <li>Click on "Administrasi" on the navigation bar</li>
+        <li>Click on "Kelola Contributor"</li>
+        <li>On the “Tabel Daftar Kontributor” table, you can view all contributors. For each contributor, you can either look at the details or delete by pressing “Detail” or “Delete”</li>
+    </ol>
+{% elif state == 14 %}
+    <h1>Manage Admin</h1>
+    <ol>
+        <li>Click on "Administrasi" on the navigation bar</li>
+        <li>Click on "Kelola Admin"</li>
+        <li>On the “Tabel Daftar Kontributor” table, you can view all contributors. For each contributor, you can either look at the details or delete by pressing “Detail” or “Delete”</li>
+    </ol>
+{% elif state == 15 %}
+    <h1>Edit Profile</h1>
+    <ol>
+        <li>Click "Profil" on the top navigation bar</li>
+        <li>Configure your profile from there</li>
+    </ol>
+{% elif state == 21 %}
+    <h1>Login as a kontributor</h1>
+    <ol>
+        <li>Click on the Login Kontributor button on the dashboard.</li>
+        <li>Type your credentials and solve the captcha problem.</li>
+        <li>Click “Login”</li>
+    </ol>
+    <h1>Register as an admin</h1>
+    <ol>
+        <li>Click on the Login Kontributor button on the dashboard.</li>
+        <li>Click on the “Belum mendaftar? klik di sini” Text</li>
+        <li>Type your registration details and click “Daftar”</li>
+    </ol>
+{% elif state == 22 %}
+    <h1>Upload a material</h1>
+    <ol>
+        <li>Click on Dashboard on the Navigation Bar</li>
+        <li>Click on "Unggah Materi" on the top left</li>
+        <li>Fill in the form</li>
+        <li>Click "Simpan"</li>
+        <li>You can also upload through excel by clicking on “Unggah Materi (Excel)” instead of “Unggah Materi” The template for the excel can be downloaded from there</li>
+    </ol>
+{% elif state == 23 %}
+    <h1>History of Uploads</h1>
+    <ol>
+        <li>Click on Dashboard on the Navigation Bar</li>
+        <li>Click on "Riwayat Unggah"</li>
+        <li>For each of the things on the table, you can either view the details or delete by clicking “Detail” or “Hapus” respectively</li>
+    </ol>
+{% elif state == 24 %}
+    <h1>Edit Profile</h1>
+    <ol>
+        <li>Click "Profil" on the top navigation bar</li>
+        <li>Configure your profile from there</li>
+    </ol>
+{% elif state == 25 %}
+    <h1>Look at statistic comments</h1>
+    This feature is under construction
+{% elif state == 31 %}
+    <h1>Look on a material</h1>
+    <ol>
+        <li>Find the material that you want to look at</li>
+        <li>Click on "Detil"</li>
+    </ol>
+{% elif state == 32 %}
+    <h1>Give Comment</h1>
+This feature is under construction
+{% elif state == 33 %}
+    <h1> Download or Read Materials </h1>
+    <ol>
+        <li>Find the material that you want to look at</li>
+        <li>Download or read by clicking on “Unduh” or “Baca” respectively</li>
+    </ol>
+{% endif %}
\ No newline at end of file
diff --git a/userguide/templates/home.html b/userguide/templates/home.html
new file mode 100644
index 0000000000000000000000000000000000000000..02009e8a951d3ee784ee248af18416f2c1302b06
--- /dev/null
+++ b/userguide/templates/home.html
@@ -0,0 +1,82 @@
+{% extends "base.html" %}
+{% load static %}
+{% block header %}
+
+
+<!DOCTYPE html>
+<html>
+    <head>
+        {% csrf_token %}
+        <link href="../../static/app/css/heroic-features.css" rel="stylesheet">
+        <!--===============================================================================================-->
+        <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' %}">
+        <link rel="stylesheet" type="text/css" href="{% static 'css/home.css' %}">
+        <script>
+        const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
+        </script>
+        {% endblock header %}
+        {% block content%}
+    </head>
+    
+    <div class="container">
+        {% include 'app/includes/navbar_katalog_materi.html' %}
+        <div class="row">
+            <div><span id = "userGuideText">USER GUIDE</span></div>
+        </div>
+        <div class="row">
+            <div class="col-5">
+                <a class="btn-block rounded padded purple" data-toggle="collapse" href="#collapseAdmin" role="button" aria-expanded="false" aria-controls="collapseAdmin">
+                    Admin
+                </a>
+                <div class="card">
+                    <div class="collapse" id="collapseAdmin">
+                        <ul>
+                            <li><a href = "" id = "loginAdmin">Login/Register as Admin</a></li>
+                            <li><a href = "" id = "approveRejectAdmin">Approve/Reject a material</a></li>
+                            <li><a href = "" id = "configCategoryAdmin">Configuration Category and Criteria of material</a></li>
+                            <li><a href = "" id = "manageContributorAdmin">Manage Contributor</a></li>
+                            <li><a href = "" id = "manageAdminAdmin">Manage Admin</a></li>
+                            <li><a href = "" id = "editProfileAdmin">Edit Profile</a></li>
+                        </ul>
+                    </div>
+                </div>
+                <a class="btn-block rounded padded purple" data-toggle="collapse" href="#collapseContributor" role="button" aria-expanded="false" aria-controls="collapseContributor">
+                    Contributor
+                </a>
+                <div class="card">
+                    <div class="collapse" id="collapseContributor">
+                        <ul>
+                            <li><a href = "" id = "loginContributor">Login/Register as Contributor</a></li>
+                            <li><a href = "" id = "uploadMaterialContributor">Upload a Material</a></li>
+                            <li><a href = "" id = "historyUploadsContributor">History of Uploads</a></li>
+                            <li><a href = "" id = "editProfileContributor">Edit Profile</a></li>
+                            <li><a href = "" id = "lookStatisticCommentsContributor">Look at Statistic Comments</a></li>
+                        </ul>
+                    </div>
+                </div>
+                <a class="btn-block rounded padded purple" data-toggle="collapse" href="#collapseUser" role="button" aria-expanded="false" aria-controls="collapseContributor">
+                    User
+                </a>
+                <div class="card">
+                    <div class="collapse" id="collapseUser">
+                        <ul>
+                            <li><a href = "" id = "lookMaterialUser">Look on a Material</a></li>
+                            <li><a href = "" id = "giveCommentUser">Give Comment</a></li>
+                            <li><a href = "" id = "downloadReadMaterials">Download or Read Materials</a></li>
+                            <li><a href = "" id = "editProfileUser">Edit Profile</a></li>
+                        </ul>
+                    </div>
+                </div>
+            </div>
+            <div class = "col-7" id = "changedDiv">
+                {% include "changed.html" %}  
+            </div>
+        </div>
+    </div>
+{% endblock content %}
+{% block extra_scripts %}
+<script src="../static/js/home.js"></script>
+{% endblock extra_scripts%}
+</html>
\ No newline at end of file
diff --git a/userguide/tests.py b/userguide/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..57a8e1dd2680343982dd38c5bdf38aea25442fce
--- /dev/null
+++ b/userguide/tests.py
@@ -0,0 +1,35 @@
+from django.test import TestCase
+
+
+# Create your tests here.
+# Tests must start with the keyword test
+# Relevant Resource: https://stackoverflow.com/questions/2037364/django-test-runner-not-finding-tests/4747444
+
+class TestUserGuide(TestCase):
+
+    def test_response_returns_correct_html(self):
+        c = self.client.get('/userguide/')
+        self.assertTemplateUsed(c, 'home.html')
+
+    def test_response_returns_state_variable(self):
+        c = self.client.get('/userguide/')
+        self.assertIn('state', c.context)
+    
+    def test_state_variable_is_int(self):
+        c = self.client.get('/userguide/')
+        state = c.context['state']
+        isinstance(state, int)
+
+    def test_state_sends_request_to_controller_and_receives_state_variable_equal_to_sent_number(self):
+        for number in range(10,34):
+            c = self.client.post('/userguide/', data={'state': number})
+            self.assertEquals(number, c.context['state'])
+    
+    def test_when_state_changed_html_also_changes(self):
+        state_before = 10
+        state_after = 11
+        c_before = self.client.post('/userguide/', data={'state': state_before}).content
+        c_after = self.client.post('/userguide/', data={'state': state_after}).content
+        self.assertNotEquals(c_before, c_after)
+        
+        
\ No newline at end of file
diff --git a/userguide/urls.py b/userguide/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..e58a15584c0f52781d3fd511b2a2660935a3d5ea
--- /dev/null
+++ b/userguide/urls.py
@@ -0,0 +1,6 @@
+from django.urls import path
+from userguide.views import UserGuideHome
+
+urlpatterns = [
+    path("", UserGuideHome, name="user_guide_home"),
+]
diff --git a/userguide/views.py b/userguide/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..76008cd9837ddaea6d276a0122a428aff973fb9b
--- /dev/null
+++ b/userguide/views.py
@@ -0,0 +1,13 @@
+from django.shortcuts import render
+from django.views.decorators.csrf import ensure_csrf_cookie
+from django.http import JsonResponse
+
+
+# Create your views here.
+@ensure_csrf_cookie
+def UserGuideHome(request):
+    state = 0
+    if (request.method == 'POST'):
+        state = int(request.POST['state'])
+        return render(request, 'changed.html', {'state' : state})
+    return render(request, 'home.html', {'state': state})
\ No newline at end of file