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> {{ 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