diff --git a/app/forms.py b/app/forms.py
index aafd9baf3412973d7942c2e17c1a1aecd54a87d2..5b8b3438c71bbe7efcb9dbee7746662563efb42b 100644
--- a/app/forms.py
+++ b/app/forms.py
@@ -28,6 +28,8 @@ class UploadMateriForm(forms.ModelForm):
             if (field_name == "categories"):
                 continue
             field.widget.attrs["class"] = "form-control"
+            field.widget.attrs["placeholder"] = field.initial
+            field.initial = ""
         self.fields['categories'].widget.attrs.update({'class' : "native"})
         
 
@@ -57,7 +59,7 @@ class SuntingProfilForm(forms.ModelForm):
 class RatingContributorForm(forms.ModelForm):
     class Meta:
         model = RatingContributor
-        fields = ['score', 'user']
+        fields = ['score', 'user', 'contributor']
         SCORE_CHOICE = (
             ('', 'Select score'),
             ('1', '1'),  # First one is the value of select option and second is the displayed value in option
@@ -67,6 +69,7 @@ class RatingContributorForm(forms.ModelForm):
             ('5', '5'),
         )
         widgets = {
-            'score': forms.Select(choices=SCORE_CHOICE, attrs={'class': 'form-control'},),
-            'user': forms.HiddenInput()
+            'score': forms.Select(choices=SCORE_CHOICE, attrs={'class': 'form-control', 'id':'form-rating'},),
+            'user': forms.HiddenInput(),
+            'contributor': forms.HiddenInput()
         }
diff --git a/app/migrations/0027_auto_20201030_1648.py b/app/migrations/0027_auto_20201030_1648.py
new file mode 100644
index 0000000000000000000000000000000000000000..2c3a8b0a4d27b16db4d83a5dbd283d5556565f13
--- /dev/null
+++ b/app/migrations/0027_auto_20201030_1648.py
@@ -0,0 +1,31 @@
+# Generated by Django 3.1 on 2020-10-30 09:48
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('app', '0026_submitvisitor'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='ratingcontributor',
+            name='contributor',
+            field=models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, related_name='contributor', to='authentication.user'),
+            preserve_default=False,
+        ),
+        migrations.AlterField(
+            model_name='ratingcontributor',
+            name='user',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AlterUniqueTogether(
+            name='ratingcontributor',
+            unique_together={('contributor', 'user')},
+        ),
+    ]
diff --git a/app/models.py b/app/models.py
index d68d767677ad268dc006753aab227c960966f8c2..661cde6be8d78d8b5d785ab31b34c8764c7ecd18 100644
--- a/app/models.py
+++ b/app/models.py
@@ -253,13 +253,17 @@ class Rating(models.Model):
 class RatingContributor(models.Model):
     timestamp = models.DateTimeField(auto_now=True)
     score = models.PositiveIntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
-    user = models.ForeignKey(User, on_delete=models.CASCADE)
+    contributor = models.ForeignKey(User, on_delete=models.CASCADE, related_name='contributor')
+    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user')
 
     def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
         if 1 <= self.score <= 5:
             super().save(force_insert, force_update, using, update_fields)
         else:
             raise ValidationError("Rating score must be integer between 1-5")
+    
+    class Meta:
+        unique_together = ["contributor", "user"]
 
 class SubscribeModel(models.Model):
     sys_id = models.AutoField(primary_key=True, null=False, blank=True)
diff --git a/app/templates/app/base_dashboard.html b/app/templates/app/base_dashboard.html
index b8281931e7b8fae10fe76dd0477fee586206fcb2..c74bd589eb745d75b89cb9f1560428d76b24e4fb 100644
--- a/app/templates/app/base_dashboard.html
+++ b/app/templates/app/base_dashboard.html
@@ -24,6 +24,7 @@
     <!-- Custom styles for this page -->
     <link href="{% static 'vendor/datatables/dataTables.bootstrap4.min.css' %}" rel="stylesheet">
     {% block stylesheets %}{% endblock %}
+    {% block scripts %}{% endblock %}
 </head>
 
 <body id="page-top" style="font-family: 'Poppins', sans-serif;">
diff --git a/app/templates/app/base_profile.html b/app/templates/app/base_profile.html
index 6dc27646954f8d5b97e55725285ec708edfe6233..081dc2ef7b2c7c987d31d20c31301d3e81849d8b 100644
--- a/app/templates/app/base_profile.html
+++ b/app/templates/app/base_profile.html
@@ -24,6 +24,8 @@
     <!-- Custom styles for this page -->
     <link href="{% static 'vendor/datatables/dataTables.bootstrap4.min.css' %}" rel="stylesheet">
     {% block stylesheets %}{% endblock %}
+
+    <script type="text/javascript" src="{% static 'vendor/jquery/jquery-3.2.1.min.js' %}"></script>
 </head>
 
 <body id="page-top" style="font-family: 'Poppins', sans-serif;">
diff --git a/app/templates/app/detail_materi.html b/app/templates/app/detail_materi.html
index 278c5decd9fd975e8bdac221ee5b7f70777737fa..72d758c9e304adc8fbdae477fbbc9e3709fcd450 100644
--- a/app/templates/app/detail_materi.html
+++ b/app/templates/app/detail_materi.html
@@ -33,7 +33,6 @@ div.review {
 
         <!-- Topbar Navbar -->
         <ul class="navbar-nav ml-auto">
-
             {% if request.user.is_contributor %}
             {% if materi_data.status == "DISAPPROVE" %}
             <li class="nav-item black-text">
@@ -164,6 +163,14 @@ div.review {
                         <p class="info-content">{{materi_data.content.size|filesizeformat}}</p>
                     </dd>
                 </div>
+                <div class="info" id="1">
+                    <dl class="col col-4">
+                        <dt class="info-name">Jumlah Like</dt>
+                    </dl>
+                    <dd>
+                        <p class="info-content">{{materi_data.like_count}}</p>
+                    </dd>
+                </div>
                 <div class="info" id="1">
                     <dl class="col col-4">
                         <dt class="info-name">Jumlah Download</dt>
@@ -225,11 +232,11 @@ div.review {
                     <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>
+                <button id="thumb" class="btn btn-link btn-book shadow-sm p-2 mr-2 bg-white rounded" onClick="postLike()"><i id="thumbIcon"
+                        aria-hidden="true" class="fas fa-thumbs-up"></i><span> Disukai</span></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>
+                <button id="thumb" class="btn btn-link btn-book shadow-sm p-2 mr-2 bg-white rounded" onClick="postLike()"><i id="thumbIcon"
+                        aria-hidden="true" class="far fa-thumbs-up"></i><span> Sukai</span></button>
                 {% endif %}
 
                 {% if user.is_authenticated %}
@@ -331,7 +338,7 @@ div.review {
                         <div>
                             <button id="thumb-like-comment-{{ comment.id }}" class="btn btn-link btn-book shadow-sm p-2 mr-2 bg-white rounded" onClick="postLikeComment({{ comment.id }})">
                                 <div class="d-flex flex-row">
-                                    {% if has_liked.comment.id %}
+                                    {% if has_liked_comment.comment.id %}
                                     <i id="thumb-like-comment-icon-{{ comment.id }}" aria-hidden="true" class="fas fa-thumbs-up"></i>
                                     {% else %}
                                     <i id="thumb-like-comment-icon-{{ comment.id }}" aria-hidden="true" class="far fa-thumbs-up"></i>
@@ -341,7 +348,7 @@ div.review {
                             </button>
                             <button id="thumb-dislike-comment-{{ comment.id }}" class="btn btn-link btn-book shadow-sm p-2 mr-2 bg-white rounded" onClick="postDislikeComment({{ comment.id }})">
                                 <div class="d-flex flex-row">
-                                    {% if has_disliked.comment.id %}
+                                    {% if has_disliked_comment.comment.id %}
                                     <i id="thumb-dislike-comment-icon-{{ comment.id }}" aria-hidden="true" class="fas fa-thumbs-down"></i>
                                     {% else %}
                                     <i id="thumb-dislike-comment-icon-{{ comment.id }}" aria-hidden="true" class="far fa-thumbs-down"></i>
@@ -509,8 +516,7 @@ div.review {
     }
 </script>
 <script>
-    $('#thumb').click(function () {
-
+    function postLike() {
         $.ajaxSetup({
             beforeSend: function (xhr, settings) {
                 if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
@@ -528,7 +534,7 @@ div.review {
             success: LikePost,
             dataType: 'html'
         });
-    });
+    }
 
     function postLikeComment(comment_id) {
         $.ajaxSetup({
@@ -599,12 +605,15 @@ div.review {
 
     function LikePost(data, jqXHR) {
         var data = $.parseJSON(data)
+        var likeCount = parseInt($('.info-content')[6].textContent)
         if (data['liked']) {
             $('#thumbIcon').removeClass("fas fa-thumbs-up").addClass('far fa-thumbs-up')
-            document.getElementById("thumb").firstChild.data = " Sukai"
+            $('#thumb>span').text(" Sukai")
+            $('.info-content')[6].textContent = likeCount - 1
         } else {
             $('#thumbIcon').removeClass("far fa-thumbs-up").addClass('fas fa-thumbs-up')
-            document.getElementById("thumb").firstChild.data = " Disukai"
+            $('#thumb>span').text(" Disukai")
+            $('.info-content')[6].textContent = likeCount + 1
         }
     }
 
diff --git a/app/templates/app/katalog_kontri.html b/app/templates/app/katalog_kontri.html
index 3dc7915a72348cc02da5cf53aff3dc4143963b70..38d7e95fe9db70860a996bdacc5c1cec0ece7e44 100644
--- a/app/templates/app/katalog_kontri.html
+++ b/app/templates/app/katalog_kontri.html
@@ -1,41 +1,33 @@
 {% extends "base.html" %}
 {% load static %}
 {% load humanize %}
-{% block title %}Digipus - {% endblock %}
+{% block title %}Digipus - {{ contributor.name }}'s' Profile{% endblock %}
 {% block header %}
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
 
-<!DOCTYPE html>
-<html lang="en">
+<link href="../../static/app/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
+<link href="../../static/app/css/heroic-features.css" rel="stylesheet">
 
-<head>
-    <title>Digipus Home</title>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1">
+<link rel="stylesheet" type="text/css" href="{% static 'app/css/katalog_materi.css' %}">
+<link rel="stylesheet" type="text/css" href="{% static 'app/css/katalog_kontri.css' %}">
 
-    <link href="../../static/app/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
-    <link href="../../static/app/css/heroic-features.css" rel="stylesheet">
+<link rel="stylesheet" type="text/css" href="{% static 'css/main.css' %}">
+<link rel="stylesheet" type="text/css" href="{% static 'css/util.css' %}">
+<link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}">
 
-    <link rel="stylesheet" type="text/css" href="{% static 'app/css/katalog_materi.css' %}">
-    <link rel="stylesheet" type="text/css" href="{% static 'app/css/katalog_kontri.css' %}">
+<link rel="stylesheet" type="text/css" href="{% static 'fonts/font-awesome-4.7.0/css/font-awesome.min.css' %}">
+<link rel="stylesheet" type="text/css" href="{% static 'vendor/animsition/css/animsition.min.css' %}">
+<link rel="stylesheet" type="text/css" href="{% static 'vendor/daterangepicker/daterangepicker.css' %}">
+<link rel="stylesheet" type="text/css" href="{% static 'vendor/css-hamburgers/hamburgers.min.css' %}">
+<link rel="stylesheet" type="text/css" href="{% static 'vendor/select2/select2.min.css' %}">
+<link rel="stylesheet" type="text/css" href="{% static 'vendor/animate/animate.css' %}">
 
-    <link rel="stylesheet" type="text/css" href="{% static 'css/main.css' %}">
-    <link rel="stylesheet" type="text/css" href="{% static 'css/util.css' %}">
-    <link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}">
+<link rel="icon" type="image/png" href="{% static 'images/icons/logo.ico' %}" />
 
-    <link rel="stylesheet" type="text/css" href="{% static 'fonts/font-awesome-4.7.0/css/font-awesome.min.css' %}">
-    <link rel="stylesheet" type="text/css" href="{% static 'vendor/animsition/css/animsition.min.css' %}">
-    <link rel="stylesheet" type="text/css" href="{% static 'vendor/daterangepicker/daterangepicker.css' %}">
-    <link rel="stylesheet" type="text/css" href="{% static 'vendor/css-hamburgers/hamburgers.min.css' %}">
-    <link rel="stylesheet" type="text/css" href="{% static 'vendor/select2/select2.min.css' %}">
-    <link rel="stylesheet" type="text/css" href="{% static 'vendor/animate/animate.css' %}">
+{% endblock header %}
 
-    <link rel="icon" type="image/png" href="{% static 'images/icons/logo.ico' %}" />
-
-
-    {% endblock header %}
-    {% block content %}
-
-</head>
+{% block content %}
 
 <body style="background-color: #f8f8f8;">
     <div class="container main">
@@ -87,10 +79,28 @@
                         </table>
                     </div>
                 </div>
-            <h4 id="rating">Rating: {% if  avg_rating.avg_rating %}{{ avg_rating.avg_rating|floatformat:"2"|intcomma }}{% else %}0{% endif %}</h4>
-            <h6>oleh: {{ count_rating }} orang</h6>
-            <div class="row"><form method="post">{{ form_rating }} {% csrf_token %}
-                <button type="submit" class="form-control" style="margin-top: 1rem">Submit</button></form> </div>
+                <div class="row mx-5 mt-3">
+                    <h4 id="rating">Rating: {% if  avg_rating.avg_rating %}{{ avg_rating.avg_rating|floatformat:"2"|intcomma }}{% else %}0{% endif %}
+                    </h4>
+                </div>
+                <div class="row mx-5 mt-2">
+                    <h6>oleh: {{ count_rating }} orang</h6>
+                </div>
+                <div class="row mx-5 mt-3">
+                    {% if not user.is_authenticated %}
+                    <h4><a href="{% url 'login' %}">Kamu harus login untuk memberi rating</a></h4>
+                    {% else %}
+                    <form method="post" class="form-inline">{{ form_rating }} {% csrf_token %}
+                        {% if has_rated %}
+                        <button class="form-control mx-3" id="updateButton">Update</button>
+                        <button class="btn btn-danger" id="deleteButton">Delete Rating</button>
+                        {% else %}
+                        <button type="submit" class="form-control mx-3">Submit</button>
+                        {% endif %}
+                    </form>
+                    {% endif %}
+
+                </div>
             </div>
         </header>
 
@@ -134,7 +144,73 @@
         </div>
     </div>
 </body>
-
-</html>
-
+{% endblock %}
+{% block extra_scripts %}
+<script type="text/javascript">
+    // using jQuery
+    function getCookie(name) {
+        var cookieValue = null;
+        if (document.cookie && document.cookie != '') {
+            var cookies = document.cookie.split(';');
+            for (var i = 0; i < cookies.length; i++) {
+                var cookie = jQuery.trim(cookies[i]);
+                if (cookie.substring(0, name.length + 1) == (name + '=')) {
+                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+                    break;
+                }
+            }
+        }
+        return cookieValue;
+    }
+    var csrftoken = getCookie('csrftoken');
+
+    function csrfSafeMethod(method) {
+        // these HTTP methods do not require CSRF protection
+        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
+    }
+
+    function reload () {
+        window.location = ''
+    };
+
+    $('#deleteButton').click(function () {
+        console.log("anjing")
+        $.ajaxSetup({
+            beforeSend: function (xhr, settings) {
+                if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
+                    xhr.setRequestHeader("X-CSRFToken", csrftoken);
+                }
+            }
+        });
+        $.ajax({
+            type: 'POST',
+            url: "{% url 'katalog-per-kontributor' contributor.email%}",
+            data: {
+                'delete': true
+            },
+            success: reload,
+            dataType: 'html'
+        });
+    });
+
+    $('#updateButton').click(function () {
+        $.ajaxSetup({
+            beforeSend: function (xhr, settings) {
+                if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
+                    xhr.setRequestHeader("X-CSRFToken", csrftoken);
+                }
+            }
+        });
+        $.ajax({
+            type: 'POST',
+            url: "{% url 'katalog-per-kontributor' contributor.email%}",
+            data: {
+                'update': true,
+                'score': $('#form-rating').val()
+            },
+            success: reload,
+            dataType: 'html'
+        });
+    });
+</script>
 {% endblock %}
\ No newline at end of file
diff --git a/app/templates/app/katalog_materi.html b/app/templates/app/katalog_materi.html
index 4745480bd96eef5ba9a4faea35a02495c272bcc2..93c719b560ad93d582913d20205b0d125f799565 100644
--- a/app/templates/app/katalog_materi.html
+++ b/app/templates/app/katalog_materi.html
@@ -57,10 +57,9 @@
             <div class="container">
                 <div class="row header">
                     <div class="col">
-						<a href= "/subscribeform"><img src="{% static 'images/sub-btn.png' %}" style="height:100px"></a>
                         <h2 class="pageTitle">Temukan Materi Yang Kamu Mau!</h2>
                         <p class="description">Cari dengan judul buku, penerbit, atau penulis</p>
-                        <form class="searchBar" action='/'>
+                        <form class="searchBar" action=''>
                             <div class="col-6 form-group">
                                 <input type="text" name='search' class="form-control" placeholder="Tulis di sini"
                                        value='{{ request.GET.search }}'>
@@ -125,6 +124,32 @@
                             <div id="collapseTwo" class="collapse" aria-labelledby="headingTwo"
                                 data-parent="#accordionExample">
                                 <div class="card-body">
+                                    <ul>
+                                        <li>
+                                            <a href="?sort=terbaru">terbaru</a>
+                                        </li>
+                                        <li>
+                                            <a href="?sort=terlama">terlama</a>
+                                        </li>
+                                        <li>
+                                            <a href="?sort=terpopuler">terpopuler</a>
+                                        </li>
+                                        <li>
+                                            <a href="?sort=judul">judul</a>
+                                        </li>
+                                        <li>
+                                            <a href="?sort=penulis">penulis</a>
+                                        </li>
+                                        <li>
+                                            <a href="?sort=pengunggah">pengunggah</a>
+                                        </li>
+                                        <li>
+                                            <a href="?sort=jumlah_unduh">jumlah unduh</a>
+                                        </li>
+                                        <li>
+                                            <a href="?sort=jumlah_tampilan">jumlah tampilan</a>
+                                        </li>
+                                    </ul>
                                 </div>
                             </div>
                         </div>
diff --git a/app/templates/req_materi.html b/app/templates/req_materi.html
index f4d071bab3f96663ae5575ecc911a502a5ad77ff..9f2eed0e638eebd49c18321ff37a263adfda4337 100644
--- a/app/templates/req_materi.html
+++ b/app/templates/req_materi.html
@@ -72,8 +72,26 @@
                 </div>
             </div>    
         </header>
-        
-        
+        {% if requested_material %}
+                <table class="table" aria-describedby="Requested Material">
+                    <thead>
+                        <tr>
+                            <th scope="col">No</th>
+                            <th scope="col">Title</th>
+                            <th scope="col">Date</th>
+                        </tr>
+                    </thead>
+                <tbody>
+                    {% for material in requested_material %}
+                        <tr>
+                            <td>{{ forloop.counter }}</td>
+                            <td>{{ material.title }}</td>
+                            <td>{{ material.timestamp }}</td>
+                        </tr>
+                    {% endfor %}
+                </tbody>
+                </table>
+        {% endif %}
     </div>
     
 <!-- /.container -->
diff --git a/app/templates/sunting.html b/app/templates/sunting.html
index 62171b1654cd60c9c9a1b262ccba0289578b65c4..02dfee9e0c555e4348098396e4b15ba87fdfb6a5 100644
--- a/app/templates/sunting.html
+++ b/app/templates/sunting.html
@@ -3,18 +3,18 @@
 
 {% block title %}
 <title>Sunting Profil | Digipus</title>
-{% endblock %} 
+{% endblock %}
 
 {% block content %}
 <div class="container">
     <div class="col-20">
         <h1 class="mt-2">
             {% if user.is_admin %}
-                Sunting Profil Admin
+            Sunting Profil Admin
             {% elif user.is_contributor %}
-                Sunting Profil Kontributor
+            Sunting Profil Kontributor
             {% else %}
-                Sunting Profil User
+            Sunting Profil User
             {% endif %}
         </h1>
         <hr class="mt-0 mb-4">
@@ -24,6 +24,26 @@
             <div class="col-md-6">
                 <div class="fieldWrapper">
                     {{ field.label_tag }} {{ field }}
+
+                    {% if field.name == 'email' %}
+                    <div class="my-3">
+
+                        {% if user.is_email_verified %}
+                        <span class="text-success">
+                            Email Status: Verified
+                        </span>
+                        {% else %}
+                        <span class="text-danger">
+                            Email Status: Unverified
+                            <button class="btn btn-success" id='verify-button'
+                                style="background-color: #615CFD; border-color: #615CFD;">
+                                Verify
+                            </button>
+                        </span>
+                        {% endif %}
+                    </div>
+                    {% endif %}
+
                     {% if field.errors %}
                     <span class="text-danger">
                         {{ field.errors }}
@@ -33,15 +53,41 @@
                     <p class="help">{{ field.help_text|safe }}</p>
                     {% endif %}
                 </div>
+
             </div>
             {% endfor %}
             <div class="form-margin"></div>
             <div class="col-md-6">
                 <div class="fieldWrapper">
-                    <button type="submit" class="btn btn-success btn-edit" style="background-color: #615CFD; border-color: #615CFD;">Simpan</button>
+                    <button type="submit" class="btn btn-success btn-edit"
+                        style="background-color: #615CFD; border-color: #615CFD;">Simpan</button>
                 </div>
             </div>
         </form>
     </div>
 </div>
+
+<script type="text/javascript">
+    $(document).ready(function () {
+        const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
+        $('#verify-button').click(function () {
+            $.ajax({
+                url: '/registrasi/send-verify-email',
+                headers: {
+                    'X-CSRFToken': csrftoken
+                },
+                method: 'POST',
+                data: {
+                    email: $('#id_email').val()
+                },
+                success: function (data, status) {
+                    alert(data)
+                },
+            })
+                
+            return false;
+        });
+    });
+</script>
+
 {% endblock %}
\ No newline at end of file
diff --git a/app/templates/unggah.html b/app/templates/unggah.html
index 5f9a0c1da2c7b2303e9c869fe6704d35bb22e4d7..b6668bb76ef6c9b73303b640bff5fb20d1168478 100644
--- a/app/templates/unggah.html
+++ b/app/templates/unggah.html
@@ -5,6 +5,10 @@
 <title>Unggah Materi | Digipus</title>
 {% endblock %}
 
+{% block scripts %}
+<script src="{% static 'js/upload-preview.js' %}"></script>
+{% endblock %}
+
 {% block content %}
 <div class="container">
     <div class="col-20">
@@ -36,6 +40,17 @@
                     {% if field.help_text %}
                     <p class="help">{{ field.help_text|safe }}</p>
                     {% endif %}
+                    {% if field.name == 'cover' %}
+                    <div hidden id="frame_cover_parent" class="my-3">
+                      <div class="mb-2">Preview Cover:</div>
+                      <img id="frame_cover" class="constraint-frame" alt="Cover Preview" />
+                    </div>
+                    {% elif field.name == 'content' %}
+                    <div hidden id="frame_content_parent" class="my-3">
+                      <div class="mb-2">Preview Content:</div>
+                      <embed id="frame_content" width="550px" height="550px" />
+                    </div>
+                    {% endif %}
                 </div>
             </div>
             {% endfor %}
diff --git a/app/tests.py b/app/tests.py
index 2123295afe447e5fa077dc7871e1b80030d685e9..2a0a6e069a9d9297915b0907ee1b1b932c5b6e6a 100644
--- a/app/tests.py
+++ b/app/tests.py
@@ -337,7 +337,9 @@ class DetailMateriTest(TestCase):
         self.download_url1 = self.url + "unduh"
         self.url2 = "/materi/" + str(self.materi2.id) + "/"
         self.download_url2 = self.url2 + "unduh"
+        self.like_url = '/materi/like/'
         self.dcount_info_name = "Jumlah Download"
+        self.lcount_info_name = "Jumlah Like"
 
         self.materi_with_published_date = Materi.objects.create(title="Materi 1", author="Agas", uploader=self.contributor,
                                                  publisher="Kelas SC", descriptions="Deskripsi Materi 1",
@@ -429,7 +431,7 @@ class DetailMateriTest(TestCase):
         session_id = response.context["session_id"]
 
         payload = {"comment": comment, "session_id": session_id}
-        ajax_response = self.client.post("/comment/dislike/", payload)
+        self.client.post("/comment/dislike/", payload)
         num_of_comment_dislikes = DislikeComment.objects.filter(comment=comment).count()
         self.assertEqual(num_of_comment_dislikes, 0)
 
@@ -444,7 +446,7 @@ class DetailMateriTest(TestCase):
         session_id = response.context["session_id"]
 
         payload = {"comment": comment, "session_id": session_id}
-        ajax_response = self.client.post("/comment/like/", payload)
+        self.client.post("/comment/like/", payload)
         num_of_comment_likes = LikeComment.objects.filter(comment=comment).count()
         self.assertEqual(num_of_comment_likes, 0)
 
@@ -459,9 +461,9 @@ class DetailMateriTest(TestCase):
         session_id = response.context["session_id"]
 
         payload = {"comment": comment, "session_id": session_id}
-        ajax_response = self.client.post("/comment/dislike/", payload)
+        self.client.post("/comment/dislike/", payload)
 
-        ajax_response = self.client.post("/comment/dislike/", payload)
+        self.client.post("/comment/dislike/", payload)
         num_of_comment_dislikes = DislikeComment.objects.filter(comment=comment, session_id=session_id).count()
         self.assertEqual(num_of_comment_dislikes, 0)
 
@@ -476,9 +478,9 @@ class DetailMateriTest(TestCase):
         session_id = response.context["session_id"]
 
         payload = {"comment": comment, "session_id": session_id}
-        ajax_response = self.client.post("/comment/like/", payload)
+        self.client.post("/comment/like/", payload)
 
-        ajax_response = self.client.post("/comment/like/", payload)
+        self.client.post("/comment/like/", payload)
         num_of_comment_likes = LikeComment.objects.filter(comment=comment, session_id=session_id).count()
         self.assertEqual(num_of_comment_likes, 0)
 
@@ -620,7 +622,7 @@ class DetailMateriTest(TestCase):
         url = self.url
         self.client.login(**self.admin_credential)
         self.client.post(url, {"comment": "This is new comment by Anonymous"})
-        deleteURL = "/delete/" + str(self.materi1.id) + "/" + str(
+        delete_url = "/delete/" + str(self.materi1.id) + "/" + str(
             Comment.objects.get(comment="This is new comment by Anonymous").id)
         if is_admin:
             self.client.login(**self.admin_credential)
@@ -628,7 +630,7 @@ class DetailMateriTest(TestCase):
             self.client.login(**self.contributor_credential)
         if not is_admin and not is_contributor:
             self.client.login(**self.anonymous_credential)
-        response = self.client.get(deleteURL)
+        response = self.client.get(delete_url)
         return response
 
     def test_delete_comments_by_admin(self):
@@ -903,6 +905,56 @@ class DetailMateriTest(TestCase):
         self.check_materi_info_in_html(self.dcount_info_name, dcount_materi2, html2)
         self.check_materi_info_not_in_html(self.dcount_info_name, dcount_materi1, html2)
 
+    def test_like_count_displayed_on_template_when_no_like(self):
+        response = self.client.get(self.url)
+        html = response.content.decode("utf-8")
+        self.check_materi_info_in_html(self.lcount_info_name, 0, html)
+
+    def test_like_count_displayed_on_template_when_single_like(self):
+        payload = {
+            'materi_id': self.materi1.id,
+            'session_id': "dummysession"
+        }
+        self.client.post(self.like_url, payload)
+
+        response = self.client.get(self.url)
+        html = response.content.decode("utf-8")
+        self.check_materi_info_in_html(self.lcount_info_name, 1, html)
+
+    def test_like_count_displayed_on_template_when_multiple_like(self):
+        payload1 = {
+            'materi_id': self.materi1.id,
+            'session_id': "dummysession1"
+        }
+        payload2 = {
+            'materi_id': self.materi1.id,
+            'session_id': "dummysession2"
+        }
+        self.client.post(self.like_url, payload1)
+        self.client.post(self.like_url, payload2)
+
+        response = self.client.get(self.url)
+        html = response.content.decode("utf-8")
+        self.check_materi_info_in_html(self.lcount_info_name, 2, html)
+
+    def test_like_count_displayed_on_template_when_unlike(self):
+        payload = {
+            'materi_id': self.materi1.id,
+            'session_id': "dummysession"
+        }
+
+        # Like materi once
+        self.client.post(self.like_url, payload)
+        response = self.client.get(self.url)
+        html = response.content.decode("utf-8")
+        self.check_materi_info_in_html(self.lcount_info_name, 1, html)
+
+        # Like it again, resulting in unlike
+        self.client.post(self.like_url, payload)
+        response = self.client.get(self.url)
+        html = response.content.decode("utf-8")
+        self.check_materi_info_in_html(self.lcount_info_name, 0, html)
+
 class PostsViewTest(TestCase):
 
     @classmethod
@@ -1303,7 +1355,7 @@ class UploadExcelPageTest(TestCase):
         file_name, data_frame = self.create_dummy_excel(field_lengths=field_lengths, categories=categories)
 
         with open(file_name, 'rb') as fp:
-            response = self.client.post("/unggah_excel/", {'excel': fp})
+            self.client.post("/unggah_excel/", {'excel': fp})
 
         title = data_frame['Title'][0]
         materi = Materi.objects.get(title=title)
@@ -1569,18 +1621,7 @@ class SuksesLoginKontributorTest(TestCase):
         # Logout
         self.client.logout()
 
-    def test_sukses_login_kontributor_url(self):
-        # Login
-        self.client.login(email="kontributor@gov.id",
-                          password="kontributor")
-        # Test
-        response = self.client.get(self.url)
-        self.assertEqual(response.status_code, 200)
-        # Logout
-        self.client.logout()
-
-    def test_sukses_login_kontributor_access(self):
-        # Kontributor
+    def test_sukses_login_kontributor_url_access(self):
         # Login
         self.client.login(email="kontributor@gov.id",
                           password="kontributor")
@@ -1616,18 +1657,7 @@ class SuksesLoginAdminTest(TestCase):
         # Logout
         self.client.logout()
 
-    def test_sukses_login_admin_url(self):
-        # Login
-        self.client.login(email="admin@gov.id",
-                          password="admin")
-        # Test
-        response = self.client.get(self.url)
-        self.assertEqual(response.status_code, 200)
-        # Logout
-        self.client.logout()
-
-    def test_sukses_login_admin_access(self):
-        # Kontributor
+    def test_sukses_login_admin_url_access(self):
         # Login
         self.client.login(email="admin@gov.id",
                           password="admin")
@@ -1682,7 +1712,7 @@ class LikeMateriTest(TestCase):
             'materi_id': materi_id,
             'session_id': session_id
         }
-        ajax_response = Client().post(self.url_like, payload)
+        Client().post(self.url_like, payload)
         num_of_likes = Like.objects.filter(materi=self.materi1).count()
         self.assertEqual(num_of_likes, 1)
 
@@ -1699,7 +1729,7 @@ class LikeMateriTest(TestCase):
             'materi_id': materi_id,
             'session_id': session_id
         }
-        ajax_response = Client().post(self.url_like, payload)
+        Client().post(self.url_like, payload)
         num_of_likes = Like.objects.filter(materi=self.materi1).count()
         self.assertEqual(num_of_likes, 1)
 
@@ -1711,7 +1741,7 @@ class LikeMateriTest(TestCase):
             'materi_id': materi_id,
             'session_id': session_id
         }
-        ajax_response = Client().post(self.url_like, payload)
+        Client().post(self.url_like, payload)
         num_of_likes = Like.objects.filter(materi=self.materi1).count()
         self.assertEqual(num_of_likes, 0)
 
@@ -1728,7 +1758,7 @@ class LikeMateriTest(TestCase):
             'materi_id': materi_id,
             'session_id': session_id
         }
-        ajax_response = Client().post(self.url_like, payload)
+        Client().post(self.url_like, payload)
         num_of_likes = Like.objects.filter(materi=self.materi1).count()
         self.assertEqual(num_of_likes, 1)
 
@@ -1740,7 +1770,7 @@ class LikeMateriTest(TestCase):
             'materi_id': materi_id,
             'session_id': session_id
         }
-        ajax_response = Client().post(self.url_like, payload)
+        Client().post(self.url_like, payload)
         num_of_likes = Like.objects.filter(materi=self.materi1).count()
         self.assertEqual(num_of_likes, 2)
 
@@ -1801,17 +1831,17 @@ class ViewMateriStatissticsTest(TestCase):
 
     # Test single view
     def test_count_one_materi_view(self):
-        response = self.client.get(self.url)
+        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)
+        self.client.get(self.url)
         num_of_views = self.materi1.baca.all().count()
         self.assertEqual(num_of_views, 1)
 
-        response = Client().get(self.url)
+        Client().get(self.url)
         num_of_views = self.materi1.baca.all().count()
         self.assertEqual(num_of_views, 2)
 
@@ -1843,17 +1873,17 @@ class DownloadMateriStatissticsTest(TestCase):
 
     # Test single download
     def test_count_one_materi_download(self):
-        response = self.client.get(self.url)
+        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)
+        self.client.get(self.url)
         num_of_downloads = self.materi1.unduh.all().count()
         self.assertEqual(num_of_downloads, 1)
 
-        response = Client().get(self.url)
+        Client().get(self.url)
         num_of_downloads = self.materi1.unduh.all().count()
         self.assertEqual(num_of_downloads, 2)
 
@@ -1899,7 +1929,7 @@ class RevisiMateriTest(TestCase):
         # Logout
         self.client.logout()
 
-    def test_revisi_materi_url(self):
+    def test_revisi_materi_url_access(self):
         # Login
         self.client.login(**self.contributor_credential)
         # Test
@@ -1908,15 +1938,6 @@ class RevisiMateriTest(TestCase):
         # Logout
         self.client.logout()
 
-    def test_revisi_materi_access(self):
-        # Kontributor
-        # Login
-        self.client.login(**self.contributor_credential)
-        # Test
-        response = self.client.get(self.url)
-        self.assertEqual(response.status_code, 200)
-        # Logout
-        self.client.logout()
 
 class GenerateDummyCommandTest(TestCase):
 
@@ -2155,7 +2176,7 @@ class RatingMateriTest(TestCase):
         response = self.client.get(self.url_materi)
         self.assertEqual(1, response.context.get('materi_rating_score'))
 
-class fileManagementUtilTest(TestCase):
+class FileManagementUtilTest(TestCase):
     def setUp(self):
         self.filename = "image_with_exif_data.gif"
         self.file_content = open(settings.BASE_DIR + "/app/test_files/" +
@@ -2224,8 +2245,8 @@ class RequestMateriTest(TestCase):
         self.assertEqual(response['location'], '/login/')
 
     def test_saving_and_retrieving_material_requests(self):
-        first_material_request = ReqMaterial(title="Material 1").save()
-        second_material_request = ReqMaterial(title="Material 2").save()
+        ReqMaterial(title="Material 1").save()
+        ReqMaterial(title="Material 2").save()
         saved_material_request = ReqMaterial.objects.all()
         self.assertEqual(saved_material_request.count(), 2)
 
@@ -2253,118 +2274,255 @@ class RequestMateriTest(TestCase):
         self.assertIn('Missing parameter', response.content.decode())
         self.client.logout()
 
+    def test_displays_all_requested_material(self):
+        self.client.login(**self.contributor_credential)
+        ReqMaterial(title="Material 1").save()
+        ReqMaterial(title="Material 2").save()
+        response = self.client.get(self.url)
+        self.assertContains(response, 'Material 1')
+        self.assertContains(response, 'Material 2')
+
 
 class RatingContributorTest(TransactionTestCase):
     def setUp(self):
+        self.admin_credential = {
+            "email": "admin@gov.id",
+            "password": "passwordtest"
+        }
         self.contributor_credential = {
             "email": "kontributor@gov.id",
             "password": id_generator()
         }
+        self.anonymous_credential = {
+            "email": "anonymous@gov.id",
+            "password": "passwordtest"
+        }
+        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.anonymous = get_user_model().objects.create_user(
+            **self.anonymous_credential, name="Anonymous"
+        )
+
+        self.url = f"/profil/{self.contributor.email}/"
 
     def test_add_rating_contributor(self):
-        RatingContributor.objects.create(score=3, user=self.contributor)
+        RatingContributor.objects.create(score=3, contributor=self.contributor, user=self.anonymous)
         self.assertEqual(1, RatingContributor.objects.count())
 
     def test_add_rating_contributor_should_failed_when_negative(self):
         with self.assertRaises(ValidationError):
-            RatingContributor.objects.create(score=-1, user=self.contributor)
+            RatingContributor.objects.create(score=-1, contributor=self.contributor, user=self.anonymous)
         self.assertEqual(0, RatingContributor.objects.count())
 
     def test_add_rating_contributor_should_failed_when_bigger_than_five(self):
         with self.assertRaises(ValidationError):
-            RatingContributor.objects.create(score=6, user=self.contributor)
+            RatingContributor.objects.create(score=6, contributor=self.contributor, user=self.anonymous)
         self.assertEqual(0, RatingContributor.objects.count())
 
     def test_submit_form_correct_rating_contributor_should_added(self):
-        url = f"/profil/{self.contributor.email}/"
-        self.client.post(url, data={"user": self.contributor.id, "score": 5})
-        self.assertEqual(1, RatingContributor.objects.filter(user=self.contributor.id).count())
-        self.client.post(url, data={"user": self.contributor.id, "score": 1})
-        self.assertEqual(2, RatingContributor.objects.filter(user=self.contributor.id).count())
+        self.client.login(**self.anonymous_credential)
+        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.anonymous.id, "score": 5})
+        self.assertEqual(1, RatingContributor.objects.filter(contributor=self.contributor.id).count())
+        self.client.logout()
+
+        self.client.login(**self.admin_credential)
+        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.admin.id,  "score": 1})
+        self.assertEqual(2, RatingContributor.objects.filter(contributor=self.contributor.id).count())
 
     def test_submit_form_not_correct_rating_contributor_should__not_added(self):
-        url = f"/profil/{self.contributor.email}/"
-        self.client.post(url, data={"user": self.contributor.id, "score": 6})
-        self.assertEqual(0, RatingContributor.objects.filter(user=self.contributor.id).count())
-        self.client.post(url, data={"user": self.contributor.id, "score": 0})
-        self.assertEqual(0, RatingContributor.objects.filter(user=self.contributor.id).count())
-
-<<<<<<< HEAD
-    class EmailTest(TestCase):
-    def test_send_email(self):
-        send_gmail('bass_of_ncreep@yahoo.com', 'Here is the message')
-        mail.send_mail('Subject here', 'Here is the message.',
-            'pmplmailer@gmail.com', ['bass_of_ncreep@yahoo.com'],
-            fail_silently=False)
-        self.assertEqual(len(mail.outbox), 1)
-        self.assertEqual(mail.outbox[0].subject, 'Subject here')
-
-=======
+        self.client.login(**self.anonymous_credential)
+        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.anonymous.id, "score": 6})
+        self.assertEqual(0, RatingContributor.objects.filter(contributor=self.contributor.id).count())
+        self.client.logout()
+
+        self.client.login(**self.admin_credential)
+        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.admin.id, "score": 0})
+        self.assertEqual(0, RatingContributor.objects.filter(contributor=self.contributor.id).count())
+
     def test_average_rating_score_empty(self):
-        url = f"/profil/{self.contributor.email}/"
-        response = self.client.get(url)
+        response = self.client.get(self.url)
         self.assertTemplateUsed(response=response, template_name="app/katalog_kontri.html")
         self.assertContains(response=response, text="Rating: 0", count=1)
         self.assertContains(response=response, text="oleh: 0 orang", count=1)
 
     def test_average_rating_correct(self):
-        url = f"/profil/{self.contributor.email}/"
-        self.client.post(url, data={"user": self.contributor.id, "score": 5})
-        response = self.client.get(url)
+        self.client.login(**self.anonymous_credential)
+        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.anonymous.id, "score": 5})
+        response = self.client.get(self.url)
         self.assertContains(response=response, text="Rating: 5", count=1)
         self.assertContains(response=response, text="oleh: 1 orang", count=1)
 
     def test_average_rating_form_incorrect_correct(self):
-        url = f"/profil/{self.contributor.email}/"
-        self.client.post(url, data={"user": self.contributor.id, "score": 6})
-        response = self.client.get(url)
+        self.client.login(**self.anonymous_credential)
+        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.anonymous.id, "score": 6})
+        response = self.client.get(self.url)
         self.assertContains(response=response, text="Rating: 0", count=1)
         self.assertContains(response=response, text="oleh: 0 orang", count=1)
-        self.client.post(url, data={"user": self.contributor.id, "score": -1})
-        response = self.client.get(url)
+        self.client.logout()
+
+        self.client.login(**self.admin_credential)
+        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.admin.id, "score": -1})
+        response = self.client.get(self.url)
         self.assertContains(response=response, text="Rating: 0", count=1)
         self.assertContains(response=response, text="oleh: 0 orang", count=1)
 
     def test_average_with_multiple_score_correct(self):
         score = 5
         avg = [score]
-        url = f"/profil/{self.contributor.email}/"
-        self.client.post(url, data={"user": self.contributor.id, "score": score})
-        response = self.client.get(url)
+        self.client.login(**self.anonymous_credential)
+        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.anonymous.id, "score": score})
+        response = self.client.get(self.url)
         self.assertContains(response=response, text=f"Rating: {mean(avg)}", count=1)
         self.assertContains(response=response, text=f"oleh: {len(avg)} orang", count=1)
+        self.client.logout()
 
+        self.anonymous2 = get_user_model().objects.create_user(
+            email="anonymous2@gov.id", password="test", name="Anonymous 2"
+        )
         score = 4
         avg.append(score)
-        self.client.post(url, data={"user": self.contributor.id, "score": score})
-        response = self.client.get(url)
+        self.client.login(email=self.anonymous2.email, password="test")
+        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.anonymous2.id, "score": score})
+        response = self.client.get(self.url)
         self.assertContains(response=response, text=f"Rating: {mean(avg)}", count=1)
         self.assertContains(response=response, text=f"oleh: {len(avg)} orang", count=1)
+        self.client.logout()
 
+        self.anonymous3 = get_user_model().objects.create_user(
+            email="anonymous3@gov.id", password="test", name="Anonymous 3"
+        )
         score = 3
         avg.append(score)
-        self.client.post(url, data={"user": self.contributor.id, "score": score})
-        response = self.client.get(url)
+        self.client.login(email=self.anonymous3.email, password="test")
+        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.anonymous3.id, "score": score})
+        response = self.client.get(self.url)
         self.assertContains(response=response, text=f"Rating: {mean(avg)}", count=1)
         self.assertContains(response=response, text=f"oleh: {len(avg)} orang", count=1)
+        self.client.logout()
 
+        self.anonymous4 = get_user_model().objects.create_user(
+            email="anonymous4@gov.id", password="test", name="Anonymous 4"
+        )
         score = 2
         avg.append(score)
-        self.client.post(url, data={"user": self.contributor.id, "score": score})
-        response = self.client.get(url)
+        self.client.login(email=self.anonymous4.email, password="test")
+        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.anonymous4.id, "score": score})
+        response = self.client.get(self.url)
         self.assertContains(response=response, text=f"Rating: {mean(avg)}", count=1)
         self.assertContains(response=response, text=f"oleh: {len(avg)} orang", count=1)
+        self.client.logout()
 
         score = 1
         avg.append(score)
-        self.client.post(url, data={"user": self.contributor.id, "score": score})
-        response = self.client.get(url)
+        self.client.login(**self.admin_credential)
+        self.client.post(self.url, data={"contributor": self.contributor.id, "user": self.admin.id, "score": score})
+        response = self.client.get(self.url)
         self.assertContains(response=response, text=f"Rating: {mean(avg)}", count=1)
         self.assertContains(response=response, text=f"oleh: {len(avg)} orang", count=1)
 
+    def test_not_authenticated_user_should_not_able_to_add_rating(self):
+        response_post = self.client.post(self.url, data={
+            "contributor":self.contributor.id,
+            "user":self.anonymous.id,
+            "score":3
+        })
+        response_get = self.client.get(self.url)
+        self.assertEqual(response_post.status_code, 403)
+        self.assertContains(response_get, "Kamu harus login untuk memberi rating")
+
+    def test_not_authenticated_user_should_not_able_to_delete_rating(self):
+        response_post = self.client.post(self.url, data={"delete":True})
+        self.assertEqual(response_post.status_code, 403)
+
+    def test_authenticated_user_should_not_delete_rating_if_has_not_been_rated(self):
+        self.client.login(**self.admin_credential)
+        self.client.post(self.url, data={
+            "contributor":self.contributor.id,
+            "user":self.admin.id,
+            "score":3
+        })
+
+        self.assertEqual(1, RatingContributor.objects.filter(contributor=self.contributor).count())
+        self.client.logout()
+
+        self.client.login(**self.anonymous_credential)
+        self.client.post(self.url, data={"delete":True})
+
+        self.assertEqual(1, RatingContributor.objects.filter(contributor=self.contributor).count())
+
+    def test_authenticated_user_should_delete_rating_if_has_been_rated(self):
+        self.client.login(**self.admin_credential)
+        self.client.post(self.url, data={
+            "contributor":self.contributor.id,
+            "user":self.admin.id,
+            "score":3
+        })
+        response = self.client.get(self.url)
+        self.assertContains(response, 'Rating: 3')
+        self.assertContains(response, 'oleh: 1 orang')
+        self.assertEqual(1, RatingContributor.objects.filter(contributor=self.contributor).count())
+
+        self.client.post(self.url, data={"delete":True})
+        response = self.client.get(self.url)
+        self.assertContains(response, 'Rating: 0')
+        self.assertContains(response, 'oleh: 0 orang')
+        self.assertEqual(0, RatingContributor.objects.filter(contributor=self.contributor).count())
+
+    def test_average_still_be_correct_when_rating_was_deleted(self):
+        scores = [2,4]
+        self.client.login(**self.admin_credential)
+        self.client.post(self.url, data={
+            "contributor":self.contributor.id,
+            "user":self.admin.id,
+            "score":scores[0]
+        })
+        self.client.logout()
+
+        self.client.login(**self.anonymous_credential)
+        self.client.post(self.url, data={
+            "contributor":self.contributor.id,
+            "user":self.anonymous.id,
+            "score":scores[1]
+        })
+        response = self.client.get(self.url)
+        self.assertContains(response, f'Rating: {mean(scores)}')
+        self.assertContains(response, 'oleh: 2 orang')
+
+        self.client.post(self.url, data={"delete":True})
+        response = self.client.get(self.url)
+        self.assertContains(response, f'Rating: {scores[0]}')
+        self.assertContains(response, 'oleh: 1 orang')
+
+    def test_authenticated_user_should_update_rating_if_has_been_rated(self):
+        self.client.login(**self.anonymous_credential)
+        self.client.post(self.url, data={
+            "contributor":self.contributor.id,
+            "user":self.anonymous.id,
+            "score":5
+        })
+        response = self.client.get(self.url)
+        self.assertContains(response, 'Rating: 5')
+        self.assertContains(response, 'oleh: 1 orang')
+
+        self.client.post(self.url, data={"update":True, "score":3})
+        response = self.client.get(self.url)
+        self.assertContains(response, 'Rating: 3')
+        self.assertContains(response, 'oleh: 1 orang')
+
+    def test_authenticated_user_should_not_update_rating_if_has_not_been_rated(self):
+        self.client.login(**self.anonymous_credential)
+        response = self.client.get(self.url)
+        self.assertContains(response, 'Rating: 0')
+        self.assertContains(response, 'oleh: 0 orang')
+
+        self.client.post(self.url, data={"update":True, "score":3})
+        response = self.client.get(self.url)
+        self.assertContains(response, 'Rating: 0')
+        self.assertContains(response, 'oleh: 0 orang')
+
 
 class UserDownloadHistoryTest(TestCase):
     def setUp(self):
@@ -2853,8 +3011,8 @@ class YTUrlVideoTest(TestCase):
         )
 
     def test_yt_video_id_exists_in_Materi(self):
-        materiTemp = Materi.objects.create()
-        self.assertEqual(hasattr(materiTemp, 'yt_video_id'), True)
+        materi_temp = Materi.objects.create()
+        self.assertEqual(hasattr(materi_temp, 'yt_video_id'), True)
 
     def test_yt_video_id_exists_in_UploadMateri_page(self):
         self.client.login(**self.contributor_credential)
@@ -2864,7 +3022,7 @@ class YTUrlVideoTest(TestCase):
 
     def test_upload_materi_with_valid_yt_video_id(self):
         self.client.login(**self.contributor_credential)
-        response = self.client.post(
+        self.client.post(
             "/unggah/", data={"title":"Materi 1", "author":"Agas", "publisher":"Kelas SC", "release_year":"2000",
                                 "descriptions":"Deskripsi Materi 1", 'categories':"1",
                                 "cover":self.cover, "content":self.content,
@@ -2874,7 +3032,7 @@ class YTUrlVideoTest(TestCase):
 
     def test_upload_materi_with_invalid_yt_video_id(self):
         self.client.login(**self.contributor_credential)
-        response = self.client.post(
+        self.client.post(
             "/unggah/", data={"title":"Materi 2", "author":"Agas", "publisher":"Kelas SC", "release_year":"2000",
                                 "descriptions":"Deskripsi Materi 1", 'categories':"1",
                                 "cover":self.cover, "content":self.content,
@@ -2891,7 +3049,7 @@ class YTUrlVideoTest(TestCase):
 
     def test_detail_materi_has_no_video_if_yt_video_id_empty(self):
         self.client.login(**self.contributor_credential)
-        response = self.client.post(
+        self.client.post(
             "/unggah/", data={"title":"Materi 2", "author":"Agas", "publisher":"Kelas SC", "release_year":"2000",
                                 "descriptions":"Deskripsi Materi 1", 'categories':"1",
                                 "cover":self.cover, "content":self.content,}
@@ -2923,17 +3081,7 @@ class ChangePasswordTest(TestCase):
         # Logout
         self.client.logout()
 
-    def test_change_password_url(self):
-        # Login
-        self.client.login(email="kontributor@gov.id", password="kontributor")
-        # Test
-        response = self.client.get(self.url)
-        self.assertEqual(response.status_code, 200)
-        # Logout
-        self.client.logout()
-
-    def test_change_password_access(self):
-        # Kontributor
+    def test_change_password_url_access(self):
         # Login
         self.client.login(email="kontributor@gov.id", password="kontributor")
         # Test
@@ -3244,7 +3392,7 @@ class MateriRecommendationTest(TestCase):
         Like.objects.create(materi=materi2)
         Like.objects.create(materi=materi2)
 
-        materi3 = Materi.objects.create(
+        Materi.objects.create(
             title="Materi 3",
             author="Nandhika Prayoga",
             uploader=self.contributor,
@@ -3297,5 +3445,3 @@ class MateriRecommendationTest(TestCase):
         response = Client().get("/?recommendation=1")
         list = [int(id) for id in re.findall(r"Materi\s(\d+)[^\d]", response.content.decode())]
         self.assertEqual(list, [1, 2])
-
->>>>>>> 59a5a847cfe9cd71b272834c2ba68cbc7c331a7c
diff --git a/app/views.py b/app/views.py
index ad2cb9b0681e1a325889fb486f99122d7f303cf1..306e84073b13dea436e90faa2952bafcb618f81c 100644
--- a/app/views.py
+++ b/app/views.py
@@ -97,19 +97,51 @@ class KatalogPerKontributorView(TemplateView):
         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})
+        contributor = context["contributor"]
+        context["form_rating"] = RatingContributorForm(initial={
+            "contributor": contributor,
+            "user":request.user
+        })
         context["avg_rating"] = User.objects.filter(email=kwargs["email"]) \
-            .annotate(avg_rating=Avg("ratingcontributor__score"))[0]
-        context["count_rating"] = RatingContributor.objects.filter(user=contributor).count()
+            .annotate(avg_rating=Avg("contributor__score"))[0]
+        context["count_rating"] = RatingContributor.objects.filter(contributor=contributor).count()
+
+        if request.user.is_authenticated:
+            has_rated = RatingContributor.objects.filter(user=request.user, contributor=contributor).exists()
+            context["has_rated"] = has_rated
+
         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"])
+        context = self.get_context_data(**kwargs)
+        if not request.user.is_authenticated:
+            raise PermissionDenied(request)
 
+        is_delete = request.POST.get('delete', None)
+        is_update = request.POST.get('update', None)
+        if is_delete:
+            rating_contributor = get_object_or_404(
+                RatingContributor, 
+                user=request.user,
+                contributor=context["contributor"]
+            )
+            rating_contributor.delete()
+        elif is_update:
+            score = request.POST.get('score', None)
+            rating = RatingContributor.objects.filter(
+                user=request.user,
+                contributor=context["contributor"]
+            ).first()
+            
+            if rating and score:
+                rating.score = int(score)
+                rating.save()
+        else:
+            data = RatingContributorForm(request.POST)
+            if data.is_valid():
+                data.save()
+                
+        return redirect("katalog-per-kontributor", email=kwargs["email"])
 
 class DetailMateri(TemplateView):
     template_name = "app/detail_materi.html"
@@ -141,8 +173,8 @@ class DetailMateri(TemplateView):
         has_disliked, has_liked = DetailMateriService.find_comment_like_dislike(query_set_for_comment, self.request.session)
         context["comment_data"] = query_set_for_comment
         context["review_data"] = query_set_for_review
-        context["has_liked"] = has_liked
-        context["has_disliked"] = has_disliked
+        context["has_liked_comment"] = has_liked
+        context["has_disliked_comment"] = has_disliked
         return self.render_to_response(context=context)
 
 
@@ -605,6 +637,7 @@ class ReqMateriView(TemplateView):
 
     def get(self, request, *args, **kwargs):
         context = self.get_context_data(**kwargs)
+        context['requested_material'] = ReqMaterial.objects.all()
         return self.render_to_response(context)
 
     def post(self, request, *args, **kwargs):
diff --git a/authentication/models.py b/authentication/models.py
index 721b69c9f6d364de73e08ad1cbd4583be39e1172..84b835807a6b7e58b49389c35296466de2bc82a2 100644
--- a/authentication/models.py
+++ b/authentication/models.py
@@ -81,6 +81,7 @@ class User(AbstractUser):
     default_profile_picture = models.BooleanField(blank=True, default=False)
     profile_picture = models.ImageField(default="default-image.jpg")
     is_subscribing_to_material_comments = models.BooleanField(blank=False, default=True)
+    is_email_verified = models.BooleanField(default=False)
 
     objects = CustomUserManager()
 
diff --git a/register/migrations/__init__.py b/register/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/register/models.py b/register/models.py
index 71a836239075aa6e6e4ecb700e9c42c95c022d91..56cabee4d822988bebddb13863282faef88b581f 100644
--- a/register/models.py
+++ b/register/models.py
@@ -1,3 +1,11 @@
+from authentication.models import User
 from django.db import models
+import uuid
 
 # Create your models here.
+
+class EmailVerification(models.Model):
+    token = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
+    created_on = models.DateTimeField(auto_now_add=True)
+    expire_on = models.DateTimeField(null=True)
+    user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
diff --git a/register/services.py b/register/services.py
index 603855b114790a85861dc06a4200f2c777256cd9..a668e0426d20c444831f43d430fffe01deddcdc2 100644
--- a/register/services.py
+++ b/register/services.py
@@ -1,6 +1,12 @@
+import datetime
 from django.contrib.auth.hashers import make_password
 from django.contrib.auth.password_validation import validate_password
 from django.core.exceptions import ValidationError
+from .models import EmailVerification
+from django.utils import timezone
+from django.conf import settings
+from django.core.mail import send_mail
+from django.urls import reverse
 
 class RegistrationService:
 
@@ -69,3 +75,27 @@ class RegistrationService:
             create_result["form"] = form
 
         return create_result
+
+
+    @staticmethod
+    def create_email_verification(request, user):
+        verify = EmailVerification()
+        verify.created_on = timezone.now()
+        verify.expire_on = verify.created_on + datetime.timedelta(hours=24)
+        verify.user = user
+        verify.save()
+        
+        path = reverse('register:verify-email', args=[verify.token])
+        url = request.build_absolute_uri(path)
+
+        email_content = f"""
+                            Dear {user.name},\n\n
+                            Mohon verifikasi email Anda dengan klik pada link berikut: {url}
+                        """
+
+        send_mail(
+            subject = 'DIGIPUS: Verifikasi Alamat Email',
+            message = email_content,
+            from_email = getattr(settings, 'EMAIL_HOST_USER'),
+            recipient_list = [user.email],
+            fail_silently = False)
diff --git a/register/templates/email_verify.html b/register/templates/email_verify.html
new file mode 100644
index 0000000000000000000000000000000000000000..90909d04e636e165b11a42e1c83d78ebcd19b0ee
--- /dev/null
+++ b/register/templates/email_verify.html
@@ -0,0 +1,69 @@
+{% load static %}
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Verifikasi Email</title>
+    <link rel="icon" type="image/png" href="{% static 'images/icons/logo.ico' %}" />
+    <!--===============================================================================================-->
+    <link rel="stylesheet" type="text/css" href="{% static 'vendor/bootstrap/css/bootstrap.min.css' %}">
+    <!--===============================================================================================-->
+    <link rel="stylesheet" type="text/css" href="{% static 'fonts/font-awesome-4.7.0/css/font-awesome.min.css' %}">
+    <!--===============================================================================================-->
+    <!--===============================================================================================-->
+    <link rel="stylesheet" type="text/css" href="{% static 'vendor/animate/animate.css' %}">
+    <!--===============================================================================================-->
+    <link rel="stylesheet" type="text/css" href="{% static 'vendor/css-hamburgers/hamburgers.min.css' %}">
+    <!--===============================================================================================-->
+    <link rel="stylesheet" type="text/css" href="{% static 'vendor/animsition/css/animsition.min.css' %}">
+    <!--===============================================================================================-->
+    <link rel="stylesheet" type="text/css" href="{% static 'vendor/select2/select2.min.css' %}">
+    <!--===============================================================================================-->
+    <link rel="stylesheet" type="text/css" href="{% static 'vendor/daterangepicker/daterangepicker.css' %}">
+    <!--===============================================================================================-->
+    <link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}">
+    <link rel="stylesheet" type="text/css" href="{% static 'css/util.css' %}">
+    <link rel="stylesheet" type="text/css" href="{% static 'css/main.css' %}">
+    <!--===============================================================================================-->
+    <link href="https://fonts.googleapis.com/css2?family=Montserrat:ital@1&display=swap" rel="stylesheet"> 
+    <link href="https://fonts.googleapis.com/css2?family=Poppins&display=swap" rel="stylesheet">
+</head>
+<body>
+    
+    <main>
+
+        {% if message %}
+
+            <div>
+                {{ message }}
+            </div>
+
+            <div>
+                Kembali ke <a href="/">home</a>
+            </div>
+
+        {% endif %}
+
+    </main>
+    <!--===============================================================================================-->
+    <script src="../static/vendor/jquery/jquery-3.2.1.min.js"></script>
+    <!--===============================================================================================-->
+    <script src="../static/vendor/animsition/js/animsition.min.js"></script>
+    <!--===============================================================================================-->
+    <script src="../static/vendor/bootstrap/js/popper.js"></script>
+    <script
+        src="../static/../static/../static/../static/../static/../static/vendor/bootstrap/js/bootstrap.min.js"></script>
+    <!--===============================================================================================-->
+    <script src="../static/../static/../static/../static/../static/vendor/select2/select2.min.js"></script>
+    <!--===============================================================================================-->
+    <script src="../static/../static/../static/../static/vendor/daterangepicker/moment.min.js"></script>
+    <script src="../static/../static/../static/vendor/daterangepicker/daterangepicker.js"></script>
+    <!--===============================================================================================-->
+    <script src="../static/../static/vendor/countdowntime/countdowntime.js"></script>
+    <!--===============================================================================================-->
+    <script src="../static/js/login.js"></script>
+    <script src="../static/js/navbar.js"></script>
+</body>
+</html>
\ No newline at end of file
diff --git a/register/tests.py b/register/tests.py
index ef8205860cad0b4362a48bf9b5c1b7634595d772..799c0d2ea8e8610447e7397ca152b6a7f64e0b07 100644
--- a/register/tests.py
+++ b/register/tests.py
@@ -1,3 +1,8 @@
+import datetime
+from django.core import mail
+
+from django.utils import timezone
+from register.models import EmailVerification
 from django.test import Client, TestCase
 from django.urls import resolve
 
@@ -883,3 +888,47 @@ class RegisterPublicTest(TestCase):
         self.assertEqual(User.objects.all().count(), 0)
         self.assertIn(b"Password must have at least 8 characters", response.content)
 
+
+class VerifyEmailTest(TestCase):
+
+    def setUp(self):
+        self.credential = {
+            'email':'example@test.com', 
+            'password':'P@ssw0rd'
+            }
+        self.user = User.objects.create_contributor(**self.credential)
+        self.user.save()
+
+        self.verif_valid = EmailVerification(
+            expire_on=timezone.now() + datetime.timedelta(hours=24),
+            user = self.user)
+        self.verif_valid.save()
+
+        self.verif_expire = EmailVerification(
+            created_on=timezone.now() + datetime.timedelta(hours=-24),
+            expire_on=timezone.now() + datetime.timedelta(hours=-1),
+            user = self.user)
+        self.verif_expire.save()
+
+        self.client = Client()
+
+    def test_email_notif_anonymous(self):
+        response = self.client.post('/registrasi/send-verify-email', {'email': self.user.email})
+        self.assertEqual(response.status_code, 403)
+
+    def test_email_notif_authorized(self):
+        login = self.client.login(**self.credential)
+        self.client.post('/registrasi/send-verify-email', {'email': self.user.email})
+        self.assertEqual(len(mail.outbox), 1)
+
+    def test_verify_email_before_24h(self):
+        response = self.client.get(f'/registrasi/verify-email/{self.verif_valid.token}')
+        self.assertContains(response, f'Email Anda {self.verif_valid.user.email} berhasil diverifikasi')
+
+    def test_verify_email_after_24h(self):
+        response = self.client.get(f'/registrasi/verify-email/{self.verif_expire.token}')
+        self.assertContains(response, 'Link verifikasi telah expire')
+
+    def test_verify_email_wrong_token(self):
+        response = self.client.get(f'/registrasi/verify-email/not-a-valid-token')
+        self.assertContains(response, 'Email gagal diverifikasi')
diff --git a/register/urls.py b/register/urls.py
index 0acd03ba88ac4292c91e311da81766e4f8faf4e3..53d6a6ed6a4629ab084d3bac362718086b6126f6 100644
--- a/register/urls.py
+++ b/register/urls.py
@@ -7,5 +7,7 @@ app_name = "register"
 urlpatterns = [
     path("", views.index.as_view()),
     path("umum/", views.RegistrasiUmum.as_view()),
-    path("admin/", views.RegistrasiAdmin.as_view())
+    path("admin/", views.RegistrasiAdmin.as_view()),
+    path("verify-email/<str:token>", views.verify_email, name='verify-email'),
+    path("send-verify-email", views.send_verify_email),
 ]
diff --git a/register/views.py b/register/views.py
index 3d8112fef3563207ce1cdb5b294e6ff26c340acc..91952d6907cb91b74dfd2a6e5e451f0c0a0f26c4 100644
--- a/register/views.py
+++ b/register/views.py
@@ -1,9 +1,16 @@
+from django.http import response
+from authentication.models import User
+from django.utils import timezone
+
+from django.shortcuts import render
+from register.models import EmailVerification
 from django.contrib.auth import login
+from django.db import models
 from django.http import HttpResponseRedirect
 from django.views.generic import TemplateView
+from django.core.exceptions import PermissionDenied
 
 from register.forms import UserForm
-# Create your views here.
 from register.services import RegistrationService
 
 
@@ -98,3 +105,36 @@ class RegistrasiUmum(TemplateView):
         context = self.get_context_data(**kwargs)
         context["form"] = UserForm
         return self.render_to_response(context=context)
+
+
+def send_verify_email(request):
+    if not request.user.is_authenticated:
+        raise PermissionDenied(request)
+
+    if request.method == 'POST':
+        RegistrationService.create_email_verification(request, request.user)
+        return response.HttpResponse('Email verifikasi telah dikirim')
+
+
+def verify_email(request, token):
+    template_name = 'email_verify.html'
+
+    try:
+        verif = EmailVerification.objects.get(token=token)
+        
+        if verif.expire_on > timezone.now():
+            user = User.objects.get(email=verif.user.email)
+            user.is_email_verified = True
+            user.save()
+            
+            return render(request, template_name, {
+                'message': f'Email Anda {verif.user.email} berhasil diverifikasi'
+                })
+        else:
+            return render(request, template_name, {
+                'message': f'Link verifikasi telah expire'
+                })
+    except:
+        return render(request, template_name, {
+            'message': f'Email gagal diverifikasi'
+            })
diff --git a/sonar-project.properties b/sonar-project.properties
index 8ef13a8bdcd322e265a0a36ff3b533387a69043a..754cc49ecee83369ab9e62537c2721e67a9e9f5e 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -9,13 +9,13 @@ sonar.projectKey=$SONARQUBE_PROJECT_KEY
 
 ## Path to sources
 sonar.sources=.
-sonar.exclusions=**/migrations/**, **/__init__.py, digipus/**, **/test.py, manage.py, .vscode/**, static/**, staticfiles/**, static/admin/**, **/static/**
+sonar.exclusions=**/migrations/**, **/__init__.py, digipus/**, **/tests.py, manage.py, .vscode/**, static/**, staticfiles/**, static/admin/**, **/static/**
 # sonar.inclusions=
 
 ## Path to tests
 sonar.tests=.
 #sonar.test.exclusions=
-sonar.test.inclusions=**/test.py
+sonar.test.inclusions=**/tests.py
 
 ## Source encoding
 sonar.sourceEncoding=UTF-8
diff --git a/staticfiles/css/button.css b/staticfiles/css/button.css
index 90a8a87a089ed75ec7f3eda8a752f03fdf10b3e6..8ea2c9fe4fff979bcd96c0dedb3921ae5c782594 100755
--- a/staticfiles/css/button.css
+++ b/staticfiles/css/button.css
@@ -117,4 +117,9 @@
 .margin-content {
     margin: 0;
     padding: 0;
+}
+
+.constraint-frame {
+    max-width: 550px; 
+    max-height: 550px;
 }
\ No newline at end of file
diff --git a/staticfiles/js/upload-preview.js b/staticfiles/js/upload-preview.js
new file mode 100644
index 0000000000000000000000000000000000000000..c424f70d2de26b0c31676d1e0435928f92ef20c7
--- /dev/null
+++ b/staticfiles/js/upload-preview.js
@@ -0,0 +1,22 @@
+window.onload = () => {
+  document.querySelector('input[name="cover"]').setAttribute("onchange", "preview('frame_cover')");
+  document.querySelector('input[name="content"]').setAttribute("onchange", "preview('frame_content')");
+}
+
+const preview = (elementClass) => {
+  let frame = document.getElementById(elementClass);
+  let frameParent = document.getElementById(`${elementClass}_parent`);
+  
+  // Embed elements need to be recreated on source change
+  if (frame.nodeName.toLowerCase() === 'embed') {
+    frameParent.removeChild(frame);
+    frameParent.insertAdjacentHTML(
+      'beforeend',
+      '<embed id="frame_content" width="550px" height="550px" />'
+    );
+    frame = document.getElementById(elementClass);
+  }
+
+  frame.src = URL.createObjectURL(event.target.files[0]);
+  frameParent.removeAttribute('hidden');
+}
\ No newline at end of file