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