diff --git a/administration/migrations/0008_auto_20201009_1829.py b/administration/migrations/0008_auto_20201009_1829.py
new file mode 100644
index 0000000000000000000000000000000000000000..0302d90c1ccf01e6a7a1b313ed4ce7ee95c79573
--- /dev/null
+++ b/administration/migrations/0008_auto_20201009_1829.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1 on 2020-10-09 11:29
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('administration', '0007_auto_20200929_1218'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='verificationreport',
+            name='status',
+            field=models.CharField(choices=[('PENDING', 'Diproses'), ('APPROVE', 'Diterima'), ('DISAPPROVE', 'Ditolak'), ('REVISION', 'Perbaikan'), ('BLOCKED', 'Diblokir')], default='PENDING', max_length=30),
+        ),
+    ]
diff --git a/administration/templates/administration/includes/sidebar.html b/administration/templates/administration/includes/sidebar.html
index 6f639a5e1be5c11e6aa51de3c22edc30491d636f..d8829433f64e515a2c8417285ed53c8792fbfd93 100644
--- a/administration/templates/administration/includes/sidebar.html
+++ b/administration/templates/administration/includes/sidebar.html
@@ -21,6 +21,11 @@
         <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">
 
diff --git a/administration/templates/laporan_materi.html b/administration/templates/laporan_materi.html
new file mode 100644
index 0000000000000000000000000000000000000000..5b92f17dce51124d0ff32ba034ac282131d2954e
--- /dev/null
+++ b/administration/templates/laporan_materi.html
@@ -0,0 +1,114 @@
+{% extends 'administration/base_administrasi2.html' %}
+{% load static %}
+
+{% block title %}
+<title>Laporan Materi | Digipus</title>
+{% endblock %}
+
+{% block content %}
+<!-- Page Heading -->
+<h1 class="h3 mb-2 text-gray-800">Laporan Materi</h1>
+<p class="mb-4">Tekan blokir materi untuk memblokir materi.<br>Klik lihat laporan untuk melihat laporan dari pelapor.</ehp>
+
+<!-- DataTales Example -->
+<div class="card shadow mb-4">
+  <div class="card-header py-3">
+    <div class="d-flex">
+      <div class="mr-auto p-2">
+        <h6 class="m-0 font-weight-bold text-primary">Materi yang Dilaporkan</h6>
+      </div>
+    </div>
+  </div>
+  <div class="card-body">
+    <div class="table-responsive">
+      <table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
+        <thead>
+          <tr>
+            <th scope="col">Judul</th>
+            <th scope="col">Jumlah Laporan</th>
+            <th scope="col">Pilihan</th>
+          </tr>
+        </thead>
+        <tfoot>
+          <tr>
+            <th scope="col">Judul</th>
+            <th scope="col">Jumlah Laporan</th>
+            <th scope="col">Pilihan</th>
+          </tr>
+          </tr>
+        </tfoot>
+        <tbody>
+          {% for materi in materi_dilaporkan %}
+          <tr>
+            <td>{{ materi.title }}</td>
+            <td>{{ materi.jumlah_laporan }}</td>
+            <td class="verif-buttons">
+                <a href="{% url 'administration:laporan-materi-detail' materi.id %}" class="accept-button button-decoration">Lihat Laporan</a>
+                <button type="button" class="reject-button button-decoration" data-toggle="modal" data-target="#confirmModal{{ materi.id }}">Blokir</button>
+                <div class="modal fade" id="confirmModal{{ materi.id }}" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
+                  <div class="modal-dialog" role="document">
+                    <div class="modal-content">
+                      <div class="modal-header">
+                        <h5 class="modal-title" id="exampleModalLabel">Konfirmasi Pemblokiran Materi</h5>
+                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                          <span aria-hidden="true">&times;</span>
+                        </button>
+                      </div>
+                      <div class="modal-body">
+                        <p>Anda akan memblokir materi
+                          <br><br><span style="text-transform: lowercase; font-weight: 900;">{{ materi.title }}</span><br>
+                          email kontributor: {{ materi.uploader.email }}</p>
+                      </div>
+                      <div class="modal-footer">
+                          <a href="{% url 'administration:blok-materi' materi.id %}" id="btn-hapus-{{ materi.id }}" type="button" class="btn btn-secondary">Blokir</a>
+                          <button type="button" class="btn btn-danger" data-dismiss="modal">Batal</button>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+            </td>
+          </tr>
+          {% endfor %}
+        </tbody>
+      </table>
+    </div>
+  </div>
+</div>
+<div class="card shadow mb-4">
+  <div class="card-header py-3">
+    <h6 class="m-0 font-weight-bold text-primary" id="titleTabelPending">Materi yang Diblokir</h6>
+  </div>
+  <div class="card-body">
+    <div class="table-responsive">
+      <table class="table table-bordered" id="dataTablePending" aria-describedby="titleTabelPending">
+        {% if not materi_diblokir %}
+          Tidak ada materi yang diblokir
+        {% else %}
+          <thead>
+            <tr>
+              <th scope="col">Judul</th>
+              <th scope="col">Kontributor</th>
+            </tr>
+          </thead>
+          <tfoot>
+            <tr>
+              <th scope="col">Judul</th>
+              <th scope="col">Kontributor</th>
+            </tr>
+          </tfoot>
+          <tbody>
+            {% for materi in materi_diblokir %}
+            <tr>
+              <td>{{ materi.title }}</td>
+              <td><a href="{% url 'katalog-per-kontributor' materi.uploader.email %}">
+                {{ materi.uploader.name }}
+              </a></td>
+            </tr>
+            {% endfor %}
+          </tbody>
+        {% endif %}
+      </table>
+    </div>
+  </div>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/administration/templates/laporan_materi_detail.html b/administration/templates/laporan_materi_detail.html
new file mode 100644
index 0000000000000000000000000000000000000000..7507bfa6fc85f4d8c682bca6a2e564a30ac64197
--- /dev/null
+++ b/administration/templates/laporan_materi_detail.html
@@ -0,0 +1,55 @@
+{% extends 'administration/base_administrasi2.html' %}
+{% load static %}
+
+{% block title %}
+<title>Laporan Materi | Digipus</title>
+{% endblock %}
+
+{% block content %}
+<!-- Page Heading -->
+<h1 class="h3 mb-2 text-gray-800">Laporan Materi</h1>
+<p class="mb-4">Klik tolak untuk menolak laporan.</ehp>
+
+<!-- DataTales Example -->
+<div class="card shadow mb-4">
+  <div class="card-header py-3">
+    <h6 class="m-0 font-weight-bold text-primary" id="titleTabelPending">
+      <a href="{% url 'detail-materi' materi.id %}">{{ materi.title }}</a></h6>
+  </div>
+  <div class="card-body">
+    <div class="table-responsive">
+      <table class="table table-bordered" id="dataTablePending" aria-describedby="titleTabelPending">
+        {% if not laporan_materi %}
+          Tidak ada laporan untuk materi {{ materi.title }}
+        {% else %}
+          <thead>
+            <tr>
+              <th scope="col">Pelapor</th>
+              <th scope="col">Alasan Melapor</th>
+              <th scope="col">Pilihan</th>
+            </tr>
+          </thead>
+          <tfoot>
+            <tr>
+              <th scope="col">Pelapor</th>
+              <th scope="col">Alasan Melaporr</th>
+              <th scope="col">Pilihan</th>
+            </tr>
+          </tfoot>
+          <tbody>
+            {% for laporan in laporan_materi %}
+            <tr>
+              <td><a href="{% url 'katalog-per-kontributor' laporan.user.email %}">{{ laporan.user.name }}</a></td>
+              <td>{{ laporan.laporan }}</td>
+              <td class="verif-buttons">
+                <a href="{% url 'administration:tolak-laporan' laporan.id %}" class="reject-button button-decoration">Tolak</a>
+              </td>
+            </tr>
+            {% endfor %}
+          </tbody>
+        {% endif %}
+      </table>
+    </div>
+  </div>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/administration/templates/registrasi_admin.html b/administration/templates/registrasi_admin.html
index d4b1011cb70f5b9c1a8df0ebc4afdfb5fc857500..7d3a0151bb82231d953cbcbc96034ec44a3a8710 100644
--- a/administration/templates/registrasi_admin.html
+++ b/administration/templates/registrasi_admin.html
@@ -55,6 +55,11 @@
           <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">
 
diff --git a/administration/templates/setting_verifikasi.html b/administration/templates/setting_verifikasi.html
index e36b1ce9153632ef229f100e58ea25b6cb0c6870..f21254ef032ac3fdae82641d1827a99e6c5403fd 100644
--- a/administration/templates/setting_verifikasi.html
+++ b/administration/templates/setting_verifikasi.html
@@ -64,6 +64,11 @@
           <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">
 
diff --git a/administration/templates/settings.html b/administration/templates/settings.html
index 2df695227dacf6b33136ffbaec744bfc59058910..6381a4e088abdaa87f41c4d531d01183de455015 100644
--- a/administration/templates/settings.html
+++ b/administration/templates/settings.html
@@ -69,6 +69,11 @@
 					>
 				</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" />
 
diff --git a/administration/tests.py b/administration/tests.py
index fff6daef00396bad3176cbc8c260e3d7f6fd27b4..eedbf4c10ca54a788845f82f760fad8496430c14 100644
--- a/administration/tests.py
+++ b/administration/tests.py
@@ -6,7 +6,7 @@ from django.urls import resolve
 from administration import models, views
 from administration.utils import id_generator
 from administration.forms import EditAdminStatusForm
-from app.models import Category, Materi
+from app.models import Category, Materi, LaporanMateri
 from authentication.models import User
 from bs4 import BeautifulSoup
 
@@ -1289,3 +1289,291 @@ class KelolaMateriViewTests(TestCase):
             response.context['materi_list'].filter(id=invalid_id_materi),
             "Materi_list including materi that should not be exists"
         )
+
+
+class LaporanMateriTest(TestCase):
+    def setUp(self):
+        self.client = Client()
+        self.admin_credential = {
+            "email": "admin@gov.id",
+            "password": id_generator()
+        }
+        self.contributor_credential = {
+            "email": "kontributor@gov.id",
+            "password": id_generator()
+        }
+        self.admin = get_user_model().objects.create_user(
+            **self.admin_credential, name="Admin", is_admin=True)
+        self.contributor = get_user_model().objects.create_user(
+            **self.contributor_credential, name="Kontributor", is_contributor=True
+        )
+        self.cover = SimpleUploadedFile("cover.jpg", b"Test file")
+        self.content = SimpleUploadedFile("content.txt", b"Test file")
+        self.materi = Materi.objects.create(
+            title="Ayat-ayat cinta", author="Axel", uploader=self.contributor,
+            publisher="X Prod", descriptions="Kisah kasih cinta",
+            status="APPROVE", cover=self.cover, content=self.content
+        )
+        self.materi2 = Materi.objects.create(
+            title="Cinta ayat-ayat", author="Lexa", uploader=self.contributor,
+            publisher="Y Prod", descriptions="Cinta kasih kisah",
+            status="APPROVE", cover=self.cover, content=self.content
+        )
+        self.laporan = LaporanMateri.objects.create(
+            laporan="materi ini kopas orang lain", user_id=self.contributor.id,
+            materi_id=self.materi.id
+        )
+        self.laporan2 = LaporanMateri.objects.create(
+            laporan="materi ini kopas orang lain", user_id=self.contributor.id,
+            materi_id=self.materi2.id
+        )
+        self.url = "/administration/laporan-materi/"
+    
+    def test_admin_can_access_laporanmateri_page(self):
+        self.client.login(**self.admin_credential)
+        response = self.client.get(self.url)
+        self.assertEqual(response.status_code, 200)
+        self.client.logout()
+
+    def test_contributor_cant_access_laporanmateri_page(self):
+        self.client.login(**self.contributor_credential)
+        response = self.client.get(self.url)
+        self.assertEqual(response.status_code, 403)
+        self.client.logout()
+
+    def test_anonymous_cant_access_laporanmateri_page(self):
+        response = self.client.get(self.url)
+        self.assertEqual(response.status_code, 403)
+    
+    def test_reported_materi_exist(self):
+        self.client.login(**self.admin_credential)
+        response = self.client.get(self.url)
+        bs = BeautifulSoup(response.rendered_content, features="html.parser")
+        el = str(bs.findAll("td"))
+        self.assertIn(self.materi.title, el)
+        self.assertIn(self.materi2.title, el)
+        self.client.logout()
+
+
+class LaporanMateriDetailTest(TestCase):
+    def setUp(self):
+        self.client = Client()
+        self.admin_credential = {
+            "email": "admin@gov.id",
+            "password": id_generator()
+        }
+        self.contributor_credential = {
+            "email": "kontributor@gov.id",
+            "password": id_generator()
+        }
+        self.admin = get_user_model().objects.create_user(
+            **self.admin_credential, name="Admin", is_admin=True)
+        self.contributor = get_user_model().objects.create_user(
+            **self.contributor_credential, name="Kontributor", is_contributor=True
+        )
+        self.cover = SimpleUploadedFile("cover.jpg", b"Test file")
+        self.content = SimpleUploadedFile("content.txt", b"Test file")
+        self.materi = Materi.objects.create(
+            title="Ayat-ayat cinta", author="Axel", uploader=self.contributor,
+            publisher="X Prod", descriptions="Kisah kasih cinta",
+            status="APPROVE", cover=self.cover, content=self.content
+        )
+        self.materi2 = Materi.objects.create(
+            title="Cinta ayat-ayat", author="Lexa", uploader=self.contributor,
+            publisher="Y Prod", descriptions="Cinta kasih kisah",
+            status="APPROVE", cover=self.cover, content=self.content
+        )
+        self.laporan = LaporanMateri.objects.create(
+            laporan="materi ini kopas orang lain", user_id=self.contributor.id,
+            materi_id=self.materi.id
+        )
+        self.laporan2 = LaporanMateri.objects.create(
+            laporan="materi ini kopas orang lain", user_id=self.contributor.id,
+            materi_id=self.materi.id
+        )
+        self.url = "/administration/laporan-materi/"
+
+    def test_admin_can_access_laporanmateridetail_page(self):
+        self.client.login(**self.admin_credential)
+        response = self.client.get(self.url + str(self.laporan.materi_id) + '/')
+        self.assertEqual(response.status_code, 200)
+        self.client.logout()
+
+    def test_contributor_cant_access_laporanmateridetail_page(self):
+        self.client.login(**self.contributor_credential)
+        response = self.client.get(self.url + str(self.laporan.materi_id) + '/')
+        self.assertEqual(response.status_code, 403)
+        self.client.logout()
+
+    def test_anonymous_cant_access_laporanmateridetail_page(self):
+        response = self.client.get(self.url + str(self.laporan.materi_id) + '/')
+        self.assertEqual(response.status_code, 403)
+    
+    def test_cant_access_unknown_laporanmaterialdetail_page(self):
+        self.client.login(**self.admin_credential)
+        response = self.client.get(self.url + '100/')
+        self.assertEqual(response.status_code, 404)
+        self.client.logout()
+    
+    def test_reported_report_exist(self):
+        self.client.login(**self.admin_credential)
+        response = self.client.get(self.url + str(self.laporan.materi_id) + '/')
+        bs = BeautifulSoup(response.rendered_content, features="html.parser")
+        el = str(bs.findAll("td"))
+        self.assertIn(self.laporan.user.name, el)
+        self.assertIn(self.laporan.laporan, el)
+        self.assertIn(self.laporan2.user.name, el)
+        self.assertIn(self.laporan2.laporan, el)
+        self.client.logout()
+
+
+class BlockMateriTest(TestCase):
+    def setUp(self):
+        self.client = Client()
+        self.admin_credential = {
+            "email": "admin@gov.id",
+            "password": id_generator()
+        }
+        self.contributor_credential = {
+            "email": "kontributor@gov.id",
+            "password": id_generator()
+        }
+        self.admin = get_user_model().objects.create_user(
+            **self.admin_credential, name="Admin", is_admin=True
+        )
+        self.contributor = get_user_model().objects.create_user(
+            **self.contributor_credential, name="Kontributor", is_contributor=True
+        )
+        self.cover = SimpleUploadedFile("cover.jpg", b"Test file")
+        self.content = SimpleUploadedFile("content.txt", b"Test file")
+        self.materi = Materi.objects.create(
+            title="Ayat-ayat cinta", author="Axel", uploader=self.contributor,
+            publisher="X Prod", descriptions="Kisah kasih cinta",
+            status="APPROVE", cover=self.cover, content=self.content
+        )
+        self.materi2 = Materi.objects.create(
+            title="Cinta ayat-ayat", author="Lexa", uploader=self.contributor,
+            publisher="Y Prod", descriptions="Cinta kasih kisah",
+            status="APPROVE", cover=self.cover, content=self.content
+        )
+        self.materi3 = Materi.objects.create(
+            title="Ayat cinta ayat", author="ealx", uploader=self.contributor,
+            publisher="Z Prod", descriptions="Kisah cinta kasih",
+            status="PENDING", cover=self.cover, content=self.content
+        )
+        self.laporan = LaporanMateri.objects.create(
+            laporan="materi ini kopas orang lain", user_id=self.contributor.id,
+            materi_id=self.materi.id
+        )
+        self.url = "/administration/blok-materi/"
+    
+    def test_admin_can_block_materi(self):
+        self.client.login(**self.admin_credential)
+        self.assertEqual(self.materi.status, "APPROVE")
+        response = self.client.get(self.url + str(self.materi.id) +'/')
+        self.assertEqual(response.status_code, 302)
+        self.materi.refresh_from_db()
+        self.assertEqual(self.materi.status, "BLOCKED")
+        self.client.logout()
+    
+    def test_contributor_cant_block_materi(self):
+        self.client.login(**self.contributor_credential)
+        self.assertEqual(self.materi.status, "APPROVE")
+        response = self.client.get(self.url + str(self.materi.id) +'/')
+        self.assertEqual(response.status_code, 403)
+        self.materi.refresh_from_db()
+        self.assertEqual(self.materi.status, "APPROVE")
+        self.client.logout()
+    
+    def test_anonymous_cant_block_materi(self):
+        self.assertEqual(self.materi.status, "APPROVE")
+        response = self.client.get(self.url + str(self.materi.id) +'/')
+        self.assertEqual(response.status_code, 403)
+        self.materi.refresh_from_db()
+        self.assertEqual(self.materi.status, "APPROVE")
+    
+    def test_cant_block_unreported_materi(self):
+        self.client.login(**self.admin_credential)
+        self.assertEqual(self.materi2.status, "APPROVE")
+        response = self.client.get(self.url + str(self.materi2.id) +'/')
+        self.assertEqual(response.status_code, 302)
+        self.materi2.refresh_from_db()
+        self.assertEqual(self.materi2.status, "APPROVE")
+        self.client.logout()
+    
+    def test_cant_block_non_approve_status_materi(self):
+        self.client.login(**self.admin_credential)
+        self.assertEqual(self.materi3.status, "PENDING")
+        response = self.client.get(self.url + str(self.materi3.id) +'/')
+        self.assertEqual(response.status_code, 302)
+        self.materi3.refresh_from_db()
+        self.assertEqual(self.materi3.status, "PENDING")
+        self.client.logout()
+    
+    def test_cant_block_unknown_materi(self):
+        self.client.login(**self.admin_credential)
+        response = self.client.get(self.url + '100/')
+        self.assertEqual(response.status_code, 404)
+        self.client.logout()
+
+
+class RejectReportTest(TestCase):
+    def setUp(self):
+        self.client = Client()
+        self.admin_credential = {
+            "email": "admin@gov.id",
+            "password": id_generator()
+        }
+        self.contributor_credential = {
+            "email": "kontributor@gov.id",
+            "password": id_generator()
+        }
+        self.admin = get_user_model().objects.create_user(
+            **self.admin_credential, name="Admin", is_admin=True
+        )
+        self.contributor = get_user_model().objects.create_user(
+            **self.contributor_credential, name="Kontributor", is_contributor=True
+        )
+        self.cover = SimpleUploadedFile("cover.jpg", b"Test file")
+        self.content = SimpleUploadedFile("content.txt", b"Test file")
+        self.materi = Materi.objects.create(
+            title="Ayat-ayat cinta", author="Axel", uploader=self.contributor,
+            publisher="X Prod", descriptions="Kisah kasih cinta",
+            status="APPROVE", cover=self.cover, content=self.content
+        )
+        self.laporan = LaporanMateri.objects.create(
+            laporan="materi ini kopas orang lain", user_id=self.contributor.id,
+            materi_id=self.materi.id
+        )
+        self.url = "/administration/tolak-laporan/"
+    
+    def test_admin_can_reject_report(self):
+        self.client.login(**self.admin_credential)
+        self.assertEqual(self.laporan.is_rejected, False)
+        response = self.client.get(self.url + str(self.laporan.id) +'/')
+        self.assertEqual(response.status_code, 302)
+        self.laporan.refresh_from_db()
+        self.assertEqual(self.laporan.is_rejected, True)
+        self.client.logout()
+    
+    def test_contributor_cant_reject_report(self):
+        self.client.login(**self.contributor_credential)
+        self.assertEqual(self.laporan.is_rejected, False)
+        response = self.client.get(self.url + str(self.laporan.id) +'/')
+        self.assertEqual(response.status_code, 403)
+        self.laporan.refresh_from_db()
+        self.assertEqual(self.laporan.is_rejected, False)
+        self.client.logout()
+    
+    def test_anonymous_cant_reject_report(self):
+        self.assertEqual(self.laporan.is_rejected, False)
+        response = self.client.get(self.url + str(self.laporan.id) +'/')
+        self.assertEqual(response.status_code, 403)
+        self.laporan.refresh_from_db()
+        self.assertEqual(self.laporan.is_rejected, False)
+    
+    def test_cant_reject_unknown_report(self):
+        self.client.login(**self.admin_credential)
+        response = self.client.get(self.url + '100/')
+        self.assertEqual(response.status_code, 404)
+        self.client.logout()
diff --git a/administration/urls.py b/administration/urls.py
index b6970aa97471877910b11ac5ad026cb5d2b14f50..22ba7a66b15ddab39b4e480104eb97d9f11fe12d 100644
--- a/administration/urls.py
+++ b/administration/urls.py
@@ -9,7 +9,9 @@ from administration.views import VerificationView, DetailVerificationView, \
                                  delete_verification, StatisticsView, \
                                  StatisticApiView, EditCategoryView, \
                                  EditAdminStatusView, delete_category, \
-                                 generatedummy, KelolaMateriView
+                                 generatedummy, KelolaMateriView, \
+                                 LaporanMateriView, LaporanMateriDetailView, \
+                                 tolak_laporan, blok_materi
 
 app_name = "administration"
 
@@ -36,5 +38,10 @@ urlpatterns = [
     path("hapus-admin/<int:pk>/", delete_admin),
     path("hapus-kontributor/<int:pk>/", delete_contributor),
     path("kelola-materi/", KelolaMateriView.as_view()),
+    path("laporan-materi/", LaporanMateriView.as_view()),
+    path("laporan-materi/<int:pk>/",
+          LaporanMateriDetailView.as_view(), name="laporan-materi-detail"),
+    path("tolak-laporan/<int:pk>/", tolak_laporan, name="tolak-laporan"),
+    path("blok-materi/<int:pk>/", blok_materi, name="blok-materi"),
     path("generate-dummy", generatedummy),
 ]
diff --git a/administration/views.py b/administration/views.py
index ee97ade6000afb612d6318e07f5a4ec815b91169..864f01e4963587b36be31a21ab89cf0284ad9e97 100644
--- a/administration/views.py
+++ b/administration/views.py
@@ -3,6 +3,7 @@ from datetime import datetime, date
 
 from django.core.exceptions import PermissionDenied
 from django.contrib.auth.hashers import make_password
+from django.db.models import Count
 from django.http import HttpResponseRedirect, JsonResponse
 from django.shortcuts import get_object_or_404, render
 from django.views.generic import TemplateView, View
@@ -11,7 +12,7 @@ from django.utils import timezone, lorem_ipsum
 from dateutil.relativedelta import relativedelta
 from administration.models import VerificationReport, VerificationSetting, DeletionHistory
 from administration.forms import CategoryForm, VerificationSettingForm, RegistrasiAdminForm, PeriodForm, EditAdminStatusForm
-from app.models import Category, Materi, ViewStatistics, DownloadStatistics, Comment, Like, getRandomColor
+from app.models import Category, Materi, ViewStatistics, DownloadStatistics, Comment, Like, getRandomColor, LaporanMateri
 from app.views import permission_denied
 from authentication.models import User
 from datetime import datetime
@@ -21,6 +22,7 @@ from administration.utils import generate_time_step
 from django.core import management
 
 ADMINISTRATION_MANAGEMENT = "/administration/kelola-admin/"
+ADMINISTRATION_REPORT = "/administration/laporan-materi/"
 
 def get_start_end_date(period):
     if period == 'ALL_TIME':
@@ -644,3 +646,58 @@ class KelolaMateriView(TemplateView):
     def get(self, request, *args, **kwargs):
         context = self.get_context_data(**kwargs)
         return self.render_to_response(context)
+
+class LaporanMateriView(TemplateView):
+    template_name = "laporan_materi.html"
+    
+    def dispatch(self, request, *args, **kwargs):   
+        if not request.user.is_authenticated or not request.user.is_admin:
+            raise PermissionDenied(request)
+        return super(LaporanMateriView, self).dispatch(request, *args, **kwargs)
+
+    def get(self, request, *args, **kwargs):
+        context = super(LaporanMateriView, self).get_context_data(**kwargs)
+        context["laporan_materi"] = LaporanMateri.objects.filter(is_rejected=False)
+        context["materi_dilaporkan"] = Materi.objects \
+            .filter(laporanmateri__id__in=context["laporan_materi"], status="APPROVE") \
+            .annotate(jumlah_laporan=Count('laporanmateri__materi_id')) \
+            .order_by('-jumlah_laporan') \
+            .distinct()
+        context["materi_diblokir"] = Materi.objects.filter(status="BLOCKED")
+        return self.render_to_response(context=context)
+
+class LaporanMateriDetailView(TemplateView):
+    template_name = "laporan_materi_detail.html"
+    
+    def dispatch(self, request, *args, **kwargs):   
+        if not request.user.is_authenticated or not request.user.is_admin:
+            raise PermissionDenied(request)
+        return super(LaporanMateriDetailView, self).dispatch(request, *args, **kwargs)
+
+    def get(self, request, *args, **kwargs):
+        context = super(LaporanMateriDetailView, self).get_context_data(**kwargs)
+        context["laporan_materi"] = LaporanMateri.objects.filter(materi_id=kwargs["pk"], is_rejected=False)
+        context["materi"] = get_object_or_404(Materi, id=kwargs["pk"])
+        return self.render_to_response(context=context)
+
+def blok_materi(request, *args, **kwargs):
+    if not request.user.is_authenticated or not request.user.is_admin:
+        raise PermissionDenied(request)
+    
+    materi = get_object_or_404(Materi, pk=kwargs["pk"])
+    if materi.status == "APPROVE" and LaporanMateri.objects.filter(is_rejected=False, materi_id=materi.id):
+        materi.status = "BLOCKED"
+        materi.save()
+
+    return HttpResponseRedirect(ADMINISTRATION_REPORT)
+
+def tolak_laporan(request, *args, **kwargs):
+    if not request.user.is_authenticated or not request.user.is_admin:
+        raise PermissionDenied(request)
+    
+    laporan = get_object_or_404(LaporanMateri, pk=kwargs["pk"])
+    laporan.is_rejected = True
+    laporan.save()
+
+    return HttpResponseRedirect(ADMINISTRATION_REPORT + str(laporan.materi_id))
+
diff --git a/app/migrations/0019_auto_20201009_1829.py b/app/migrations/0019_auto_20201009_1829.py
new file mode 100644
index 0000000000000000000000000000000000000000..15bec11ed71a0b3062e68aeceda2e7434ec804e8
--- /dev/null
+++ b/app/migrations/0019_auto_20201009_1829.py
@@ -0,0 +1,34 @@
+# Generated by Django 3.1 on 2020-10-09 11:29
+
+from django.conf import settings
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('app', '0018_merge_20201009_0700'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='materi',
+            name='status',
+            field=models.CharField(choices=[('PENDING', 'Diproses'), ('APPROVE', 'Diterima'), ('DISAPPROVE', 'Ditolak'), ('REVISION', 'Perbaikan'), ('BLOCKED', 'Diblokir')], default='PENDING', max_length=30),
+        ),
+        migrations.CreateModel(
+            name='LaporanMateri',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('laporan', models.TextField(default='', validators=[django.core.validators.MinValueValidator(30), django.core.validators.MaxValueValidator(120)])),
+                ('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
+                ('is_rejected', models.BooleanField(default=False)),
+                ('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/0019_materi__search_vector.py b/app/migrations/0019_materi__search_vector.py
new file mode 100644
index 0000000000000000000000000000000000000000..bd148040bd07b733963274efadcb1ad9995396c7
--- /dev/null
+++ b/app/migrations/0019_materi__search_vector.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.1 on 2020-10-09 11:19
+
+import django.contrib.postgres.search
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0018_merge_20201009_0700'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='materi',
+            name='_search_vector',
+            field=django.contrib.postgres.search.SearchVectorField(editable=False, null=True),
+        ),
+    ]
diff --git a/app/migrations/0020_merge_20201009_2039.py b/app/migrations/0020_merge_20201009_2039.py
new file mode 100644
index 0000000000000000000000000000000000000000..80867b028075f48fb2cd1b5838561fe9b20aa4cc
--- /dev/null
+++ b/app/migrations/0020_merge_20201009_2039.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.1 on 2020-10-09 13:39
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0019_materi__search_vector'),
+        ('app', '0019_auto_20201009_1829'),
+    ]
+
+    operations = [
+    ]
diff --git a/app/models.py b/app/models.py
index b0333c1da1f631395826008f2b1a75437535b41c..58c85f2772eb29725e6494091137d7c505aeaed8 100644
--- a/app/models.py
+++ b/app/models.py
@@ -1,5 +1,6 @@
 import random
 
+from django.contrib.postgres import search
 from django.core.exceptions import ValidationError
 from django.core.validators import MinValueValidator, MaxValueValidator
 from django.db import models
@@ -12,6 +13,7 @@ VERIFICATION_STATUS = [
     ("APPROVE", "Diterima"),
     ("DISAPPROVE", "Ditolak"),
     ("REVISION", "Perbaikan"),
+    ("BLOCKED", "Diblokir"),
 ]
 
 
@@ -33,35 +35,59 @@ class Category(models.Model):
         return self.name
 
 
+class MateriManager(models.Manager):
+    def search(self, search_text):
+        search_vector = search.SearchVector("title", weight="A")
+        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")
+        )
+
+        return search_result
+
+
 class Materi(models.Model):
     cover = models.ImageField()
     content = models.FileField()
-    title = models.CharField(max_length=50, default='Judul')
-    author = models.CharField(max_length=30, default='Penyusun')
+    title = models.CharField(max_length=50, default="Judul")
+    author = models.CharField(max_length=30, default="Penyusun")
     uploader = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
     publisher = models.CharField(max_length=30, default="Penerbit")
     pages = models.IntegerField(default=0)
     descriptions = models.TextField(default="Deskripsi")
-    status = models.CharField(
-        max_length=30, choices=VERIFICATION_STATUS, default=VERIFICATION_STATUS[0][0])
+    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)
 
+    _search_vector = search.SearchVectorField(null=True, editable=False)
+
+    objects = MateriManager()
+
+    def save(self, *args, **kwargs):
+        super().save(*args, **kwargs)
+
+        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
     def is_published(self):
         published = False
         if self.verificationreport_set.exists():
-            report = self.verificationreport_set.latest('timestamp')
-            published = True if report.status == 'Diterima' else False
+            report = self.verificationreport_set.latest("timestamp")
+            published = True if report.status == "Diterima" else False
         return published
 
     @property
     def published_date(self):
         published_date = None
         if self.verificationreport_set.exists():
-            report = self.verificationreport_set.latest('timestamp')
-            if report.status == 'Diterima':
+            report = self.verificationreport_set.latest("timestamp")
+            if report.status == "Diterima":
                 published_date = report.timestamp
         return published_date
 
@@ -82,8 +108,7 @@ class Comment(models.Model):
     profile = models.CharField(max_length=100, default=getRandomColor)
     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):
@@ -95,22 +120,20 @@ class Like(models.Model):
     timestamp = models.DateTimeField(default=timezone.now)
     session_id = models.CharField(max_length=32, blank=False)
 
+
 class ReqMaterial(models.Model):
     title = models.CharField(max_length=100)
     timestamp = models.DateTimeField(default=timezone.now)
 
 
 class ViewStatistics(models.Model):
-    materi = models.ForeignKey(
-        Materi, models.SET_NULL, null=True, related_name="baca")
+    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)
 
 
@@ -158,4 +181,12 @@ class RatingContributor(models.Model):
         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")
\ No newline at end of file
+            raise ValidationError("Rating score must be integer between 1-5")
+
+
+class LaporanMateri(models.Model):
+    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="")
+    timestamp = models.DateTimeField(default=timezone.now)
+    is_rejected = models.BooleanField(default=False)
diff --git a/app/tests.py b/app/tests.py
index 69dc1d1ccca8fb6288215680807d03fa5d5c617f..f32aaf82aa3344e5128895b4cd64657f0eca4774 100644
--- a/app/tests.py
+++ b/app/tests.py
@@ -33,7 +33,7 @@ from .views import (DaftarKatalog, DashboardKontributorView, DetailMateri,
                     SuksesLoginKontributorView, SuntingProfilView,
                     ProfilAdminView, PostsView, SuntingProfilAdminView, 
                     RevisiMateriView, ReqMateriView, KatalogPerKontributorView,
-                    UploadMateriView, UploadMateriExcelView)
+                    UploadMateriView, UploadMateriExcelView, MateriFavorite)
 from app.forms import SuntingProfilForm
 from app.utils.fileManagementUtil import get_random_filename, remove_image_exifdata
 
@@ -2163,3 +2163,41 @@ class MateriModelTest(TestCase):
 
         Like.objects.create(timestamp=datetime.now(), materi=self.materi, session_id="dummysessionid2")
         self.assertEqual(2, self.materi.like_count)
+
+class MateriFavoriteTest(TestCase):
+
+    def _request_as_user(self):
+        self.client.login(**self.user_credentials)
+        return self.client.get(self.url)
+
+    def test_url_resolves_to_favorite_view(self):
+        found = resolve(self.url)
+        self.assertEqual(found.func.__name__, MateriFavorite.as_view().__name__)
+
+    def test_returns_200_on_authenticated_access(self):
+        response = self._request_as_user()
+        self.assertEqual(response.status_code, 200)
+
+    def test_returns_403_on_unauthenticated_access(self):
+        response = self.client.get(self.url)
+        self.assertRaises(PermissionDenied)
+        self.assertEqual(response.status_code, 403)
+
+        html = response.content.decode("utf-8")
+        self.assertIn(ERROR_403_MESSAGE, html)
+
+    def test_returns_correct_template(self):
+        response = self._request_as_user()
+        self.assertTemplateUsed(response, "user_favorite_materi.html")
+
+    # def test_success_returns_correct_comment_post_groupings_by_context(self):
+    #     post_comment_group_dict = self.data
+
+    #     response = self._request_as_user()
+
+    #     response_user = response.context_data["user"]
+    #     self.assertEqual(response_user, self.user)
+
+    #     response_data = response.context_data["likes"]
+    #     actual_data = post_comment_group_dict
+    #     self.assertDictEqual(response_data, actual_data)
diff --git a/app/views.py b/app/views.py
index 94353f25fa8f3d8365220d22aaf2134d0c30b00f..1fbf229df0ba125f88fbffab32f4b8e1205057b2 100644
--- a/app/views.py
+++ b/app/views.py
@@ -6,34 +6,41 @@ from django.conf import settings
 from django.contrib import messages
 from django.contrib.auth.models import AnonymousUser
 from django.core.exceptions import PermissionDenied, ValidationError
-from django.core.paginator import Paginator
 from django.db.models import Q, Count
-from django.http import (Http404, HttpResponse, HttpResponseRedirect,
-                         JsonResponse)
+from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
 from django.urls import reverse
 from django.shortcuts import get_object_or_404, redirect
 from django.template import loader
-from django.urls import reverse
 from django.views import defaults
 from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
 from django.views.generic import TemplateView
 from administration.models import VerificationReport
 from app.forms import SuntingProfilForm, UploadMateriForm, RatingContributorForm
-from app.models import Category, Comment, Materi, Like, ViewStatistics, DownloadStatistics, ReqMaterial, Rating, \
-    RatingContributor
+from app.models import (
+    Category,
+    Comment,
+    Materi,
+    Like,
+    ViewStatistics,
+    DownloadStatistics,
+    ReqMaterial,
+    Rating,
+    RatingContributor,
+)
 from app.utils.fileManagementUtil import get_random_filename, remove_image_exifdata
 from authentication.models import User
 import django
 import pandas as pd
 from io import BytesIO
-from django.contrib import messages
 from pydrive.auth import GoogleAuth
 from pydrive.drive import GoogleDrive
 from pydrive.auth import AuthenticationRejected
 
-def permission_denied(request, exception, template_name = 'error_403.html'):
+
+def permission_denied(request, exception, template_name="error_403.html"):
     return defaults.permission_denied(request, exception, template_name)
 
+
 class DaftarKatalog(TemplateView):
     paginate_by = 2
     template_name = "app/katalog_materi.html"
@@ -48,16 +55,19 @@ class DaftarKatalog(TemplateView):
         lstMateri = Materi.objects.filter(status="APPROVE").order_by("date_modified")
         url = ""
 
-        getSearch = request.GET.get('search')
+        getSearch = request.GET.get("search")
         if getSearch:
             url = url + "&search={0}".format(getSearch)
-            lstMateri = lstMateri.filter(
-                Q(title__icontains=getSearch) |
-                Q(author__icontains=getSearch) |
-                Q(uploader__name__icontains=getSearch) |
-                Q(descriptions__icontains=getSearch) |
-                Q(publisher__icontains=getSearch)
-            ).distinct()
+            lstMateri = (
+                lstMateri.search(getSearch)
+                .filter(
+                    Q(author__icontains=getSearch)
+                    | Q(uploader__name__icontains=getSearch)
+                    | Q(descriptions__icontains=getSearch)
+                    | Q(publisher__icontains=getSearch)
+                )
+                .distinct()
+            )
 
         getKategori = request.GET.get("kategori")
         if getKategori:
@@ -68,31 +78,32 @@ class DaftarKatalog(TemplateView):
         getSort = request.GET.get("sort")
         if getSort:
             url = url + "&sort={0}".format(getSort)
-            if(getSort == "judul"):
-                lstMateri = lstMateri.order_by('title')
-            elif(getSort == "penulis"):
-                lstMateri = lstMateri.order_by('author')
-            elif(getSort == "pengunggah"):
-                lstMateri = lstMateri.order_by('uploader')
-            elif(getSort == "terbaru"):
-                lstMateri = lstMateri.order_by('-date_created')
-            elif(getSort == "terlama"):
-                lstMateri = lstMateri.order_by('date_created')
-            elif(getSort == "terpopuler"):
-                lstMateri = lstMateri.annotate(count=Count('like__id')).order_by('-count')
+            if getSort == "judul":
+                lstMateri = lstMateri.order_by("title")
+            elif getSort == "penulis":
+                lstMateri = lstMateri.order_by("author")
+            elif getSort == "pengunggah":
+                lstMateri = lstMateri.order_by("uploader")
+            elif getSort == "terbaru":
+                lstMateri = lstMateri.order_by("-date_created")
+            elif getSort == "terlama":
+                lstMateri = lstMateri.order_by("date_created")
+            elif getSort == "terpopuler":
+                lstMateri = lstMateri.annotate(count=Count("like__id")).order_by("-count")
 
         context["materi_list"] = lstMateri
         paginator = Paginator(context["materi_list"], 15)
-        page_number = request.GET.get('page')
+        page_number = request.GET.get("page")
         page_obj = paginator.get_page(page_number)
         context["materi_list"] = page_obj
 
         context["url"] = url
         return self.render_to_response(context=context)
 
+
 class KatalogPerKontributorView(TemplateView):
     template_name = "app/katalog_kontri.html"
-    
+
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
         contributor = get_object_or_404(User, email=kwargs["email"])
@@ -101,22 +112,25 @@ class KatalogPerKontributorView(TemplateView):
 
     def get(self, request, *args, **kwargs):
         context = self.get_context_data(**kwargs)
-         
-        materi_list = Materi.objects.filter(status="APPROVE", uploader=context["contributor"]).order_by("date_modified")
-                    
+
+        materi_list = Materi.objects.filter(status="APPROVE", uploader=context["contributor"]).order_by(
+            "date_modified"
+        )
+
         paginator = Paginator(materi_list, 15)
-        page_number = request.GET.get('page')
+        page_number = request.GET.get("page")
         materi_list_by_page = paginator.get_page(page_number)
         context["materi_list"] = materi_list_by_page
         contributor = get_object_or_404(User, email=kwargs["email"])
-        context["form_rating"] = RatingContributorForm(initial={'user': contributor})
+        context["form_rating"] = RatingContributorForm(initial={"user": contributor})
         return self.render_to_response(context=context)
 
     def post(self, request, *args, **kwargs):
         data = RatingContributorForm(request.POST)
         if data.is_valid():
             data.save()
-        return redirect('katalog-per-kontributor', email=kwargs['email'])
+        return redirect("katalog-per-kontributor", email=kwargs["email"])
+
 
 class DetailMateri(TemplateView):
     template_name = "app/detail_materi.html"
@@ -129,29 +143,27 @@ class DetailMateri(TemplateView):
         context["session_id"] = self.request.session.session_key
         context["materi_data"] = materi
         context["report"] = VerificationReport.objects.filter(materi=materi)
-        context["has_liked"] = Like.objects.filter(
-            materi=materi, session_id=self.request.session.session_key).exists()
-        publishedDate = ''
-        if(materi.published_date == None):
-            publishedDate = 'n.d'
-        else :
-            publishedDate = materi.published_date.strftime('%Y-%m-%d %H:%M')
-        citationAPA = materi.author+' . ('  + publishedDate +') . ' + materi.title +' . '+materi.publisher
+        context["has_liked"] = Like.objects.filter(materi=materi, session_id=self.request.session.session_key).exists()
+        publishedDate = ""
+        if materi.published_date == None:
+            publishedDate = "n.d"
+        else:
+            publishedDate = materi.published_date.strftime("%Y-%m-%d %H:%M")
+        citationAPA = materi.author + " . (" + publishedDate + ") . " + materi.title + " . " + materi.publisher
         context["citationAPA"] = citationAPA
         context["citationIEEE"] = get_citation_ieee(self.request, materi)
-        context['materi_rating_score'] = 0
+        context["materi_rating_score"] = 0
 
         if self.request.user.is_authenticated:
             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
+                context["materi_rating_score"] = materi_rating.score
 
         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_comment = Comment.objects.filter(materi=context["materi_data"])
         context["comment_data"] = query_set_for_comment
         return self.render_to_response(context=context)
 
@@ -171,31 +183,29 @@ class DetailMateri(TemplateView):
             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
             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
-        comment = Comment.objects.create(comment=commentText,
-                                         username=self.get_user_name(request), materi=materi, user=user_obj)
+        comment = Comment.objects.create(
+            comment=commentText, username=self.get_user_name(request), materi=materi, user=user_obj
+        )
         comment.save()
         return HttpResponseRedirect(request.path)
 
 
 def toggle_like(request):
-    if request.method == 'POST':
-        materi_id = request.POST.get('materi_id', None)
-        session_id = request.POST.get('session_id', None)
+    if request.method == "POST":
+        materi_id = request.POST.get("materi_id", None)
+        session_id = request.POST.get("session_id", None)
         if materi_id is None or session_id is None:
             return JsonResponse({"success": False, "msg": "Missing parameter"})
         materi = get_object_or_404(Materi, pk=materi_id)
-        has_liked = Like.objects.filter(
-            materi=materi, session_id=session_id).exists()
+        has_liked = Like.objects.filter(materi=materi, session_id=session_id).exists()
         if has_liked:
-            like = get_object_or_404(
-                Like, materi=materi, session_id=session_id)
+            like = get_object_or_404(Like, materi=materi, session_id=session_id)
             like.delete()
             return JsonResponse({"success": True, "liked": True})
         else:
@@ -207,47 +217,61 @@ def toggle_like(request):
 
 def delete_comment(request, pk_materi, pk_comment):
     comment = get_object_or_404(Comment, pk=pk_comment)
-    url = '/materi/' + str(pk_materi) + "/"
+    url = "/materi/" + str(pk_materi) + "/"
     comment.delete()
     return HttpResponseRedirect(url)
 
+
 def get_citation_ieee(request, materi):
     current_date = datetime.datetime.now()
     current_day = str(current_date.day)
     current_month = current_date.strftime("%b")
     current_year = str(current_date.year)
     published_date = ""
-    if(materi.published_date == None):
+    if materi.published_date == None:
         published_date = "n.d"
-    else :
-        published_date = materi.published_date.strftime('%Y')
-    
+    else:
+        published_date = materi.published_date.strftime("%Y")
+
     author_list = materi.author.split(",")
     author_list_abbrv = ""
     for author_name in author_list:
         author_name_split = author_name.split(" ")
         author_name_abbrv = ""
         for j, name in enumerate(author_name_split):
-            if j < (len(author_name_split)-1):
+            if j < (len(author_name_split) - 1):
                 abbrv_name = name[0].upper()
                 author_name_abbrv = author_name_abbrv + abbrv_name + ". "
             else:
                 author_name_abbrv = author_name_abbrv + name
         author_list_abbrv = author_list_abbrv + author_name_abbrv + ", "
 
-    citation_result = author_list_abbrv + \
-                      materi.title + ". " + \
-                      materi.publisher + ", " + published_date + ". " + \
-                      "Accessed on: " + current_month + ". " + current_day + ", " + current_year + \
-                      ". [Online]. " + \
-                      "Available: " + request.build_absolute_uri()
+    citation_result = (
+        author_list_abbrv
+        + materi.title
+        + ". "
+        + materi.publisher
+        + ", "
+        + published_date
+        + ". "
+        + "Accessed on: "
+        + current_month
+        + ". "
+        + current_day
+        + ", "
+        + current_year
+        + ". [Online]. "
+        + "Available: "
+        + request.build_absolute_uri()
+    )
     return citation_result
 
+
 def add_rating_materi(request):
-    if request.method == 'POST' and request.user.is_authenticated:
+    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)
+        materi_id = request.POST.get("materi_id", None)
+        rating_score = request.POST.get("rating_score", None)
 
         if materi_id is None or rating_score is None:
             return JsonResponse({"success": False, "msg": "Missing param"}, status=422)
@@ -273,8 +297,9 @@ def add_rating_materi(request):
             return JsonResponse({"success": False, "msg": "Rating already exist"}, status=409)
 
         Rating(materi=materi, user=request.user, score=rating_score).save()
-        return JsonResponse({"success": True, "msg": "Rating successfully created", "rating_score": rating_score},
-                            status=201)
+        return JsonResponse(
+            {"success": True, "msg": "Rating successfully created", "rating_score": rating_score}, status=201
+        )
     return JsonResponse({"success": False, "msg": "Forbidden"}, status=403)
 
 
@@ -287,15 +312,14 @@ def download_materi(request, pk):
             mimetype = mimetypes.guess_type(file_path)
             with open(file_path, "rb") as fh:
                 response = HttpResponse(fh.read(), content_type=mimetype[0])
-                response["Content-Disposition"] = "attachment; filename=" + \
-                    os.path.basename(file_path)
+                response["Content-Disposition"] = "attachment; filename=" + os.path.basename(file_path)
                 if request.user.is_authenticated:
                     DownloadStatistics(materi=materi, downloader=request.user).save()
                 else:
                     downloaded_materi = DownloadStatistics.objects.create(materi=materi)
-                    if 'downloaded_materi' not in request.session:
-                        request.session['downloaded_materi'] = []
-                    request.session['downloaded_materi'].append(downloaded_materi.pk)
+                    if "downloaded_materi" not in request.session:
+                        request.session["downloaded_materi"] = []
+                    request.session["downloaded_materi"].append(downloaded_materi.pk)
                     request.session.modified = True
                 return response
         except Exception as e:
@@ -313,8 +337,7 @@ def view_materi(request, pk):
         try:
             with open(file_path, "rb") as fh:
                 response = HttpResponse(fh.read(), content_type=mimetype[0])
-                response["Content-Disposition"] = "inline; filename=" + \
-                    os.path.basename(file_path)
+                response["Content-Disposition"] = "inline; filename=" + os.path.basename(file_path)
                 ViewStatistics(materi=materi).save()
                 return response
         except Exception as e:
@@ -338,18 +361,17 @@ class UploadMateriView(TemplateView):
         if form.is_valid():
             materi = form.save(commit=False)
             materi.uploader = request.user
-            konten = form.cleaned_data['content']
+            konten = form.cleaned_data["content"]
             try:
                 self.validate_file_extension(konten)
             except ValidationError:
                 messages.error(request, "Materi gagal diunggah, format file tidak sesuai")
                 return HttpResponseRedirect("/unggah/")
             materi.save()
-            kateg = form.cleaned_data['categories']
+            kateg = form.cleaned_data["categories"]
             for i in kateg:
                 materi.categories.add(i)
-            messages.success(
-                request, "Materi berhasil diunggah, periksa riwayat unggah anda")
+            messages.success(request, "Materi berhasil diunggah, periksa riwayat unggah anda")
             return HttpResponseRedirect("/unggah/")
         else:
             context = self.get_context_data(**kwargs)
@@ -360,16 +382,16 @@ class UploadMateriView(TemplateView):
     def get(self, request, *args, **kwargs):
         if request.user.is_authenticated == False or not request.user.is_contributor:
             raise PermissionDenied(request)
-        
+
         context = self.get_context_data(**kwargs)
         context["form"] = UploadMateriForm
         return self.render_to_response(context)
 
     def validate_file_extension(self, value):
         ext = os.path.splitext(value.name)[1]  # [0] returns path+filename
-        valid_extensions = ['.pdf', '.doc', '.docx', '.jpg', '.png', '.xlsx', '.xls', '.mp4', '.mp3']
+        valid_extensions = [".pdf", ".doc", ".docx", ".jpg", ".png", ".xlsx", ".xls", ".mp4", ".mp3"]
         if not ext.lower() in valid_extensions:
-            raise ValidationError('Unsupported file extension.')
+            raise ValidationError("Unsupported file extension.")
 
 
 class UploadMateriHTML(TemplateView):
@@ -393,23 +415,19 @@ class UploadMateriExcelView(TemplateView):
 
     def get(self, request, *args, **kwargs):
 
-        if 'template' in self.request.GET:
+        if "template" in self.request.GET:
 
-            data_frame = pd.DataFrame({
-                'Title': [],
-                'Author': [],
-                'Publisher': [],
-                'Categories': [],
-                'Description': [],
-            })
+            data_frame = pd.DataFrame(
+                {"Title": [], "Author": [], "Publisher": [], "Categories": [], "Description": [],}
+            )
 
             with BytesIO() as b:
-                writer = pd.ExcelWriter(b, engine='xlsxwriter') #pylint: disable=abstract-class-instantiated
+                writer = pd.ExcelWriter(b, engine="xlsxwriter")  # pylint: disable=abstract-class-instantiated
                 data_frame.to_excel(writer, index=0)
                 writer.save()
                 response = HttpResponse(
-                        b.getvalue(), 
-                        content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+                    b.getvalue(), content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+                )
 
                 response["Content-Disposition"] = "attachment; filename=template.xlsx"
 
@@ -419,66 +437,65 @@ class UploadMateriExcelView(TemplateView):
             context = self.get_context_data(**kwargs)
             return self.render_to_response(context)
 
-
     def post(self, request, *args, **kwargs):
-        excel_file = request.FILES['excel']
-        excel = pd.read_excel(excel_file)        
+        excel_file = request.FILES["excel"]
+        excel = pd.read_excel(excel_file)
 
-        row,lines = excel.shape
+        row, lines = excel.shape
         categories = Category.objects.all()
-        
+
         field_length = {
-            'title' : 50,
-            'author' : 30,
-            'publisher' : 30,
+            "title": 50,
+            "author": 30,
+            "publisher": 30,
         }
 
         message = None
 
         # First pass, validate input
         for i in range(row):
-            
+
             # Validate Categories
-            for c in excel['Categories'][i].split(","):
+            for c in excel["Categories"][i].split(","):
                 sel_cat = categories.filter(name=c)
                 if sel_cat.count() == 0:
                     message = f"Kategori %s tidak ditemukan" % c
                     break
 
-            if len(excel['Title'][i]) > field_length['title']:
-                message = f"Title maksimal %d karakter" % field_length['title']
+            if len(excel["Title"][i]) > field_length["title"]:
+                message = f"Title maksimal %d karakter" % field_length["title"]
 
-            if len(excel['Author'][i]) > field_length['author']:
-                message = f"Author maksimal %d karakter" % field_length['author']
+            if len(excel["Author"][i]) > field_length["author"]:
+                message = f"Author maksimal %d karakter" % field_length["author"]
 
-            if len(excel['Publisher'][i]) > field_length['publisher']:
-                message = f"Publisher maksimal %d karakter" % field_length['publisher']
+            if len(excel["Publisher"][i]) > field_length["publisher"]:
+                message = f"Publisher maksimal %d karakter" % field_length["publisher"]
 
             if message != None:
                 break
-            
+
         if message != None:
             messages.error(request, message)
-            return HttpResponseRedirect('/unggah_excel/')
+            return HttpResponseRedirect("/unggah_excel/")
 
         # Second pass, save data
         with django.db.transaction.atomic():
             for i in range(row):
                 materi = Materi(
-                            title=excel['Title'][i],
-                            author=excel['Author'][i],
-                            publisher=excel['Publisher'][i],
-                            descriptions=excel['Description'][i],
-                            uploader=request.user
-                        )
+                    title=excel["Title"][i],
+                    author=excel["Author"][i],
+                    publisher=excel["Publisher"][i],
+                    descriptions=excel["Description"][i],
+                    uploader=request.user,
+                )
                 materi.save()
-                
-                for c in excel['Categories'][i].split(","):
+
+                for c in excel["Categories"][i].split(","):
                     materi.categories.add(categories.get(name=c))
 
-        messages.success(request, 'Materi berhasil diunggah')
+        messages.success(request, "Materi berhasil diunggah")
 
-        return HttpResponseRedirect('/unggah_excel/')
+        return HttpResponseRedirect("/unggah_excel/")
 
 
 class DashboardKontributorView(TemplateView):
@@ -490,8 +507,7 @@ 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):
@@ -567,24 +583,22 @@ 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:
-                f_name = request.FILES['profile_picture'].name
+                f_name = request.FILES["profile_picture"].name
                 f_name = get_random_filename(f_name)
                 f_path = settings.MEDIA_ROOT + "/" + f_name
-                request.FILES['profile_picture'].name = f_name
+                request.FILES["profile_picture"].name = f_name
 
-                form = SuntingProfilForm(
-            request.POST, request.FILES, instance=current_user)
+                form = SuntingProfilForm(request.POST, request.FILES, instance=current_user)
                 form.save()
 
                 remove_image_exifdata(f_path)
-            else: 
+            else:
                 form.save()
             return HttpResponseRedirect("/profil/")
         else:
@@ -602,8 +616,7 @@ class SuntingProfilAdminView(TemplateView):
         return super(SuntingProfilAdminView, self).dispatch(request, *args, **kwargs)
 
     def get_context_data(self, **kwargs):
-        context = super(SuntingProfilAdminView,
-                        self).get_context_data(**kwargs)
+        context = super(SuntingProfilAdminView, self).get_context_data(**kwargs)
         return context
 
     def get(self, request, *args, **kwargs):
@@ -621,24 +634,22 @@ class SuntingProfilAdminView(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:
-                f_name = request.FILES['profile_picture'].name
+                f_name = request.FILES["profile_picture"].name
                 f_name = get_random_filename(f_name)
                 f_path = settings.MEDIA_ROOT + "/" + f_name
-                request.FILES['profile_picture'].name = f_name
+                request.FILES["profile_picture"].name = f_name
 
-                form = SuntingProfilForm(
-            request.POST, request.FILES, instance=current_user)
+                form = SuntingProfilForm(request.POST, request.FILES, instance=current_user)
                 form.save()
 
                 remove_image_exifdata(f_path)
-            else: 
+            else:
                 form.save()
             return HttpResponseRedirect("/profil-admin/")
         else:
@@ -646,6 +657,7 @@ class SuntingProfilAdminView(TemplateView):
             context["form"] = form
             return self.render_to_response(context)
 
+
 class ReqMateriView(TemplateView):
     template_name = "req_materi.html"
 
@@ -655,8 +667,7 @@ class ReqMateriView(TemplateView):
         return super(ReqMateriView, self).dispatch(request, *args, **kwargs)
 
     def get_context_data(self, **kwargs):
-        context = super(ReqMateriView,
-                        self).get_context_data(**kwargs)
+        context = super(ReqMateriView, self).get_context_data(**kwargs)
         return context
 
     def get(self, request, *args, **kwargs):
@@ -664,7 +675,7 @@ class ReqMateriView(TemplateView):
         return self.render_to_response(context)
 
     def post(self, request, *args, **kwargs):
-        title = request.POST.get('title', None)
+        title = request.POST.get("title", None)
         if title is None:
             return JsonResponse({"success": False, "msg": "Missing parameter"})
         ReqMaterial(title=title).save()
@@ -680,8 +691,7 @@ 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):
@@ -728,11 +738,9 @@ 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)
@@ -762,19 +770,18 @@ class RevisiMateriView(TemplateView):
         context = self.get_context_data(**kwargs)
         return self.render_to_response(context)
 
-    def post(self,request, *args, **kwargs):
+    def post(self, request, *args, **kwargs):
         if request.user.is_authenticated == False:
             raise PermissionDenied(request)
 
         current_materi = get_object_or_404(Materi, pk=kwargs["pk"])
-        form = UploadMateriForm(
-            request.POST, request.FILES, instance=current_materi)
+        form = UploadMateriForm(request.POST, request.FILES, instance=current_materi)
         if form.is_valid():
             materi = form.save(commit=False)
             materi.uploader = request.user
             materi.status = "REVISION"
             materi.save()
-            kateg = form.cleaned_data['categories']
+            kateg = form.cleaned_data["categories"]
             for i in kateg:
                 materi.categories.add(i)
             materi.save()
@@ -784,6 +791,7 @@ class RevisiMateriView(TemplateView):
             context["form_revisi"] = form
             return self.render_to_response(context)
 
+
 def pages(request):
     context = {}
     # All resource paths end in .html.
@@ -798,31 +806,31 @@ def pages(request):
 
         template = loader.get_template("error-404.html")
         return HttpResponse(template.render(context, request))
-		
+
+
 class DownloadHistoryView(TemplateView):
     template_name = "download_history.html"
 
     def get_context_data(self, **kwargs):
-        context = super(DownloadHistoryView,
-                        self).get_context_data(**kwargs)
+        context = super(DownloadHistoryView, self).get_context_data(**kwargs)
         return context
 
     def get(self, request, *args, **kwargs):
         context = self.get_context_data(**kwargs)
         if request.user.is_authenticated:
             current_user = self.request.user
-            riwayat_list = current_user.riwayat_unduh.all().order_by('-timestamp')
+            riwayat_list = current_user.riwayat_unduh.all().order_by("-timestamp")
             context["riwayat_list"] = riwayat_list
             context["user_name"] = current_user.name
         else:
-            has_downloaded_materi = 'downloaded_materi' in request.session
-            downloaded_materi = request.session['downloaded_materi'] if has_downloaded_materi else []
-            riwayat_list = DownloadStatistics.objects.filter(
-            pk__in=downloaded_materi).order_by('-timestamp')
+            has_downloaded_materi = "downloaded_materi" in request.session
+            downloaded_materi = request.session["downloaded_materi"] if has_downloaded_materi else []
+            riwayat_list = DownloadStatistics.objects.filter(pk__in=downloaded_materi).order_by("-timestamp")
             context["riwayat_list"] = riwayat_list
-            context["user_name"] = 'Guest'
+            context["user_name"] = "Guest"
         return self.render_to_response(context)
 
+
 def upload_to_gdrive(file_path, title):
     gauth = GoogleAuth()
     gauth.LocalWebserverAuth()
@@ -830,10 +838,11 @@ def upload_to_gdrive(file_path, title):
     drive = GoogleDrive(gauth)
     file1 = drive.CreateFile()
     file1.SetContentFile(file_path)
-    file1['title'] = title
-    print('title: %s, mimeType: %s' % (file1['title'], file1['mimeType']))
+    file1["title"] = title
+    print("title: %s, mimeType: %s" % (file1["title"], file1["mimeType"]))
     file1.Upload()
 
+
 def save_to_gdrive(request, pk):
     materi = get_object_or_404(Materi, pk=pk)
     path = materi.content.path
@@ -872,4 +881,4 @@ class MateriFavorite(TemplateView):
         context["user"] = user
         context["likes"] = likes_data
 
-        return self.render_to_response(context=context)
\ No newline at end of file
+        return self.render_to_response(context=context)
diff --git a/pyproject.toml b/pyproject.toml
index a70904f11f778a303dac2180fec540896169b88b..05df741509f11bbfe88ab75b26b7a6e836637ddf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,5 +12,6 @@ exclude = '''
     | buck-out
     | build
     | dist
+    | migrations
 )/
 '''
\ No newline at end of file