diff --git a/.gitignore b/.gitignore
index 60f5748be92a8d9c26883ec23e60d30ffcb5a7f9..9298ea62c6ef1853f27957a63d1b73c69d00b8d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -204,4 +204,5 @@ pip-selfcheck.json
 __pycache__/
 db.sqlite3
 .coverage
-htmlcov/
\ No newline at end of file
+htmlcov/
+flowchart/
\ No newline at end of file
diff --git a/administration/migrations/0004_auto_20200517_1713.py b/administration/migrations/0004_auto_20200517_1713.py
new file mode 100644
index 0000000000000000000000000000000000000000..8189cd965de7bc2ddf6453176ec3ec75c07fc7f9
--- /dev/null
+++ b/administration/migrations/0004_auto_20200517_1713.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.0.3 on 2020-05-17 10:13
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('administration', '0003_verificationreport_user'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='verificationreport',
+            name='timestamp',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+    ]
diff --git a/administration/models.py b/administration/models.py
index 628ad75f41109d08d7839f0112197a95a121d995..0e4ea37b8ee9467fe1c345a6551ecde21c589caa 100644
--- a/administration/models.py
+++ b/administration/models.py
@@ -1,5 +1,6 @@
 from django.contrib.postgres.fields import JSONField
 from django.db import models
+from django.utils import timezone
 
 from app.models import VERIFICATION_STATUS, Materi
 from authentication.models import User
@@ -15,6 +16,6 @@ class VerificationReport(models.Model):
     report = JSONField()
     materi = models.ForeignKey(Materi, models.SET_NULL, null=True)
     user = models.ForeignKey(User, models.SET_NULL, null=True)
-    timestamp = models.DateTimeField(auto_now_add=True)
+    timestamp = models.DateTimeField(default=timezone.now)
     status = models.CharField(
         max_length=30, choices=VERIFICATION_STATUS, default=VERIFICATION_STATUS[0][0])
diff --git a/administration/views.py b/administration/views.py
index 8180c2b098d6bcbcbb9ac5d79535a476a424efaa..f840e676f774f4214b53fb9c4d65a997e7654e88 100644
--- a/administration/views.py
+++ b/administration/views.py
@@ -75,11 +75,9 @@ class DetailVerificationView(TemplateView):
 
         if action == "approve" and feedback != "":
             materi.status = "APPROVE"
-            materi.feedback = feedback
             materi.save()
         elif action == "disapprove" and feedback != "":
             materi.status = "DISAPPROVE"
-            materi.feedback = feedback
             materi.save()
         else:
             context = self.get_context_data(**kwargs)
diff --git a/app/management/commands/__init__.py b/app/management/commands/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/app/management/commands/generatedummy.py b/app/management/commands/generatedummy.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff917c4e2c5bbe5093c3e7385001e5c02be0467a
--- /dev/null
+++ b/app/management/commands/generatedummy.py
@@ -0,0 +1,207 @@
+from datetime import datetime, timedelta
+from math import floor, ceil
+from random import randint, choice, choices, sample
+from typing import List
+
+from django.core.management.base import BaseCommand
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.utils import timezone
+
+from app.models import Materi, Category
+from app.management.commands.utils import SECONDS_IN_DAY, get_time_before, get_random_datetime, generate_list_of_random_datetime
+from administration.models import VerificationReport, VerificationSetting
+from authentication.models import User
+
+
+class Command(BaseCommand):
+    help = 'Generate dummy data for seeding.'
+
+    def add_arguments(self, parser):
+        parser.add_argument('num_of_materi', type=int,
+                            help="Number of materi to create")
+
+    def _generate_category(self, num_of_category: int = 5):
+        counter = 0
+        category_name = f"Kategori dummy {counter}"
+        dummy_category = []
+        for i in range(num_of_category):
+            while (Category.objects.filter(name=category_name).exists()):
+                counter += 1
+                category_name = f"Kategori dummy {counter}"
+            category = Category(name=category_name,
+                                description=f"{category_name}.")
+            category.save()
+            dummy_category.append(category)
+        return dummy_category
+
+    def _generate_verifivation_criteria(self, num_of_criteria: int = 5):
+        counter = 0
+        criterion_name = f"Kriteria dummy {counter}"
+        dummy_criteria = []
+        for i in range(num_of_criteria):
+            while (VerificationSetting.objects.filter(title=criterion_name).exists()):
+                counter += 1
+                criterion_name = f"Kriteria dummy {counter}"
+            criterion = VerificationSetting(title=criterion_name,
+                                            description=f"{criterion_name}.")
+            criterion.save()
+            dummy_criteria.append(criterion)
+        return dummy_criteria
+
+    def _generate_cover(self):
+        
+        cover = SimpleUploadedFile(
+            "cover.jpg",
+            b"Test file"
+        )
+        return cover
+
+    def _generate_content(self):
+        content = SimpleUploadedFile(
+            "content.txt",
+            b"Test file"
+        )
+        return content
+
+    def _generate_admin(self, num_of_user: int = 2):
+        counter = 0
+        dummy_user = []
+        email = f"admin-dummy-{counter}@email.com"
+        end_date = timezone.now()
+        for i in range(num_of_user):
+            while (User.objects.filter(email=email).exists()):
+                counter += 1
+                email = f"admin-dummy-{counter}@email.com"
+            name = f"admin-dummy-{counter}"
+            user = User(email=email, name=name, is_admin=True)
+            user.set_password(name)
+            user.date_joined = get_time_before(
+                timezone.now(), (365*SECONDS_IN_DAY))
+            user.save()
+            dummy_user.append(user)
+        return dummy_user
+
+    def _generate_contributor(self, max_num_of_user: int = 10):
+        counter = 0
+        dummy_user = []
+        email = f"kontributor-dummy-{counter}@email.com"
+        now = timezone.now() - timedelta(days=30)
+        last_year = timezone.now() - timedelta(days=365)
+        dates = generate_list_of_random_datetime(
+            last_year, now, max_num_of_user)
+        self.first_created_contributor = dates[0]
+        for date_joined in dates:
+            while (User.objects.filter(email=email).exists()):
+                counter += 1
+                email = f"kontributor-dummy-{counter}@email.com"
+            name = f"kontributor-dummy-{counter}"
+            user = User(email=email, name=name, is_contributor=True)
+            user.set_password(name)
+            user.date_joined = date_joined
+            user.save()
+            dummy_user.append(user)
+        return dummy_user
+
+    def _generate_materi(self, num_of_materi: int):
+        counter = 0
+        materi_name = "Materi Dummy {}"
+        materi_list = []
+        now = timezone.now() - timedelta(days=30)
+        dates = generate_list_of_random_datetime(
+            self.first_created_contributor, now, num_of_materi)
+        for date_created in dates:
+            contributor = choice(
+                [i for i in self.contributors if i.date_joined < date_created])
+            while Materi.objects.filter(title=materi_name.format(counter)).exists():
+                counter += 1
+            materi = Materi(title=materi_name.format(counter), author=contributor.name, uploader=contributor,
+                            publisher=f"Penerbit {contributor.name}", descriptions=f"Deskripsi {materi_name.format(counter)}.",
+                            status="PENDING", cover=self.cover, content=self.content, date_created=date_created)
+            materi.save()
+            category = sample(self.categories, k=2)
+            materi.categories.add(*category)
+            materi.save()
+            materi_list.append(materi)
+
+            # Validasi
+            status = self._get_status()
+            if status != "PENDING":
+                timestamp = get_random_datetime(
+                    date_created, date_created+timedelta(days=7))
+                admin = choice(self.admins)
+                report_field = {}
+                if status == "APPROVE":
+                    report_field = self._generate_report(approved=True)
+                    materi.status = "APPROVE"
+                elif status == "DISAPPROVE":
+                    report_field = self._generate_report(approved=False)
+                    materi.status = "DISAPPROVE"
+                materi.save()
+                verif_report = VerificationReport(
+                    report=report_field, materi=materi, user=admin, status=materi.get_status_display(), timestamp=timestamp)
+                verif_report.save()
+            else:
+                pass
+
+        return materi
+
+    def _verify_count(self, num_of_materi: int, reject_rate: float = 0.15, pending_rate: float = 0.2):
+        num_of_reject = floor(reject_rate * num_of_materi)
+        num_of_pending = floor(pending_rate * num_of_materi)
+        num_of_approve = num_of_materi - (num_of_reject + num_of_pending)
+        return (num_of_reject, num_of_pending, num_of_approve)
+
+    def _get_status(self):
+        choices = []
+        if self.num_of_approve > 0:
+            choices.append("APPROVE")
+        if self.num_of_reject > 0:
+            choices.append("DISAPPROVE")
+        if self.num_of_pending > 0:
+            choices.append("PENDING")
+        status = choice(choices)
+        if status == "APPROVE":
+            self.num_of_approve -= 1
+        elif status == "DISAPPROVE":
+            self.num_of_reject -= 1
+        else:
+            self.num_of_pending -= 1
+        return status
+
+    def _generate_report(self, approved):
+        report = {}
+        kriteria_list = []
+        for kriteria in self.criteria:
+            kriteria_list.append({
+                "title": kriteria.title,
+                "status": True
+            })
+        if not approved:
+            n_failed_kriteria = randint(1, len(kriteria_list))
+            for i in range(n_failed_kriteria):
+                kriteria_list[i]["status"] = False
+            feedback = "Materi Dummy tidak diterima karena tidak memenuhi kriteria"
+        else:
+            feedback = "Materi Dummy diterima"
+        report["kriteria"] = kriteria_list
+        report["feedback"] = feedback
+        return report
+
+    def handle(self, *args, **options):
+        num_of_materi = options['num_of_materi']
+        num_of_contributor = min(floor(num_of_materi*0.2), 10)
+        num_of_admin = ceil(num_of_materi*0.2*0.1)
+        self.last_contributor_created = get_time_before(
+            timezone.now(), (365*SECONDS_IN_DAY))
+        self.cover = self._generate_cover()
+        self.content = self._generate_content()
+        self.categories = self._generate_category()
+        self.criteria = self._generate_verifivation_criteria()
+        self.num_of_reject, self.num_of_pending, self.num_of_approve = self._verify_count(
+            num_of_materi)
+        self.admins = self._generate_admin(num_of_admin)
+        self.contributors = self._generate_contributor(num_of_contributor)
+        self._generate_materi(num_of_materi)
+
+        self.stdout.write(self.style.SUCCESS(
+            'Successfully created %s materi' % options['num_of_materi']))
diff --git a/app/management/commands/generatetraffic.py b/app/management/commands/generatetraffic.py
new file mode 100644
index 0000000000000000000000000000000000000000..503e0eaff206646c6c33ef6b31fb64f1059c2c78
--- /dev/null
+++ b/app/management/commands/generatetraffic.py
@@ -0,0 +1,113 @@
+from datetime import datetime, timedelta
+from math import floor, ceil
+from random import randint, choice, choices, sample, uniform, random
+from typing import List
+
+from django.core.management.base import BaseCommand
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.utils import timezone
+
+from app.models import Materi, Comment, Like, DownloadStatistics, ViewStatistics, DummyLike, DummyViewStatistics, DummyDownloadStatistics, DummyComment
+from app.management.commands.utils import SECONDS_IN_DAY, get_time_before, get_random_datetime, generate_list_of_random_datetime, generate_random_string, getRandomColor, getLoremWithLength
+
+
+class Command(BaseCommand):
+    help = 'Generate dummy data for seeding.'
+
+    def add_arguments(self, parser):
+        parser.add_argument('baseline', type=int, default=100,
+                            help="Baseline number of visit perday")
+        parser.add_argument('--coef-materi', type=float, default=0.10,
+                            help="Coef number for visit because of materi i.e. more materi = more visit")
+        parser.add_argument('--coef-time', type=float, default=0.05,
+                            help="Coef number for visit because of time i.e. more time = more visit")
+        parser.add_argument('--coef-visit-range', type=float, default=0.25,
+                            help="Coef number for visit because of time i.e. more time = more visit")
+        parser.add_argument('--coef-read', type=float, default=0.20,
+                            help="chance of visiting user to view a materi")
+        parser.add_argument('--coef-download', type=float, default=0.5,
+                            help="chance of viewing user to download a materi")
+        parser.add_argument('--coef-like', type=float, default=0.4,
+                            help="chance of viewing user to like a materi")
+        parser.add_argument('--coef-comment', type=float, default=0.2,
+                            help="chance of viewing user to comment on a materi")
+
+    def _view_materi(self, timestamp, materi):
+        item = ViewStatistics(materi=materi, timestamp=timestamp)
+        item.save()
+        DummyViewStatistics(item=item).save()
+
+    def _download_materi(self, timestamp, materi):
+        item = DownloadStatistics(materi=materi, timestamp=timestamp)
+        item.save()
+        DummyDownloadStatistics(item=item).save()
+
+    def _like_materi(self, timestamp, materi):
+        item = Like(materi=materi, timestamp=timestamp,
+                    session_id=f"DuMmY{generate_random_string(27)}")
+        item.save()
+        DummyLike(item=item).save()
+
+    def _comment_materi(self, timestamp, materi):
+        item = Comment(materi=materi, timestamp=timestamp,
+                       profile=getRandomColor(), comment=getLoremWithLength(240))
+        item.save()
+        DummyComment(item=item).save()
+
+    def handle(self, *args, **options):
+        materi = Materi.objects.filter(title__icontains="dummy")
+        materi = [i for i in materi if i.published_date is not None]
+        today = timezone.now()
+        materi_published_date = [i.published_date for i in materi]
+        materi_published_date.sort()
+        r_day = 1
+        s_date = materi_published_date[0]
+        s_date = s_date.replace(day=s_date.day+1, hour=0,
+                                minute=0, second=0, microsecond=0)
+        reports = []
+        while (s_date < today):
+            report = {
+                "s_date": s_date,
+                "visit": 0,
+                "view": 0,
+                "download": 0,
+                "like": 0,
+                "comment": 0,
+            }
+            today_materi = [i for i in materi if i.published_date < s_date]
+            visiting_user = options["baseline"]
+            visiting_user += int(options["coef_time"] * r_day) + \
+                int(options["coef_materi"] * len(today_materi))
+            visiting_user = int(visiting_user *
+                                (1 + uniform(-options["coef_visit_range"],
+                                             options["coef_visit_range"])))
+            active_user = int(visiting_user * options["coef_read"])
+            report["visit"] = visiting_user
+            report["view"] = active_user
+            times = generate_list_of_random_datetime(
+                s_date, s_date+timedelta(days=1), active_user)
+            for timestamp in times:
+                selected_materi = choice(today_materi)
+                self._view_materi(timestamp, selected_materi)
+                if random() < options["coef_download"]:
+                    self._download_materi(timestamp, selected_materi)
+                    report["download"] += 1
+                if random() < options["coef_like"]:
+                    self._like_materi(timestamp, selected_materi)
+                    report["like"] += 1
+                if random() < options["coef_comment"]:
+                    self._comment_materi(timestamp, selected_materi)
+                    report["comment"] += 1
+            s_date = s_date + timedelta(days=1)
+            r_day += 1
+            reports.append(report)
+        # for i in range
+        for i in reports:
+            self.stdout.write(self.style.SUCCESS(f"Today is {i['s_date']}"))
+            self.stdout.write(self.style.SUCCESS(f"User visit {i['visit']}"))
+            self.stdout.write(self.style.SUCCESS(f"User view {i['view']}"))
+            self.stdout.write(self.style.SUCCESS(
+                f"User download {i['download']}"))
+            self.stdout.write(self.style.SUCCESS(f"User like {i['like']}"))
+            self.stdout.write(self.style.SUCCESS(
+                f"User comment {i['comment']}"))
diff --git a/app/management/commands/removedummy.py b/app/management/commands/removedummy.py
new file mode 100644
index 0000000000000000000000000000000000000000..bbe6e9a1892ba29df1d1250b748366ed6d12c338
--- /dev/null
+++ b/app/management/commands/removedummy.py
@@ -0,0 +1,45 @@
+from datetime import datetime
+from math import floor
+from random import randint, choice, choices
+from typing import List
+
+from django.core.management.base import BaseCommand
+from django.core.files.uploadedfile import SimpleUploadedFile
+
+from app.models import Materi, Category, Comment, Like, DownloadStatistics, ViewStatistics
+from administration.models import VerificationReport, VerificationSetting
+from authentication.models import User
+
+
+class Command(BaseCommand):
+    help = 'Remmove dummy data for seeding.'
+
+    def _remove_user(self):
+        for item in User.objects.filter(email__icontains="dummy"):
+            item.delete()
+
+    def _remove_category(self):
+        for item in Category.objects.filter(name__icontains="dummy"):
+            item.delete()
+
+    def _remove_materi(self):
+        for item in Materi.objects.filter(title__icontains="dummy"):
+            item.verificationreport_set.all().delete()
+            item.delete()
+
+    def _remove_verifivation_criteria(self):
+        for item in VerificationSetting.objects.filter(title__icontains="dummy"):
+            item.delete()
+
+    def _remove_verivication_report(self):
+        for item in VerificationReport.objects.filter(report__feedback__icontains="dummy"):
+            item.delete()
+        
+
+    def handle(self, *args, **options):
+        self._remove_user()
+        self._remove_category()
+        self._remove_materi()
+        self._remove_verifivation_criteria()
+        self.stdout.write(self.style.SUCCESS(
+            'Successfully remove all dummy object'))
diff --git a/app/management/commands/removetraffic.py b/app/management/commands/removetraffic.py
new file mode 100644
index 0000000000000000000000000000000000000000..73afceda4a06d41f29892df66ad8c20d97a86db5
--- /dev/null
+++ b/app/management/commands/removetraffic.py
@@ -0,0 +1,44 @@
+from datetime import datetime, timedelta
+from math import floor, ceil
+from random import randint, choice, choices, sample, uniform, random
+from typing import List
+
+from django.core.management.base import BaseCommand
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.utils import timezone, lorem_ipsum
+
+from app.models import DummyLike, DummyViewStatistics, DummyDownloadStatistics, DummyComment
+from app.management.commands.utils import SECONDS_IN_DAY, get_time_before, get_random_datetime, generate_list_of_random_datetime, generate_random_string, getRandomColor
+
+
+class Command(BaseCommand):
+    help = 'Remmove dummy data for seeding.'
+
+    def _remove_view(self):
+        for item in DummyViewStatistics.objects.all():
+            item.item.delete()
+            item.delete()
+
+    def _remove_download(self):
+        for item in DummyDownloadStatistics.objects.all():
+            item.item.delete()
+            item.delete()
+
+    def _remove_like(self):
+        for item in DummyLike.objects.all():
+            item.item.delete()
+            item.delete()
+
+    def _remove_comment(self):
+        for item in DummyComment.objects.all():
+            item.item.delete()
+            item.delete()
+        
+
+    def handle(self, *args, **options):
+        self._remove_view()
+        self._remove_download()
+        self._remove_like()
+        self._remove_comment()
+        self.stdout.write(self.style.SUCCESS(
+            'Successfully remove all dummy traffic'))
diff --git a/app/management/commands/utils.py b/app/management/commands/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..35884ddbeac236a264a0ab4e7fe96c32f2ea0833
--- /dev/null
+++ b/app/management/commands/utils.py
@@ -0,0 +1,59 @@
+from datetime import datetime, timedelta
+from math import floor, ceil
+from random import randint, choice, choices, sample, random
+from string import ascii_letters
+
+from django.utils import timezone, lorem_ipsum
+
+
+SECONDS_IN_DAY = 86400
+
+
+def getRandomColor():
+    color = "%06x" % randint(0, 0xFFFFFF)
+    return color
+
+
+def getLoremWithLength(n):
+    while True:
+        s = lorem_ipsum.sentence()
+        if len(s) < n:
+            return s
+
+
+def get_time_before(datetime, delta):
+    return datetime - timedelta(seconds=delta)
+
+
+def get_time_after(datetime, delta):
+    return datetime + timedelta(seconds=delta)
+
+
+def get_delta_in_seconds(s, e):
+    return int((e-s).total_seconds())
+
+
+def get_random_datetime(start_date, end_date, max_delta_seconds=None, min_delta_seconds=None):
+    delta = get_delta_in_seconds(start_date, end_date)
+    lower = 0 if min_delta_seconds is None else min_delta_seconds
+    upper = delta if max_delta_seconds is None else max_delta_seconds
+    delta = randint(lower, upper)
+    return start_date + timedelta(seconds=delta)
+
+
+def generate_list_of_random_datetime(start, end, n):
+    res = []
+    for i in range(n):
+        res.append(get_random_datetime(start, end))
+    res.sort()
+    return res
+
+
+def get_last_year():
+    datetime = timezone.now() - timedelta(days=365)
+    datetime = datetime.replace(hour=0, minute=0, second=0, microsecond=0)
+    return datetime
+
+
+def generate_random_string(n):
+    return(''.join(choice(ascii_letters) for i in range(n)))
diff --git a/app/migrations/0004_like.py b/app/migrations/0004_like.py
new file mode 100644
index 0000000000000000000000000000000000000000..7240ea7ac31c62b66b4ef2cdcd9790b2f6d2e143
--- /dev/null
+++ b/app/migrations/0004_like.py
@@ -0,0 +1,22 @@
+# Generated by Django 3.0.3 on 2020-05-12 08:34
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0003_auto_20200509_2108'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Like',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('timestamp', models.DateTimeField(auto_now_add=True)),
+                ('materi', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='app.Materi')),
+            ],
+        ),
+    ]
diff --git a/app/migrations/0005_like_session_id.py b/app/migrations/0005_like_session_id.py
new file mode 100644
index 0000000000000000000000000000000000000000..8520a4d3769e9bf68d9ed2fad52454391df55777
--- /dev/null
+++ b/app/migrations/0005_like_session_id.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.0.3 on 2020-05-12 09:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0004_like'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='like',
+            name='session_id',
+            field=models.CharField(default='', max_length=32),
+            preserve_default=False,
+        ),
+    ]
diff --git a/app/migrations/0006_downloadstatistics_viewstatistics.py b/app/migrations/0006_downloadstatistics_viewstatistics.py
new file mode 100644
index 0000000000000000000000000000000000000000..2b29fb125a53831e97d5888d6d878894dcd580d3
--- /dev/null
+++ b/app/migrations/0006_downloadstatistics_viewstatistics.py
@@ -0,0 +1,30 @@
+# Generated by Django 3.0.3 on 2020-05-13 10:34
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0005_like_session_id'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ViewStatistics',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('timestamp', models.DateTimeField(auto_now_add=True)),
+                ('materi', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='baca', to='app.Materi')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='DownloadStatistics',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('timestamp', models.DateTimeField(auto_now_add=True)),
+                ('materi', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='unduh', to='app.Materi')),
+            ],
+        ),
+    ]
diff --git a/app/migrations/0007_auto_20200516_1743.py b/app/migrations/0007_auto_20200516_1743.py
new file mode 100644
index 0000000000000000000000000000000000000000..5a0973432393117a5aa0435217f0244c49a9cfe3
--- /dev/null
+++ b/app/migrations/0007_auto_20200516_1743.py
@@ -0,0 +1,43 @@
+# Generated by Django 3.0.3 on 2020-05-16 10:43
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0006_downloadstatistics_viewstatistics'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='materi',
+            name='date_added',
+        ),
+        migrations.AddField(
+            model_name='materi',
+            name='date_created',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+        migrations.AddField(
+            model_name='materi',
+            name='date_modified',
+            field=models.DateTimeField(auto_now=True),
+        ),
+        migrations.AlterField(
+            model_name='downloadstatistics',
+            name='timestamp',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+        migrations.AlterField(
+            model_name='like',
+            name='timestamp',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+        migrations.AlterField(
+            model_name='viewstatistics',
+            name='timestamp',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+    ]
diff --git a/app/migrations/0008_auto_20200518_1919.py b/app/migrations/0008_auto_20200518_1919.py
new file mode 100644
index 0000000000000000000000000000000000000000..df20b16663a25a3d064ff3f30b01012f1ecf150c
--- /dev/null
+++ b/app/migrations/0008_auto_20200518_1919.py
@@ -0,0 +1,48 @@
+# Generated by Django 3.0.3 on 2020-05-18 12:19
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0007_auto_20200516_1743'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='comment',
+            name='timestamp',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+        migrations.CreateModel(
+            name='DummyViewStatistics',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.ViewStatistics')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='DummyLike',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.Like')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='DummyDownloadStatistics',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.DownloadStatistics')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='DummyComment',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.Comment')),
+            ],
+        ),
+    ]
diff --git a/app/migrations/0009_auto_20200518_2245.py b/app/migrations/0009_auto_20200518_2245.py
new file mode 100644
index 0000000000000000000000000000000000000000..68785da21a33971eced0e5cf01c37c9e350a56d9
--- /dev/null
+++ b/app/migrations/0009_auto_20200518_2245.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.3 on 2020-05-18 15:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0008_auto_20200518_1919'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='comment',
+            name='comment',
+            field=models.CharField(default='comments', max_length=240),
+        ),
+    ]
diff --git a/app/models.py b/app/models.py
index fa75ca32c6efa8e6924f9c729040ab9a6e060b32..95411a206ec24bcd9ce1b09f4761bf1b99dcc840 100644
--- a/app/models.py
+++ b/app/models.py
@@ -1,6 +1,7 @@
 import random
 
 from django.db import models
+from django.utils import timezone
 
 from authentication.models import User
 
@@ -40,16 +41,69 @@ class Materi(models.Model):
     status = models.CharField(
         max_length=30, choices=VERIFICATION_STATUS, default=VERIFICATION_STATUS[0][0])
     categories = models.ManyToManyField(Category)
-    date_added = models.DateTimeField(auto_now_add=True)
+    date_created = models.DateTimeField(default=timezone.now)
+    date_modified = models.DateTimeField(auto_now=True)
+
+    @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
+        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':
+                published_date = report.timestamp
+        return published_date
 
 
 class Comment(models.Model):
     username = models.CharField(max_length=100)
     profile = models.CharField(max_length=100, default=getRandomColor)
-    comment = models.CharField(max_length=150, default="comments")
+    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)
+    timestamp =  models.DateTimeField(default=timezone.now)
 
     def __str__(self):
         return self.username
+
+
+class Like(models.Model):
+    materi = models.ForeignKey(Materi, models.SET_NULL, null=True)
+    timestamp = models.DateTimeField(default=timezone.now)
+    session_id = models.CharField(max_length=32, blank=False)
+
+
+class ViewStatistics(models.Model):
+    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")
+    timestamp = models.DateTimeField(default=timezone.now)
+
+
+class DummyLike(models.Model):
+    item = models.ForeignKey(Like, on_delete=models.CASCADE)
+
+
+class DummyViewStatistics(models.Model):
+    item = models.ForeignKey(ViewStatistics, on_delete=models.CASCADE)
+
+
+class DummyDownloadStatistics(models.Model):
+    item = models.ForeignKey(DownloadStatistics, on_delete=models.CASCADE)
+
+
+class DummyComment(models.Model):
+    item = models.ForeignKey(Comment, on_delete=models.CASCADE)
diff --git a/app/templates/app/detail_materi.html b/app/templates/app/detail_materi.html
index d22d5dbcb04a8d1c029ae332b5a0a8aa114239a1..bb91484a398d10bdba812107fbc9e633c46514b2 100644
--- a/app/templates/app/detail_materi.html
+++ b/app/templates/app/detail_materi.html
@@ -119,6 +119,14 @@
                             <button class="dropdown-item btn-book" onclick="copyToClipboard('#url')">Bagikan Tautan</button>
                         </div>
                     </div>
+                    <form action="" method="POST">
+                        <input type="hidden" name="action" value="like">
+                    </form>
+                    {% if has_liked %}
+                    <button id="thumb" class="btn btn-link btn-book shadow-sm p-2 mr-2 bg-white rounded"><i id="thumbIcon" aria-hidden="true" class="fas fa-thumbs-up"></i> Disukai</button>
+                    {% else %}
+                    <button id="thumb" class="btn btn-link btn-book shadow-sm p-2 mr-2 bg-white rounded"><i id="thumbIcon" aria-hidden="true" class="far fa-thumbs-up"></i> Sukai</button>
+                    {% endif %}
                 </div>
             </div>
         </div>
@@ -186,3 +194,50 @@
         </div>
       </footer>
 {% endblock content %}
+{% block extra_scripts %}
+<script src="https://kit.fontawesome.com/bc2cedd6b2.js" crossorigin="anonymous"></script>
+<script type="text/javascript">
+    // using jQuery
+    var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
+</script>
+<script>
+    function csrfSafeMethod(method) {
+        // these HTTP methods do not require CSRF protection
+        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
+    }
+</script>
+<script>
+        $('#thumb').click(function () {
+
+            $.ajaxSetup({
+                beforeSend: function (xhr, settings) {
+                    if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
+                        xhr.setRequestHeader("X-CSRFToken", csrftoken);
+                    }
+                }
+            });
+            $.ajax({
+                type: 'POST',
+                url: "{% url 'PostLikeToggle' %}",
+                data: {
+                    'materi_id': "{{ materi_data.id }}",
+                    'session_id': "{{ session_id }}"
+                },
+                success: LikePost,
+                dataType: 'html'
+            });
+        });
+
+
+        function LikePost(data, jqXHR) {
+            var data = $.parseJSON(data)
+            if (data['liked']) {
+                $('#thumbIcon').removeClass("fas fa-thumbs-up").addClass('far fa-thumbs-up')
+                document.getElementById("thumb").firstChild.data = " Sukai"
+            } else {
+                $('#thumbIcon').removeClass("far fa-thumbs-up").addClass('fas fa-thumbs-up')
+                document.getElementById("thumb").firstChild.data = " Disukai"
+            }
+        }
+</script>
+{% endblock extra_scripts %}
diff --git a/app/tests.py b/app/tests.py
index eb3b0e1f247403f615172c4ed19c035eefb6dd8a..a4c31ff23c5107f7cc719cf721600a4374598b3e 100644
--- a/app/tests.py
+++ b/app/tests.py
@@ -11,7 +11,7 @@ from administration.utils import id_generator
 from app.views import UploadMateriHTML, UploadMateriView
 from authentication.models import User
 
-from .models import Category, Comment, Materi
+from .models import Category, Comment, Materi, Like, ViewStatistics, DownloadStatistics
 from .views import (DaftarKatalog, DashboardKontributorView, DetailMateri,
                     ProfilKontributorView, SuksesLoginAdminView,
                     SuksesLoginKontributorView, SuntingProfilView,
@@ -541,3 +541,222 @@ class SuksesLoginAdminTest(TestCase):
         self.assertEqual(response.status_code, 200)
         # Logout
         self.client.logout()
+
+
+class LikeMateriTest(TestCase):
+    def setUp(self):
+        self.contributor_credential = {
+            "email": "kontributor@gov.id",
+            "password": id_generator()
+        }
+        self.contributor = get_user_model().objects.create_user(
+            **self.contributor_credential, name="Kontributor", is_contributor=True
+        )
+        self.client = Client()
+        self.url_like = '/materi/like/'
+        content = b"Test file"
+        self.cover = SimpleUploadedFile(
+            "cover.jpg",
+            content
+        )
+        self.content = SimpleUploadedFile(
+            "content.txt",
+            content
+        )
+        Materi(title="Materi 1", author="Agas", uploader=self.contributor,
+               publisher="Kelas SC", descriptions="Deskripsi Materi 1",
+               status="PENDING", cover=self.cover, content=self.content).save()
+        self.materi1 = Materi.objects.first()
+        self.url_materi = f'/materi/{self.materi1.id}/'
+
+    def test_get_method(self):
+        response = self.client.get(self.url_like)
+        response = json.loads(response.content)
+        self.assertEqual(response.get("success", None), False)
+
+    def test_like_materi(self):
+        # Verify that materi doesn't have any like to start with
+        num_of_likes = Like.objects.filter(materi = self.materi1).count()
+        self.assertEqual(num_of_likes, 0)
+
+        # Like a materi 
+        response = self.client.get(self.url_materi)
+        session_id = response.context["session_id"]
+        materi_id = response.context["materi_data"].id
+        payload = {
+            'materi_id': materi_id,
+            'session_id': session_id
+        }
+        ajax_response = Client().post(self.url_like, payload)
+        num_of_likes = Like.objects.filter(materi = self.materi1).count()
+        self.assertEqual(num_of_likes, 1)
+
+    def test_unlike_materi(self):
+        # Verify that materi doesn't have any like to start with
+        num_of_likes = Like.objects.filter(materi = self.materi1).count()
+        self.assertEqual(num_of_likes, 0)
+
+        # Like a materi 
+        response = self.client.get(self.url_materi)
+        session_id = response.context["session_id"]
+        materi_id = response.context["materi_data"].id
+        payload = {
+            'materi_id': materi_id,
+            'session_id': session_id
+        }
+        ajax_response = Client().post(self.url_like, payload)
+        num_of_likes = Like.objects.filter(materi = self.materi1).count()
+        self.assertEqual(num_of_likes, 1)
+
+        # Unlike a materi 
+        response = self.client.get(self.url_materi)
+        session_id = response.context["session_id"]
+        materi_id = response.context["materi_data"].id
+        payload = {
+            'materi_id': materi_id,
+            'session_id': session_id
+        }
+        ajax_response = Client().post(self.url_like, payload)
+        num_of_likes = Like.objects.filter(materi = self.materi1).count()
+        self.assertEqual(num_of_likes, 0)
+
+    def test_2_client_like_materi(self):
+        # Verify that materi doesn't have any like to start with
+        num_of_likes = Like.objects.filter(materi = self.materi1).count()
+        self.assertEqual(num_of_likes, 0)
+
+        # Client 1 like a materi 
+        response = self.client.get(self.url_materi)
+        session_id = response.context["session_id"]
+        materi_id = response.context["materi_data"].id
+        payload = {
+            'materi_id': materi_id,
+            'session_id': session_id
+        }
+        ajax_response = Client().post(self.url_like, payload)
+        num_of_likes = Like.objects.filter(materi = self.materi1).count()
+        self.assertEqual(num_of_likes, 1)
+
+        # Client 2 like a materi 
+        response = Client().get(self.url_materi)
+        session_id = response.context["session_id"]
+        materi_id = response.context["materi_data"].id
+        payload = {
+            'materi_id': materi_id,
+            'session_id': session_id
+        }
+        ajax_response = Client().post(self.url_like, payload)
+        num_of_likes = Like.objects.filter(materi = self.materi1).count()
+        self.assertEqual(num_of_likes, 2)
+
+    def test_incomplete_like_parameter(self):
+        # Verify that materi doesn't have any like to start with
+        num_of_likes = Like.objects.filter(materi = self.materi1).count()
+        self.assertEqual(num_of_likes, 0)
+
+        # missing session id 
+        response = self.client.get(self.url_materi)
+        materi_id = response.context["materi_data"].id
+        payload = {
+            'materi_id': materi_id,
+        }
+        ajax_response = Client().post(self.url_like, payload)
+        ajax_response = json.loads(ajax_response.content) 
+        num_of_likes = Like.objects.filter(materi = self.materi1).count()
+        self.assertEqual(num_of_likes, 0)
+        self.assertEqual(ajax_response.get("success", None), False)
+        
+        # missing materi id 
+        response = self.client.get(self.url_materi)
+        session_id = response.context["session_id"]
+        payload = {
+            'session_id': session_id
+        }
+        ajax_response = Client().post(self.url_like, payload)
+        ajax_response = json.loads(ajax_response.content) 
+        num_of_likes = Like.objects.filter(materi = self.materi1).count()
+        self.assertEqual(num_of_likes, 0)
+        self.assertEqual(ajax_response.get("success", None), False)
+
+
+class ViewMateriStatissticsTest(TestCase):
+    def setUp(self):
+        self.contributor_credential = {
+            "email": "kontributor@gov.id",
+            "password": id_generator()
+        }
+        self.contributor = get_user_model().objects.create_user(
+            **self.contributor_credential, name="Kontributor", is_contributor=True
+        )
+        self.client = Client()
+        content = b"Test file"
+        self.cover = SimpleUploadedFile(
+            "cover.jpg",
+            content
+        )
+        self.content = SimpleUploadedFile(
+            "content.txt",
+            content
+        )
+        Materi(title="Materi 1", author="Agas", uploader=self.contributor,
+               publisher="Kelas SC", descriptions="Deskripsi Materi 1",
+               status="PENDING", cover=self.cover, content=self.content).save()
+        self.materi1 = Materi.objects.first()
+        self.url = f"/materi/{self.materi1.id}/view"
+
+    # Test single view 
+    def test_count_one_materi_view(self):
+        response = self.client.get(self.url)
+        num_of_views = self.materi1.baca.all().count()
+        self.assertEqual(num_of_views, 1)
+
+    # Test more than one view
+    def test_count_more_than_one_materi_view(self):
+        response = self.client.get(self.url)
+        num_of_views = self.materi1.baca.all().count()
+        self.assertEqual(num_of_views, 1)
+
+        response = Client().get(self.url)
+        num_of_views = self.materi1.baca.all().count()
+        self.assertEqual(num_of_views, 2)
+
+class DownloadMateriStatissticsTest(TestCase):
+    def setUp(self):
+        self.contributor_credential = {
+            "email": "kontributor@gov.id",
+            "password": id_generator()
+        }
+        self.contributor = get_user_model().objects.create_user(
+            **self.contributor_credential, name="Kontributor", is_contributor=True
+        )
+        self.client = Client()
+        content = b"Test file"
+        self.cover = SimpleUploadedFile(
+            "cover.jpg",
+            content
+        )
+        self.content = SimpleUploadedFile(
+            "content.txt",
+            content
+        )
+        Materi(title="Materi 1", author="Agas", uploader=self.contributor,
+               publisher="Kelas SC", descriptions="Deskripsi Materi 1",
+               status="PENDING", cover=self.cover, content=self.content).save()
+        self.materi1 = Materi.objects.first()
+        self.url = f"/materi/{self.materi1.id}/unduh"
+
+    # Test single download 
+    def test_count_one_materi_download(self):
+        response = self.client.get(self.url)
+        num_of_downloads = self.materi1.unduh.all().count()
+        self.assertEqual(num_of_downloads, 1)
+
+    # Test more than one download
+    def test_count_more_than_one_materi_download(self):
+        response = self.client.get(self.url)
+        num_of_downloads = self.materi1.unduh.all().count()
+        self.assertEqual(num_of_downloads, 1)
+
+        response = Client().get(self.url)
+        num_of_downloads = self.materi1.unduh.all().count()
+        self.assertEqual(num_of_downloads, 2)
\ No newline at end of file
diff --git a/app/urls.py b/app/urls.py
index d2ff9c8d315e0912d66b4679d6347ce40eebe504..3fe4a19f8318111d075a72d73ca675bece9e6820 100644
--- a/app/urls.py
+++ b/app/urls.py
@@ -9,6 +9,7 @@ from app.views import (DashboardKontributorView, ProfilKontributorView,
 urlpatterns = [
     path("", views.DaftarKatalog.as_view(), name="daftar_katalog"),
     path("materi/<int:pk>/", views.DetailMateri.as_view(), name="detail-materi"),
+    path("materi/like/", views.toggle_like, name="PostLikeToggle"),
     path("delete/<int:pk_materi>/<int:pk_comment>",
          views.delete_comment, name="delete-comment"),
     path("materi/<int:pk>/unduh", views.download_materi, name="download-materi"),
diff --git a/app/views.py b/app/views.py
index 61220a21b9ce87efbbbb745ed4f8a370c75f2f39..6114f45ad536265f6cd733dffc5b473f8e6e0c3d 100644
--- a/app/views.py
+++ b/app/views.py
@@ -6,16 +6,19 @@ from django.contrib.auth.models import AnonymousUser
 from django.contrib import messages
 from django.core import serializers
 from django.core.exceptions import PermissionDenied
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
 from django.db.models import Q
 from django.http import (Http404, HttpResponse, HttpResponseRedirect,
                          JsonResponse)
 from django.shortcuts import get_object_or_404, redirect, render
 from django.template import loader
+from django.urls import reverse
 from django.views.generic import TemplateView, ListView
+
+from app.forms import SuntingProfilForm, UploadMateriForm
+from app.models import Category, Comment, Materi, Like, ViewStatistics, DownloadStatistics
 from authentication.models import User
-from .forms import SuntingProfilForm, UploadMateriForm
-from .models import Category, Comment, Materi
-from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
 
 class DaftarKatalog(TemplateView):
     paginate_by = 2
@@ -68,11 +71,18 @@ class DetailMateri(TemplateView):
     template_name = "app/detail_materi.html"
 
     def get_context_data(self, **kwargs):
-        return super().get_context_data(**kwargs)
+        context = super(DetailMateri, self).get_context_data(**kwargs)
+        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"])
+        context["session_id"] = self.request.session.session_key
+        context["materi_data"] = materi
+        context["has_liked"] = Like.objects.filter(
+            materi=materi, session_id=self.request.session.session_key).exists()
+        return context
 
     def get(self, request, *args, **kwargs):
         context = self.get_context_data(**kwargs)
-        context["materi_data"] = get_object_or_404(Materi, pk=kwargs["pk"])
         query_set_for_comment = Comment.objects.filter(
             materi=context["materi_data"])
         context["comment_data"] = query_set_for_comment
@@ -108,6 +118,27 @@ class DetailMateri(TemplateView):
         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 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()
+        if has_liked:
+            like = get_object_or_404(
+                Like, materi=materi, session_id=session_id)
+            like.delete()
+            return JsonResponse({"success": True, "liked": True})
+        else:
+            Like(materi=materi, session_id=session_id).save()
+            return JsonResponse({"success": True, "liked": False})
+    else:
+        return JsonResponse({"success": False, "msg": "Unsuported method"})
+
+
 def delete_comment(request, pk_materi, pk_comment):
     comment = get_object_or_404(Comment, pk=pk_comment)
     url = '/materi/' + str(pk_materi) + "/"
@@ -120,29 +151,39 @@ def download_materi(request, pk):
     path = materi.content.path
     file_path = os.path.join(settings.MEDIA_ROOT, path)
     if os.path.exists(file_path):
-        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)
-            return response
+        try:
+            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)
+                DownloadStatistics(materi=materi).save()
+                return response
+        except Exception as e:
+            raise Http404("File tidak dapat ditemukan.")
     else:
         raise Http404("File tidak dapat ditemukan.")
 
+
 def view_materi(request, pk):
     materi = get_object_or_404(Materi, pk=pk)
     path = materi.content.path
     file_path = os.path.join(settings.MEDIA_ROOT, path)
     if os.path.exists(file_path):
         mimetype = mimetypes.guess_type(file_path)
-        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)
-            return response
+        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)
+                ViewStatistics(materi=materi).save()
+                return response
+        except Exception as e:
+            raise Http404("File tidak dapat ditemukan.")
     else:
         raise Http404("File tidak dapat ditemukan.")
 
+
 class UploadMateriView(TemplateView):
     template_name = "unggah.html"
     context = {}
@@ -162,8 +203,9 @@ class UploadMateriView(TemplateView):
             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)
@@ -182,6 +224,7 @@ class UploadMateriView(TemplateView):
 class UploadMateriHTML(TemplateView):
     template_name = "unggah.html"
     context = {}
+
     def get_template_names(self):
         if self.request.path == "/unggah/":
             template_name = "unggah.html"
@@ -208,6 +251,7 @@ class DashboardKontributorView(TemplateView):
         context["materi_list"] = materi_list
         return self.render_to_response(context)
 
+
 class ProfilAdminView(TemplateView):
     template_name = "profil-admin.html"
 
@@ -226,6 +270,7 @@ class ProfilAdminView(TemplateView):
         context["user"] = current_user
         return self.render_to_response(context)
 
+
 class ProfilKontributorView(TemplateView):
     template_name = "profil.html"
 
diff --git a/digipus/__pycache__/settings.cpython-36.pyc b/digipus/__pycache__/settings.cpython-36.pyc
index 4d8cab7898ac0be58fd5dcbcfa9f21028fa43c5c..8135dbec6f96b3073a31ea5325e6b2dc5bd097d0 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 71f8eafbaedf817b261b37222b7d58596cf614f9..eb93ff7d99f684905e72333b26573ac9082ed52a 100644
--- a/digipus/settings.py
+++ b/digipus/settings.py
@@ -58,6 +58,8 @@ MIDDLEWARE = [
     "whitenoise.middleware.WhiteNoiseMiddleware",
 ]
 
+SESSION_SAVE_EVERY_REQUEST = True
+
 ROOT_URLCONF = "digipus.urls"
 
 TEMPLATES = [
diff --git a/templates/base.html b/templates/base.html
index ec438a814545de77031ab78f25fc6196ce447eae..3c19553f4d883cca6a53929fff2669f03714d224 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -20,7 +20,7 @@
 
     <!-- Optional JavaScript -->
     <!-- jQuery first, then Popper.js, then Bootstrap JS -->
-    <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://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" 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>
     {% block extra_scripts %}