From f96ddea61b11763bbd20e8af4aee608e2c7d21e6 Mon Sep 17 00:00:00 2001
From: Jonathan Christopher Jakub <jonathan.christopher@ui.ac.id>
Date: Sat, 3 Oct 2020 22:28:43 +0700
Subject: [PATCH] [#75] Group comments per post on User Comments page

---
 app/static/app/css/user_uploaded_posts.css |  50 +++++++
 app/templates/app/includes/sidebar.html    |   6 +-
 app/templates/user_uploaded_posts.html     |  72 ++++++++++
 app/tests.py                               | 151 +++++++++++++--------
 app/urls.py                                |   4 +-
 app/views.py                               |  42 +++---
 6 files changed, 244 insertions(+), 81 deletions(-)
 create mode 100644 app/static/app/css/user_uploaded_posts.css
 create mode 100644 app/templates/user_uploaded_posts.html

diff --git a/app/static/app/css/user_uploaded_posts.css b/app/static/app/css/user_uploaded_posts.css
new file mode 100644
index 0000000..46d49de
--- /dev/null
+++ b/app/static/app/css/user_uploaded_posts.css
@@ -0,0 +1,50 @@
+.posts-vertically-centered {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	font-size: 1rem;
+}
+
+.posts-space-between-container {
+	display: flex;
+	flex-direction: row;
+	justify-content: space-between;
+}
+
+#posts-profile-picture {
+	border-radius: 50%;
+}
+
+#posts-user-profile {
+	display: flex;
+	flex-direction: row;
+}
+
+#posts-user-name {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	margin-left: 1rem;
+	font-size: 1.5rem;
+}
+
+#posts-img {
+    width: 100px;
+    height: auto;
+	height: inherit !important;
+	margin-right: 1rem;
+}
+
+#posts-info {
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+}
+
+#posts-comment-info {
+	display: flex;
+	flex-direction: row;
+	margin: 0.5rem 6rem 0.5rem 6rem;
+	padding: 0.5rem;
+}
diff --git a/app/templates/app/includes/sidebar.html b/app/templates/app/includes/sidebar.html
index 59f4e67..95d5ea7 100644
--- a/app/templates/app/includes/sidebar.html
+++ b/app/templates/app/includes/sidebar.html
@@ -29,8 +29,8 @@
         </a>
     </li>
     <li class="nav-item">
-        <a class="nav-link" href="{% url 'comments' user.id %}">
-        <span>Komentar</span></a>
+        <a class="nav-link" href="{% url 'posts' %}">
+        <span>Materi Diunggah</span></a>
     </li>
 
-</ul>
\ No newline at end of file
+</ul>
diff --git a/app/templates/user_uploaded_posts.html b/app/templates/user_uploaded_posts.html
new file mode 100644
index 0000000..2ce0d68
--- /dev/null
+++ b/app/templates/user_uploaded_posts.html
@@ -0,0 +1,72 @@
+{% extends 'app/base_dashboard.html' %}
+{% load static %}
+
+{% block title %}
+<title>Materi Diunggah | Digipus</title>
+{% endblock %}
+
+{% block stylesheets %}
+<link rel="stylesheet" type="text/css" href="{% static 'app/css/user_uploaded_posts.css' %}"/>
+{% endblock %}
+
+{% block content %}
+<div class="posts-space-between-container bg-white rounded shadow bd-highlight" style="padding: 1rem 1.5rem; margin-bottom: 1.5rem;">
+	<div id="posts-user-profile">
+		<img id="posts-profile-picture" src="{{ user.profile_picture.url }}" alt="profile-picture" width="100px" height="100px"/>
+		<div id="posts-user-name">
+			Materi oleh <strong>&nbsp{{ user.name }}</strong>
+		</div>
+	</div>
+	<div class="posts-vertically-centered">
+		<span>{{ posts|length }}</span>
+		<span>Materi</span>
+	</div>
+</div>
+
+<div style="padding: 1rem 0" id="posts">
+	{% if posts %}
+		{% for _, post in posts.items %}
+		<div id="post-{{ post.data.id }}">
+			<div class="posts-space-between-container bg-white rounded shadow" style="margin: 0.5rem 2rem; padding: 1rem;">
+				<div id="posts-user-profile">
+					{% if post.data.cover %}
+					<img id="posts-img" src="{{ post.data.cover.url }}" alt="profile-picture"/>
+					{% else %}
+					</div style="background-color: grey; width: 100px; height: 100px;">
+					{% endif %}
+					<div id="posts-info">
+						<span><a class="ml-auto p-1 link" style="text-align: left; font-size: 2rem;" href="{% url 'detail-materi' post.data.id %}">
+							{{ post.data.title }}
+						</a></span>
+						<span style="font-size: 0.75rem; padding-left: 0.3rem;">{{ post.data.date_created }}</span>
+					</div>
+				</div>
+				<div class="posts-vertically-centered">
+					<span>{{ post.comments|length }}</span>
+					<span>Komentar</span>
+				</div>
+			</div>
+            {% for comment in post.comments %}
+			<div id="post-{{ post.data.id }}-comment-{{ comment.id }}">
+				<div id="posts-comment-info" class="bg-white rounded shadow" >
+					<img id="posts-profile-picture" src="{{ comment.user.profile_picture.url }}" alt="profile-picture" width="40px" height="40px" style="margin: 18px"/>
+					<div style="display: flex; align-items: center;">
+						<div style="display: flex; flex-direction: column;">
+							<span style="font-size: 0.9rem;"><strong>{{ comment.user.name }}</strong> - {{ comment.timestamp }}</span>
+							{{ comment.comment }}
+						</div>
+					</div>
+				</div>
+			</div>
+            {% endfor %}
+		</div>
+		{% endfor %}
+	{% else %}
+		<div class="text-center h5">
+			Anda belum memiliki materi yang telah diunggah / disetujui
+		</div>
+	{% endif %}
+
+</div>
+
+{% endblock %}
diff --git a/app/tests.py b/app/tests.py
index e4c3915..196b210 100644
--- a/app/tests.py
+++ b/app/tests.py
@@ -2,7 +2,7 @@ import json
 from io import StringIO
 
 from django.contrib.auth import get_user_model
-from django.core.exceptions import ValidationError
+from django.core.exceptions import PermissionDenied, ValidationError
 from django.core.files.uploadedfile import SimpleUploadedFile
 from django.core.management import call_command
 from django.test import Client, RequestFactory, TestCase
@@ -17,7 +17,7 @@ from .models import Category, Comment, Materi, Like, Rating
 from .views import (DaftarKatalog, DashboardKontributorView, DetailMateri,
                     ProfilKontributorView, SuksesLoginAdminView,
                     SuksesLoginKontributorView, SuntingProfilView,
-                    ProfilAdminView, CommentsView, SuntingProfilAdminView, RevisiMateriView)
+                    ProfilAdminView, PostsView, SuntingProfilAdminView, RevisiMateriView)
 from app.forms import SuntingProfilForm
 
 
@@ -160,69 +160,108 @@ class DetailMateriTest(TestCase):
         self.assertEqual(Comment.objects.all().filter(
             comment="This is new comment by Anonymous").count(), 0)
 
-class ViewCommentsTest(TestCase):
-    def setUp(self):
-        self.client = Client()
-        self.contributor_credential = {
-            "email": "kontributor@gov.id",
-            "password": "passwordtest"
-        }
-        self.matt_damon_credential = {
-            "email": "mattdamon@gov.id",
-            "password": "passwordtest"
+
+class PostsViewTest(TestCase):
+
+    @classmethod
+    def generate_posts_data(cls, user):
+        POST_COUNT = 3
+        COMMENTS_COUNT_PER_POST = [1, 0, 3]
+
+        assert POST_COUNT == len(COMMENTS_COUNT_PER_POST)
+
+        sample_file = SimpleUploadedFile("Test.jpg", b"Test file")
+        sample_category = Category.objects.create(name="Test Category")
+
+        post_comment_group_dict = {}
+        for _ in range(POST_COUNT):
+            post = Materi.objects.create(
+                uploader=user,
+                cover=sample_file,
+                content=sample_file,
+            )
+            post.categories.add(sample_category)
+
+            post_comment_group_dict[post.id] = {
+                "data": post,
+                "comments": [],
+            }
+
+        for i, post_id in enumerate(post_comment_group_dict):
+            post = post_comment_group_dict[post_id]["data"]
+
+            for _ in range(COMMENTS_COUNT_PER_POST[i]):
+                comment = Comment.objects.create(materi=post)
+                post_comment_group_dict[post_id]["comments"].append(comment)
+
+            # order by latest (-timestamp)
+            post_comment_group_dict[post_id]["comments"].reverse()
+
+        return post_comment_group_dict
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.url = '/posts/'
+        cls.user_credentials = {
+            "email": "user@email.com",
+            "password": "justpass"
         }
-        self.contributor = get_user_model().objects.create_user(
-            **self.contributor_credential, name="Kontributor", is_contributor=True)
-        self.mattDamon = get_user_model().objects.create_user(
-            **self.matt_damon_credential, name="Matt Damon", is_contributor=True)
-        self.cover = SimpleUploadedFile(
-            "Cherprang_Areekul40_nJM9dGt.jpg", b"Test file")
-        self.content = SimpleUploadedFile("Bahan_PA_RKK.pdf", b"Test file")
-        Materi(title="Materi 1", author="Agas", uploader=self.contributor,
-               publisher="Kelas SC", descriptions="Deskripsi Materi 1",
-               status="APPROVE", cover=self.cover, content=self.content).save()
-        Materi(title="Materi 2", author="Matt", uploader=self.mattDamon,
-               publisher="Kelas SC", descriptions="Deskripsi Materi 2",
-               status="APPROVE", cover=self.cover, content=self.content).save()
-        self.materi1 = Materi.objects.first()
-        self.materi2 = Materi.objects.get(uploader=self.mattDamon)
-        self.commentByKontributor = Comment.objects.create(username='saul', comment="this is contributor comment", materi=self.materi1, user=self.contributor)
-        self.commentByMatt = Comment.objects.create(username='saul', comment="this is Matt Damon", materi=self.materi2, user=self.mattDamon)
-        self.url = '/kontributor/' + str(self.contributor.id) + '/comments/'
+        cls.user = User.objects.create_user(**cls.user_credentials, is_contributor=True)
+        cls.data = cls.generate_posts_data(cls.user)
 
-    def test_comments_url_exist(self):
-        self.client.login(**self.contributor_credential)
-        response = self.client.get(self.url)
+    def _request_as_user(self):
+        self.client.login(**self.user_credentials)
+        return self.client.get(self.url)
+
+    def test_url_resolves_to_posts_view(self):
+        found = resolve(self.url)
+        self.assertEqual(found.func.__name__, PostsView.as_view().__name__)
+    
+    def test_returns_200_on_authenticated_access(self):
+        response = self._request_as_user()
         self.assertEqual(response.status_code, 200)
-        self.assertNotEqual(response.status_code, 404)
 
-    def test_comments_using_comments_template(self):
-        self.client.login(**self.contributor_credential)
+    def test_returns_403_on_unauthenticated_access(self):
         response = self.client.get(self.url)
-        self.assertTemplateUsed(response, 'comments.html')
+        self.assertRaises(PermissionDenied)
+        self.assertEqual(response.status_code, 403)
 
-    def test_comments_using_comments_func(self):
-        self.client.login(**self.contributor_credential)
-        found = resolve(self.url)
-        self.assertEqual(found.func.__name__, CommentsView.as_view().__name__)
+    def test_returns_correct_template(self):
+        response = self._request_as_user()
+        self.assertTemplateUsed(response, "user_uploaded_posts.html")
 
-    def test_comments_page_render_comments(self):
-        self.client.login(**self.contributor_credential)
-        response = self.client.get(self.url)
-        self.assertContains(response, "this is contributor comment")
-        self.assertNotContains(response, 'bukan comment')
+    def test_success_returns_correct_comment_post_groupings_by_context(self):
+        post_comment_group_dict = self.data
 
-    def test_comments_page_only_render_specific_user_comments(self):
-        self.client.login(**self.contributor_credential)
-        response = self.client.get(self.url)
-        self.assertContains(response, "this is contributor comment")
-        self.assertNotContains(response, "this is Matt Damon")
+        response = self._request_as_user()
+
+        response_user = response.context_data["user"]
+        self.assertEqual(response_user, self.user)
+
+        response_data = response.context_data["posts"]
+        actual_data = post_comment_group_dict
+        self.assertDictEqual(response_data, actual_data)
+
+    def test_html_contains_grouped_posts_and_comments(self):
+        response = self._request_as_user()
+
+        posts = list(self.data.keys())
+        comments = {
+            i: [comment.id for comment in self.data[post_id]["comments"]]
+            for i, post_id in enumerate(posts)
+        }
+
+        self.assertRegex(
+            str(response.content), 
+            rf'.*(<div id="post-{posts[2]}">)' + \
+                rf'.*(<div id="post-{posts[2]}-comment-{comments[2][0]}">)' + \
+                rf'.*(<div id="post-{posts[2]}-comment-{comments[2][1]}">)' + \
+                rf'.*(<div id="post-{posts[2]}-comment-{comments[2][2]}">)' + \
+            rf'.*(<div id="post-{posts[1]}">)' + \
+            rf'.*(<div id="post-{posts[0]}">)' + \
+                rf'.*(<div id="post-{posts[0]}-comment-{comments[0][0]}">)'
+        )
 
-    def test_comments_page_only_for_specific_contributor(self):
-        self.client.login(**self.matt_damon_credential)
-        response = self.client.get(self.url)
-        self.assertEqual(response.status_code, 403)
-        self.assertNotEqual(response.status_code, 200)
 
 class TemplateLoaderTest(TestCase):
     def test_template_loader_url_exist(self):
diff --git a/app/urls.py b/app/urls.py
index c5fa795..ad34cef 100644
--- a/app/urls.py
+++ b/app/urls.py
@@ -4,7 +4,7 @@ from app import views
 from app.views import (DashboardKontributorView, ProfilKontributorView,
                        SuksesLoginAdminView, SuksesLoginKontributorView,
                        SuntingProfilView, UploadMateriHTML, UploadMateriView,
-                       ProfilAdminView, CommentsView, SuntingProfilAdminView, ReqMateriView)
+                       ProfilAdminView, PostsView, SuntingProfilAdminView, ReqMateriView)
 
 urlpatterns = [
     path("", views.DaftarKatalog.as_view(), name="daftar_katalog"),
@@ -24,7 +24,7 @@ urlpatterns = [
     path("sukses-admin/", SuksesLoginAdminView.as_view(), name="sukses-admin"),
     re_path(r"^.*\.html", views.pages, name="pages"),
     path("profil-admin/", ProfilAdminView.as_view(), name="profil-admin"),
-    path('kontributor/<int:pk_user>/comments/', CommentsView.as_view(), name='comments'),
+    path("posts/", PostsView.as_view(), name='posts'),
     path("sunting-admin/", SuntingProfilAdminView.as_view(), name="sunting-admin"),
     path("req-materi/", ReqMateriView.as_view(), name="req-materi"),
     path("post-req-materi/", views.post_req_materi, name="post-req-materi"),
diff --git a/app/views.py b/app/views.py
index 8298326..167f7c1 100644
--- a/app/views.py
+++ b/app/views.py
@@ -486,33 +486,35 @@ class SuksesLoginAdminView(TemplateView):
         return self.render_to_response(context)
 
 
-class CommentsView(TemplateView):
-    template_name = "comments.html"
+class PostsView(TemplateView):
+
+    template_name = "user_uploaded_posts.html"
 
     def dispatch(self, request, *args, **kwargs):
-        if not request.user.pk == kwargs["pk_user"]:
+        if not request.user.is_authenticated:
             raise PermissionDenied(request)
-        return super(CommentsView, self).dispatch(request, *args, **kwargs)
-
-    def get_context_data(self, **kwargs):
-        context = super(CommentsView, self).get_context_data(**kwargs)
-        user = get_object_or_404(User, pk=kwargs["pk_user"])
-        users_materi = Materi.objects.filter(uploader=user)
-        numb_of_comments = 0
-        qset_comments = Comment.objects.none()
-        for materi in users_materi:
-            materi_comments = Comment.objects.filter(materi=materi).count()
-            numb_of_comments += materi_comments
-            qset_comments |= Comment.objects.filter(materi=materi)
-        context['comments'] = qset_comments.order_by('-timestamp')
-        context["numb_of_comments"] = numb_of_comments
-        context["numb_of_materi"] = users_materi.count()
-        return context
+        return super(PostsView, self).dispatch(request, *args, **kwargs)
 
     def get(self, request, *args, **kwargs):
-        context = self.get_context_data(**kwargs)
+        context = super().get_context_data(**kwargs)
+        user = self.request.user
+
+        posts = Materi.objects.filter(uploader=user).order_by("-date_created")
+        posts_data = { post.id: { "data": post, "comments": [] } for post in posts }
+
+        comments = Comment.objects \
+            .filter(materi__id__in=posts_data.keys()) \
+            .order_by("-timestamp")
+
+        for comment in comments:
+            posts_data[comment.materi.id]["comments"].append(comment)
+
+        context["user"] = user
+        context["posts"] = posts_data
+
         return self.render_to_response(context=context)
 
+
 class RevisiMateriView(TemplateView):
     template_name = "revisi.html"
 
-- 
GitLab