diff --git a/app/models.py b/app/models.py index fb932f7546c54b0f22af0e02c9e1212f123b51f6..b4d3fda4b9f0563394814cb97af4647a76cfdafd 100644 --- a/app/models.py +++ b/app/models.py @@ -1,5 +1,6 @@ import random import datetime +import math from django.contrib.postgres import search from django.core.exceptions import ValidationError @@ -135,6 +136,25 @@ class Materi(SoftDeleteModel): count = Review.objects.filter(materi=self).count() return count + @staticmethod + def earliest_materi_timestamp(): + return Materi.objects.earliest('date_created').date_created.timestamp() + + @property + def seconds_since_earliest_materi(self): + return self.date_created.timestamp() - Materi.earliest_materi_timestamp() + + @property + def view_count(self): + count = ViewStatistics.objects.filter(materi=self).count() + return count + + @property + def hot_score(self): + view_score = math.log(max(self.view_count, 1), 10) + time_score = self.seconds_since_earliest_materi / 604800 # 1 week + return round(view_score + time_score, 7) + @property def is_like(self): like = False diff --git a/app/services.py b/app/services.py index 4ae99b55b1349e03cdcf79d9ef244944592b64b9..6b298bf76b65929bf2a458ee31869639f6023a8a 100644 --- a/app/services.py +++ b/app/services.py @@ -63,6 +63,9 @@ class DafterKatalogService: lst_materi = lst_materi.order_by('date_created') elif (get_sort == "terpopuler"): lst_materi = lst_materi.annotate(count=Count('like__id')).order_by('-count') + elif (get_sort == "terhangat"): + lst_materi = sorted(lst_materi, + key=lambda t: (t.hot_score, t.date_created), reverse=True) elif (get_sort == "jumlah_unduh"): lst_materi = lst_materi.annotate(count=Count('unduh__id')).order_by('-count') elif (get_sort == "jumlah_tampilan"): diff --git a/app/templates/app/katalog_materi.html b/app/templates/app/katalog_materi.html index 62736dfe989318cc88f0b122fb4ca01e737cc96a..9309d5d8bbe9223685f91ffb7169f08256718193 100644 --- a/app/templates/app/katalog_materi.html +++ b/app/templates/app/katalog_materi.html @@ -134,6 +134,9 @@ <li> <a href="?sort=terpopuler">terpopuler</a> </li> + <li> + <a href="?sort=terhangat">terhangat</a> + </li> <li> <a href="?sort=judul">judul</a> </li> diff --git a/app/tests.py b/app/tests.py index d65e62e06fe7eda4938aa653c4da9a976b4b3100..79d25ec49eda35ade3c1db8746815e3017c0b7ce 100644 --- a/app/tests.py +++ b/app/tests.py @@ -6,6 +6,8 @@ import random import re import tempfile import time +import itertools +from django.test import override_settings from datetime import datetime from io import StringIO from time import sleep @@ -27,6 +29,7 @@ from django.core.files import File from django.core.files.uploadedfile import SimpleUploadedFile from django.core.management import call_command from django.db.utils import IntegrityError +from pytz import timezone, UTC from django.test import (Client, RequestFactory, TestCase, TransactionTestCase, override_settings) from django.urls import resolve, reverse @@ -207,6 +210,163 @@ class DaftarKatalogSortingByJumlahTampilanTest(TestCase): response = self.client.get("/?sort=jumlah_tampilan") self.assertRegex(str(response.content), rf'.*Materi 2.*Materi 1.*') + +class DaftarKatalogSortingByTerhangatTest(TestCase): + @classmethod + def generate_view_materi(cls, materi, view_count): + for _ in itertools.repeat(None, view_count): + ViewStatistics.objects.create(materi=materi) + + def get_displayed_materi_in_number(self): + response = self.client.get("/?sort=terhangat") + lst = [int(id) for id in re.findall(r"Materi\s(\d+)[^\d]", + response.content.decode())] + return lst + + def setUp(self): + self.contributor_credential = { + "email": "kontributor@gov.id", + "password": id_generator() + } + self.contributor = get_user_model().objects.create_user( + name="Kontributor", + is_contributor=True, + **self.contributor_credential, + ) + self.client = Client() + content = b"Test file" + self.cover = SimpleUploadedFile( + "cover.jpg", + content + ) + self.content = SimpleUploadedFile( + "content.txt", + content + ) + + self.materi_data = { + "author": "Reyhan", + "uploader": self.contributor, + "publisher": "Publisher", + "descriptions": "Deskripsi Materi", + "status": "APPROVE", + "cover": self.cover, + "content": self.content, + } + + def test_1_week_difference_give_1_hot_score_difference(self): + materi1 = Materi.objects.create( + title='Materi 1', + date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC), + **self.materi_data + ) + materi2 = Materi.objects.create( + title='Materi 2', + date_created=datetime(2020, 10, 8, 7, 0, 0, tzinfo=UTC), + **self.materi_data + ) + materi3 = Materi.objects.create( + title='Materi 3', + date_created=datetime(2020, 10, 15, 7, 0, 0, tzinfo=UTC), + **self.materi_data + ) + self.generate_view_materi(materi1, 1) + self.generate_view_materi(materi2, 1) + self.generate_view_materi(materi3, 1) + + self.assertAlmostEqual(materi3.hot_score - materi2.hot_score, 1) + self.assertAlmostEqual(materi2.hot_score - materi1.hot_score, 1) + + def test_10_exponential_view_count_difference_give_1_hot_score_difference(self): + materi1 = Materi.objects.create( + title='Materi 1', + date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC), + **self.materi_data + ) + materi2 = Materi.objects.create( + title='Materi 2', + date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC), + **self.materi_data + ) + materi3 = Materi.objects.create( + title='Materi 3', + date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC), + **self.materi_data + ) + self.generate_view_materi(materi1, 1) + self.generate_view_materi(materi2, 10) + self.generate_view_materi(materi3, 100) + + self.assertAlmostEqual(materi3.hot_score - materi2.hot_score, 1) + self.assertAlmostEqual(materi2.hot_score - materi1.hot_score, 1) + + def test_0_and_1_views_has_the_same_hot_score(self): + materi1 = Materi.objects.create( + title='Materi 1', + date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC), + **self.materi_data + ) + materi2 = Materi.objects.create( + title='Materi 2', + date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC), + **self.materi_data + ) + self.generate_view_materi(materi1, 0) + self.generate_view_materi(materi2, 1) + + self.assertAlmostEqual(materi1.hot_score, materi2.hot_score) + + def test_page_has_option_sort_by_hottest(self): + response = self.client.get("/") + self.assertIn("terhangat", response.content.decode()) + + def test_page_display_sort_by_hottest(self): + materi1 = Materi.objects.create( + title='Materi 1', + date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC), + **self.materi_data + ) + materi2 = Materi.objects.create( + title='Materi 2', + date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC), + **self.materi_data + ) + materi3 = Materi.objects.create( + title='Materi 3', + date_created=datetime(2020, 10, 8, 7, 0, 0, tzinfo=UTC), + **self.materi_data + ) + materi4 = Materi.objects.create( + title='Materi 4', + date_created=datetime(2020, 10, 9, 7, 0, 0, tzinfo=UTC), + **self.materi_data + ) + self.generate_view_materi(materi1, 11) + self.generate_view_materi(materi2, 10) + self.generate_view_materi(materi3, 1) + self.generate_view_materi(materi4, 1) + + lst = self.get_displayed_materi_in_number() + self.assertEqual(lst, [4, 1, 3, 2]) + + def test_prefer_newest_materi_if_hot_score_is_same(self): + materi1 = Materi.objects.create( + title='Materi 1', + date_created=datetime(2020, 10, 1, 7, 0, 0, tzinfo=UTC), + **self.materi_data + ) + materi2 = Materi.objects.create( + title='Materi 2', + date_created=datetime(2020, 10, 8, 7, 0, 0, tzinfo=UTC), + **self.materi_data + ) + self.generate_view_materi(materi1, 10) + self.generate_view_materi(materi2, 1) + + lst = self.get_displayed_materi_in_number() + self.assertEqual(lst, [2, 1]) + + class DaftarKatalogSortingByJumlahKomentarTest(TestCase): def setUp(self): self.client = Client() @@ -3465,7 +3625,7 @@ 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]) - + class BacaNantiTest(TestCase): def setUp(self): self.contributor_credential = { diff --git a/digipus/__pycache__/settings.cpython-36.pyc b/digipus/__pycache__/settings.cpython-36.pyc index 43d2d1c4169e7113ebfa287197b9840fd637a652..2e8d5505327c65aad19bcc06e556bd3f737fca54 100644 Binary files a/digipus/__pycache__/settings.cpython-36.pyc and b/digipus/__pycache__/settings.cpython-36.pyc differ