diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..12e9607 Binary files /dev/null and b/.DS_Store differ diff --git a/.ebextensions/django.config b/.ebextensions/django.config deleted file mode 100644 index 976a34d..0000000 --- a/.ebextensions/django.config +++ /dev/null @@ -1,21 +0,0 @@ -option_settings: - aws:elasticbeanstalk:application:environment: - DJANGO_SETTINGS_MODULE: "UlasKelas.settings" - POSTGRES_DB: "" - POSTGRES_USER: "" - POSTGRES_PASSWORD: "" - POSTGRES_HOST: "" - SENTRY_DSN: "" - aws:elasticbeanstalk:container:python: - WSGIPath: "UlasKelas.wsgi:application" - aws:elasticbeanstalk:environment:proxy:staticfiles: - /static: static - aws:elasticbeanstalk:environment:process:default: - HealthCheckPath: "/ping" - MatcherHTTPCode: "200-499" - -commands: - 01_postgres_activate: - command: sudo amazon-linux-extras enable postgresql11 - 02_postgres_install: - command: sudo yum install -y postgresql-devel diff --git a/Procfile b/Procfile deleted file mode 100644 index 8dffd15..0000000 --- a/Procfile +++ /dev/null @@ -1,2 +0,0 @@ -release: bash deployment.sh -web: gunicorn UlasKelas.wsgi diff --git a/UlasKelas/.env.sample b/UlasKelas/.env.sample deleted file mode 100644 index 563b698..0000000 --- a/UlasKelas/.env.sample +++ /dev/null @@ -1,10 +0,0 @@ -POSTGRES_DB=postgres -POSTGRES_USER=postgres -POSTGRES_PASSWORD=postgres - -POSTGRES_HOST=localhost -DEBUG=true -# prod : POSTGRES_HOST=postgres - - -SENTRY_DSN= \ No newline at end of file diff --git a/UlasKelas/settings.py b/UlasKelas/settings.py index 20b3847..7548d51 100644 --- a/UlasKelas/settings.py +++ b/UlasKelas/settings.py @@ -30,7 +30,7 @@ # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '-zrv7c0@3$c6#7e#ll!z94oy0=-2-e0eqvy4%so=!z3zw6k=da' +SECRET_KEY = env("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False @@ -80,6 +80,7 @@ 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated' ], + 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning', } ROOT_URLCONF = 'UlasKelas.urls' diff --git a/UlasKelas/urls.py b/UlasKelas/urls.py index e7a2aeb..f80fa51 100644 --- a/UlasKelas/urls.py +++ b/UlasKelas/urls.py @@ -26,10 +26,13 @@ urlpatterns = [ path('admin/', admin.site.urls), path('update-course/', views.update_course), + path('update-leaderboard/', views.update_leaderboard), path('ping', views.ping), path('health-check', views.health_check), path('login/', views.login), - path('api/', include("main.urls")), + path('api/', include(("main.urls", "api"), namespace="v1")), + path('api/v1/', include(("main.urls", "api"), namespace="v1")), + path('api/v2/', include(("main.urls", "api"), namespace="v2")), path('api-auth-token/', views_token.obtain_auth_token), path('token/', views.token, name='token'), path('logout/', views.logout), diff --git a/UlasKelas/wsgi.py b/UlasKelas/wsgi.py index e7e7e44..5244952 100644 --- a/UlasKelas/wsgi.py +++ b/UlasKelas/wsgi.py @@ -16,6 +16,8 @@ application = get_wsgi_application() # Scheduler update course -from courseUpdater import updater -updater.start() +from courseUpdater import updater as course_updater +from leaderboard_updater import updater as leaderboard_updater +course_updater.start() +leaderboard_updater.start() \ No newline at end of file diff --git a/courseUpdater/courseApi.py b/courseUpdater/courseApi.py index ecc2952..167a218 100644 --- a/courseUpdater/courseApi.py +++ b/courseUpdater/courseApi.py @@ -11,8 +11,9 @@ def update_courses(): if json is not None: courses_json = json['courses'] for course_json in courses_json: - course = getCourse(course_json) - course.save() + if course_json['code']: + course = getCourse(course_json) + course.save() def _get_courses_json(): url = django_settings.SUNJAD_BASE_URL + 'susunjadwal/api/courses' diff --git a/instance/config.cfg b/leaderboard_updater/__init__.py similarity index 100% rename from instance/config.cfg rename to leaderboard_updater/__init__.py diff --git a/leaderboard_updater/updater.py b/leaderboard_updater/updater.py new file mode 100644 index 0000000..c42ec4e --- /dev/null +++ b/leaderboard_updater/updater.py @@ -0,0 +1,29 @@ +from datetime import datetime +from django.db import transaction +from django.db.models import Count, Sum +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.interval import IntervalTrigger + +from main.models import Profile, Review + +def update_leaderboard(): + users = Profile.objects.all() + + for user in users: + likes_count = Review.objects.filter(user=user).filter(is_active=True).annotate(likes_count=Count('reviewlike')).aggregate(Sum('likes_count'))['likes_count__sum'] + user.likes_count = likes_count + + with transaction.atomic(): + for user in users: + user.save() + +def start(): + scheduler = BackgroundScheduler() + scheduler.add_job( + update_leaderboard, + trigger=IntervalTrigger(minutes=5), # Every 5 minutes + id="update_courses", + max_instances=1, + replace_existing=True, + ) + scheduler.start() \ No newline at end of file diff --git a/main/admin.py b/main/admin.py index 76c8bb9..884574d 100644 --- a/main/admin.py +++ b/main/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Course, ReviewLike, Tag, Profile, Review, ReviewTag +from .models import Calculator, Course, ReviewLike, ScoreComponent, Tag, Profile, Review, ReviewTag # Register your models here. admin.site.register(Course) @@ -8,3 +8,5 @@ admin.site.register(Review) admin.site.register(ReviewLike) admin.site.register(ReviewTag) +admin.site.register(Calculator) +admin.site.register(ScoreComponent) diff --git a/main/migrations/0003_review_is_reviewed.py b/main/migrations/0003_review_is_reviewed.py new file mode 100644 index 0000000..72dd811 --- /dev/null +++ b/main/migrations/0003_review_is_reviewed.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.2 on 2022-07-22 12:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0002_profile_is_blocked'), + ] + + operations = [ + migrations.AddField( + model_name='review', + name='is_reviewed', + field=models.BooleanField(default=False), + ), + ] diff --git a/main/migrations/0004_calculator_scorecomponent.py b/main/migrations/0004_calculator_scorecomponent.py new file mode 100644 index 0000000..d9e1948 --- /dev/null +++ b/main/migrations/0004_calculator_scorecomponent.py @@ -0,0 +1,34 @@ +# Generated by Django 3.1.2 on 2022-07-28 07:25 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0003_review_is_reviewed'), + ] + + operations = [ + migrations.CreateModel( + name='Calculator', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('total_score', models.FloatField(default=0)), + ('total_percentage', models.FloatField(default=0)), + ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.course')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.profile')), + ], + ), + migrations.CreateModel( + name='ScoreComponent', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.TextField()), + ('weight', models.FloatField()), + ('score', models.FloatField()), + ('calculator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.calculator')), + ], + ), + ] diff --git a/main/migrations/0005_profile_likes_count_review_rating_beneficial_and_more.py b/main/migrations/0005_profile_likes_count_review_rating_beneficial_and_more.py new file mode 100644 index 0000000..0377b46 --- /dev/null +++ b/main/migrations/0005_profile_likes_count_review_rating_beneficial_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.0.6 on 2022-07-30 03:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0004_calculator_scorecomponent'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='likes_count', + field=models.PositiveIntegerField(default=0), + ), + migrations.AddField( + model_name='review', + name='rating_beneficial', + field=models.PositiveSmallIntegerField(default=0), + ), + migrations.AddField( + model_name='review', + name='rating_fit_to_credit', + field=models.PositiveSmallIntegerField(default=0), + ), + migrations.AddField( + model_name='review', + name='rating_fit_to_study_book', + field=models.PositiveSmallIntegerField(default=0), + ), + migrations.AddField( + model_name='review', + name='rating_recommended', + field=models.PositiveSmallIntegerField(default=0), + ), + migrations.AddField( + model_name='review', + name='rating_understandable', + field=models.PositiveSmallIntegerField(default=0), + ), + ] diff --git a/main/migrations/0006_alter_profile_likes_count.py b/main/migrations/0006_alter_profile_likes_count.py new file mode 100644 index 0000000..c1a0ed8 --- /dev/null +++ b/main/migrations/0006_alter_profile_likes_count.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.6 on 2022-07-30 06:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0005_profile_likes_count_review_rating_beneficial_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='likes_count', + field=models.PositiveIntegerField(default=0, null=True), + ), + ] diff --git a/main/migrations/0007_alter_review_rating_beneficial_and_more.py b/main/migrations/0007_alter_review_rating_beneficial_and_more.py new file mode 100644 index 0000000..a3c7bc5 --- /dev/null +++ b/main/migrations/0007_alter_review_rating_beneficial_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 4.0.6 on 2022-07-30 07:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0006_alter_profile_likes_count'), + ] + + operations = [ + migrations.AlterField( + model_name='review', + name='rating_beneficial', + field=models.FloatField(default=0, null=True), + ), + migrations.AlterField( + model_name='review', + name='rating_fit_to_credit', + field=models.FloatField(default=0, null=True), + ), + migrations.AlterField( + model_name='review', + name='rating_fit_to_study_book', + field=models.FloatField(default=0, null=True), + ), + migrations.AlterField( + model_name='review', + name='rating_recommended', + field=models.FloatField(default=0, null=True), + ), + migrations.AlterField( + model_name='review', + name='rating_understandable', + field=models.FloatField(default=0, null=True), + ), + ] diff --git a/main/models.py b/main/models.py index 8c91843..a5301be 100644 --- a/main/models.py +++ b/main/models.py @@ -43,7 +43,7 @@ class Profile(models.Model): role = models.CharField(max_length=63) org_code = models.CharField(max_length=63) is_blocked = models.BooleanField(default=False) - + likes_count = models.PositiveIntegerField(default=0, null=True) class Review(models.Model): """ @@ -69,6 +69,12 @@ class HateSpeechStatus(models.TextChoices): sentimen = models.PositiveSmallIntegerField(null=True) is_anonym = models.BooleanField(default=False) is_active = models.BooleanField(default=True) + is_reviewed = models.BooleanField(default=False) + rating_understandable = models.FloatField(null=True, default=0) + rating_fit_to_credit = models.FloatField(null=True, default=0) + rating_fit_to_study_book = models.FloatField(null=True, default=0) + rating_beneficial = models.FloatField(null=True, default=0) + rating_recommended = models.FloatField(null=True, default=0) def save(self, *args, **kwargs): ''' On save, update timestamps ''' @@ -93,3 +99,21 @@ class Bookmark(models.Model): user = models.ForeignKey(Profile, on_delete=CASCADE) course = models.ForeignKey(Course, on_delete=CASCADE) +class Calculator(models.Model): + """ + Calculator for course score + """ + user = models.ForeignKey(Profile, on_delete=CASCADE) + course = models.ForeignKey(Course, on_delete=CASCADE) + total_score = models.FloatField(default=0) + total_percentage = models.FloatField(default=0) + +class ScoreComponent(models.Model): + """ + Score component for calculator + """ + calculator = models.ForeignKey(Calculator, on_delete=CASCADE) + name = models.TextField() + weight = models.FloatField() + score = models.FloatField() + diff --git a/main/serializers.py b/main/serializers.py index 76acdae..719d9d2 100644 --- a/main/serializers.py +++ b/main/serializers.py @@ -1,8 +1,9 @@ from live_config.views import get_config from main.utils import get_profile_term from rest_framework import serializers +from django.db.models import Avg -from .models import Course, Profile, Review, Tag, Bookmark +from .models import Calculator, Course, Profile, Review, ScoreComponent, Tag, Bookmark # class CurriculumSerializer(serializers.ModelSerializer): # class Meta: @@ -30,7 +31,13 @@ class CourseSerializer(serializers.ModelSerializer): review_count = serializers.SerializerMethodField('get_review_count') code_desc = serializers.SerializerMethodField('get_code_desc') tags = serializers.SerializerMethodField('get_top_tags') - + rating_understandable = serializers.SerializerMethodField('get_rating_understandable') + rating_fit_to_credit = serializers.SerializerMethodField('get_rating_fit_to_credit') + rating_fit_to_study_book = serializers.SerializerMethodField('get_rating_fit_to_study_book') + rating_beneficial = serializers.SerializerMethodField('get_rating_beneficial') + rating_recommended = serializers.SerializerMethodField('get_rating_recommended') + rating_average = serializers.SerializerMethodField('get_rating_average') + def get_code_desc(self, obj): course_prefixes = get_config('course_prefixes') code = obj.code[:4] @@ -56,10 +63,57 @@ def get_top_tags(self, obj): top_tags = [k for k, v in sorted(tag_count.items(), key=lambda tag: tag[1], reverse=True)] return top_tags[:3] + def get_all_rating(self, obj): + obj.ratings = obj.reviews.filter(is_active=True).filter(hate_speech_status='APPROVED').filter(rating_understandable__gte=1).aggregate( + rating_understandable=Avg('rating_understandable'), + rating_fit_to_credit=Avg('rating_fit_to_credit'), + rating_fit_to_study_book=Avg('rating_fit_to_study_book'), + rating_beneficial=Avg('rating_beneficial'), + rating_recommended=Avg('rating_recommended') + ) + + def get_rating_understandable(self, obj): + if not hasattr(obj, 'ratings'): + self.get_all_rating(obj) + return obj.ratings.get('rating_understandable') or 0.0 + + def get_rating_fit_to_credit(self, obj): + if not hasattr(obj, 'ratings'): + self.get_all_rating(obj) + return obj.ratings.get('rating_fit_to_credit') or 0.0 + + def get_rating_fit_to_study_book(self, obj): + if not hasattr(obj, 'ratings'): + self.get_all_rating(obj) + return obj.ratings.get('rating_fit_to_study_book') or 0.0 + + def get_rating_beneficial(self, obj): + if not hasattr(obj, 'ratings'): + self.get_all_rating(obj) + return obj.ratings.get('rating_beneficial') or 0.0 + + def get_rating_recommended(self, obj): + if not hasattr(obj, 'ratings'): + self.get_all_rating(obj) + return obj.ratings.get('rating_recommended') or 0.0 + + def get_rating_average(self, obj): + if not hasattr(obj, 'ratings'): + self.get_all_rating(obj) + + rating_total = (obj.ratings.get('rating_understandable') or 0.0)\ + + (obj.ratings.get('rating_fit_to_credit') or 0.0)\ + + (obj.ratings.get('rating_fit_to_study_book') or 0.0)\ + + (obj.ratings.get('rating_beneficial') or 0.0)\ + + (obj.ratings.get('rating_recommended') or 0.0) + + return rating_total / 5.0 + class Meta: model = Course fields = [field.name for field in model._meta.fields] - fields.extend(['review_count','code_desc', 'tags']) + fields.extend(['review_count','code_desc', 'tags', 'rating_understandable', 'rating_fit_to_credit', + 'rating_fit_to_study_book', 'rating_beneficial', 'rating_recommended', 'rating_average']) class ReviewSerializer(serializers.ModelSerializer): likes_count = serializers.SerializerMethodField('get_likes_count') @@ -72,12 +126,13 @@ class ReviewSerializer(serializers.ModelSerializer): course_code_desc = serializers.SerializerMethodField('get_course_code_desc') course_name = serializers.SerializerMethodField('get_course_name') course_review_count = serializers.SerializerMethodField('get_course_review_count') + rating_average = serializers.SerializerMethodField('get_rating_average') class Meta: model = Review fields = [field.name for field in model._meta.fields] fields.extend(['author', 'author_generation', 'author_study_program', 'course_code', 'course_code_desc', 'course_name', - 'course_review_count', 'tags', 'likes_count', 'is_liked']) + 'course_review_count', 'tags', 'likes_count', 'is_liked', 'rating_average']) def get_author(self, obj): return obj.user.username @@ -138,6 +193,9 @@ def get_course_name(self, obj): def get_course_review_count(self, obj): return obj.course.reviews.filter(is_active=True).filter(hate_speech_status='APPROVED').count() + def get_rating_average(self, obj): + return ((obj.rating_understandable or 0) + (obj.rating_fit_to_credit or 0) + (obj.rating_fit_to_study_book or 0) + (obj.rating_beneficial or 0) + (obj.rating_recommended or 0)) / 5 + class ReviewDSSerializer(serializers.ModelSerializer): class Meta: model = Review @@ -191,4 +249,32 @@ def get_term(self, obj): def get_generation(self, obj): generation = 2000 + int(obj.npm[:2]) - return str(generation) \ No newline at end of file + return str(generation) + +class CalculatorSerializer(serializers.ModelSerializer): + user = serializers.SerializerMethodField('get_user') + course_id = serializers.SerializerMethodField('get_course_id') + course_name = serializers.SerializerMethodField('get_course_name') + + class Meta: + model = Calculator + fields = ('id', 'user', 'course_id', 'course_name', 'total_score', "total_percentage") + + def get_user(self, obj): + return obj.user.username + + def get_course_id(self, obj): + return obj.course.id + + def get_course_name(self, obj): + return obj.course.name + +class ScoreComponentSerializer(serializers.ModelSerializer): + calculator_id = serializers.SerializerMethodField('get_calculator_id') + + class Meta: + model = ScoreComponent + fields = ('id', 'calculator_id', 'name', 'weight', 'score') + + def get_calculator_id(self, obj): + return obj.calculator.id \ No newline at end of file diff --git a/main/urls.py b/main/urls.py index d5e0c61..cca2bd5 100644 --- a/main/urls.py +++ b/main/urls.py @@ -1,7 +1,8 @@ from django import conf from rest_framework import routers from django.urls import path, include -from .views import like, tag, bookmark, account +from .views_calculator import calculator, score_component +from .views import like, tag, bookmark, account, leaderboard from .views_review import ds_review, review from .views_course import CourseViewSet @@ -15,7 +16,9 @@ path("ds-reviews", ds_review, name="ds-reviews"), path("likes", like, name="likes"), path("tags", tag, name="tags"), - path("account", account, name="account") - # path("update_courses", update_courses, name="update_courses"), # temporary + path("account", account, name="account"), + path("leaderboard", leaderboard, name="leaderboard"), + path("calculator", calculator, name="calculator"), + path("score-component", score_component, name="score-component") ] + router.urls diff --git a/main/views.py b/main/views.py index 7a260f4..4291794 100644 --- a/main/views.py +++ b/main/views.py @@ -14,6 +14,7 @@ from .serializers import AccountSerializer, BookmarkSerializer from django.http.response import HttpResponseRedirect from courseUpdater import courseApi +from leaderboard_updater import updater as leaderboard_updater from django.shortcuts import redirect import logging @@ -116,7 +117,7 @@ def like(request): if review is None: return response(error="Review not found", status=status.HTTP_404_NOT_FOUND) - review_likes = ReviewLike.objects.filter(review=review).first() + review_likes = ReviewLike.objects.filter(user=user, review=review).first() if review_likes is None: review_likes = ReviewLike.objects.create(user=user, review=review) @@ -215,4 +216,28 @@ def account(request): Remember that this endpoint require Token Authorization. """ user = Profile.objects.get(username=str(request.user)) - return response(data=AccountSerializer(user, many=False).data) \ No newline at end of file + return response(data=AccountSerializer(user, many=False).data) + +@api_view(['GET']) +def leaderboard(request): + """ + Return user leaderboard + Remember that this endpoint require Token Authorization. + """ + top_users = Profile.objects.filter(likes_count__gt=0).order_by('-likes_count')[:20] + return response(data=AccountSerializer(top_users, many=True).data) + +@api_view(['GET']) +@permission_classes((permissions.AllowAny,)) +def update_leaderboard(request): + """ + Update leaderboard data + """ + start = datetime.now() + leaderboard_updater.update_leaderboard() + finish = datetime.now() + + latency = (finish-start).seconds + time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + message = 'Leaderboard updated succeed on %s, elapsed time: %s seconds' % (time, latency) + return Response({'message': message}) \ No newline at end of file diff --git a/main/views_calculator.py b/main/views_calculator.py new file mode 100644 index 0000000..edfb847 --- /dev/null +++ b/main/views_calculator.py @@ -0,0 +1,158 @@ +import logging + +from rest_framework.decorators import api_view +from rest_framework import status +from .serializers import CalculatorSerializer, ScoreComponentSerializer + +from .utils import response, validate_body, validate_params +from .models import Calculator, Profile, Course, ScoreComponent +from django.db.models import F + +logger = logging.getLogger(__name__) + + +@api_view(['GET', 'POST', 'DELETE']) +def calculator(request): + + user = Profile.objects.get(username=str(request.user)) + if request.method == 'GET': + calculators = Calculator.objects.filter(user = user) + return response(data=CalculatorSerializer(calculators, many=True).data) + + if request.method == 'POST': + is_valid = validate_body(request, ['course_code']) + + if is_valid != None: + return is_valid + + course_code = request.data.get('course_code') + course = Course.objects.filter(code=course_code).first() + + if course is None: + return response(error="Course not found", status=status.HTTP_404_NOT_FOUND) + + calculator = Calculator.objects.filter(user=user, course=course).first() + + if calculator is None: + calculator = Calculator.objects.create(user=user, course=course) + return response(data=CalculatorSerializer(calculator).data, status=status.HTTP_201_CREATED) + + return response(error="Course calculator already exists", status=status.HTTP_409_CONFLICT) + + if request.method == 'DELETE': + is_valid = validate_params(request, ['id']) + + if is_valid != None: + return is_valid + + calculator_id = request.query_params.get('id') + calculator = Calculator.objects.filter(id=calculator_id).first() + + if calculator is None: + return response(error="Calculator not found", status=status.HTTP_404_NOT_FOUND) + + calculator.delete() + return response(status=status.HTTP_200_OK) + +@api_view(['GET', 'POST', 'PUT', 'DELETE']) +def score_component(request): + + if request.method == 'GET': + + is_valid = validate_params(request, ['calculator_id']) + + if is_valid != None: + is_valid + + calculator_id = request.query_params.get('calculator_id') + calculator = Calculator.objects.filter(id=calculator_id).first() + + if calculator is None: + return response(error="Calculator not found", status=status.HTTP_404_NOT_FOUND) + + score_components = ScoreComponent.objects.filter(calculator=calculator) + return response(data=ScoreComponentSerializer(score_components, many=True).data) + + if request.method == 'POST': + + is_valid = validate_body(request, ['calculator_id', 'name', 'weight', 'score']) + + if is_valid != None: + is_valid + + calculator_id = request.data.get('calculator_id') + name = request.data.get('name') + weight = request.data.get('weight') + score = request.data.get('score') + + calculator = Calculator.objects.filter(id=calculator_id).first() + + if calculator is None: + return response(error="Calculator not found", status=status.HTTP_404_NOT_FOUND) + + score_component = ScoreComponent.objects.create(calculator=calculator, name=name, weight=weight, score=score) + + calculator.total_score += (score * weight / 100) + calculator.total_percentage += weight + + calculator.save() + + return response(data=ScoreComponentSerializer(score_component).data, status=status.HTTP_201_CREATED) + + if request.method == 'PUT': + is_valid = validate_body(request, ['id', 'name', 'weight', 'score']) + + if is_valid != None: + is_valid + + component_id = request.data.get('id') + name = request.data.get('name') + weight = request.data.get('weight') + score = request.data.get('score') + + score_component = ScoreComponent.objects.filter(id=component_id).first() + + if score_component is None: + return response(error="Score component not found", status=status.HTTP_404_NOT_FOUND) + + calculator = Calculator.objects.filter(id=score_component.calculator.id).first() + + calculator.total_score -= (score_component.score * score_component.weight / 100) + calculator.total_percentage -= score_component.weight + + score_component.name = name + score_component.weight = weight + score_component.score = score + + score_component.save() + + calculator.total_score += (score_component.score * score_component.weight / 100) + calculator.total_percentage += score_component.weight + + calculator.save() + return response(data=ScoreComponentSerializer(score_component).data, status=status.HTTP_201_CREATED) + + if request.method == 'DELETE': + is_valid = validate_params(request, ['id']) + + if is_valid != None: + return is_valid + + component_id = request.query_params.get('id') + + score_component = ScoreComponent.objects.filter(id=component_id).first() + + if score_component is None: + return response(error="Score component not found", status=status.HTTP_404_NOT_FOUND) + + calculator = Calculator.objects.filter(id=score_component.calculator.id).first() + + calculator.total_score -= (score_component.score * score_component.weight / 100) + calculator.total_percentage -= score_component.weight + + score_component.delete() + calculator.save() + + return response(status=status.HTTP_200_OK) + + \ No newline at end of file diff --git a/main/views_review.py b/main/views_review.py index b7e4a06..da0a272 100644 --- a/main/views_review.py +++ b/main/views_review.py @@ -50,8 +50,27 @@ def review(request): return response_paged(data=[]) + if request.method == 'POST': - is_valid = validate_body(request, ['course_code', 'academic_year', 'semester', 'content', 'is_anonym', 'tags']) + required_fields = [ + 'course_code', + 'academic_year', + 'semester', + 'content', + 'is_anonym', + 'tags' + ] + + if request.version == 'v2': + required_fields.extend([ + 'rating_understandable', + 'rating_fit_to_credit', + 'rating_fit_to_study_book', + 'rating_beneficial', + 'rating_recommended' + ]) + + is_valid = validate_body(request, required_fields) if is_valid != None: return is_valid @@ -64,18 +83,31 @@ def review(request): semester = request.data.get("semester") content = request.data.get("content") is_anonym = request.data.get("is_anonym") + rating_understandable = request.data.get("rating_understandable") or 0 + rating_fit_to_credit = request.data.get("rating_fit_to_credit") or 0 + rating_fit_to_study_book = request.data.get("rating_fit_to_study_book") or 0 + rating_beneficial = request.data.get("rating_beneficial") or 0 + rating_recommended = request.data.get("rating_recommended") or 0 try: with transaction.atomic(): + # Create new review review = Review.objects.create( user=user, course=course, academic_year = academic_year, semester = semester, content = content, - is_anonym = is_anonym + is_anonym = is_anonym, + rating_understandable = rating_understandable, + rating_fit_to_credit = rating_fit_to_credit, + rating_fit_to_study_book = rating_fit_to_study_book, + rating_beneficial = rating_beneficial, + rating_recommended = rating_recommended ) + create_review_tag(review, tags) + except Exception as e: logger.error("Failed to save review, request data {}".format(request.data)) return response(error="Failed to save review, error message: {}".format(e), status=status.HTTP_404_NOT_FOUND) diff --git a/pull_request_template.md b/pull_request_template.md new file mode 100644 index 0000000..a8cab41 --- /dev/null +++ b/pull_request_template.md @@ -0,0 +1,31 @@ +## Thanks for your support to the project. Please check below mentioned points - + +### Detailed description of changes + +... + +### Recommendations on how to test the changes + +... + +### Checklist for developer + +- [ ] The code builds clean without any errors or warnings +- [ ] The code does not contain commented out code +- [ ] The code does not log anything to console +- [ ] I have added unit test(s) to cover new code and successfully executed ran it +- [ ] I have thoroughly tested the new code and any adjacent features it may affect + +### Link to related issue(s) + +... + +### Screenshots or videos (before and after if appropriate) + +#### Before + +... + +#### After + +... diff --git a/requirements.txt b/requirements.txt index 545d9be..a77bd1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ django-sso-ui==1.0.0 djangorestframework==3.12.2 gunicorn==20.0.4 idna==2.10 -lxml==4.6.1 +lxml==4.9.1 psycopg2==2.9.1 pycodestyle==2.6.0 python-cas==1.5.0 diff --git a/sample.env b/sample.env new file mode 100644 index 0000000..e4b9be8 --- /dev/null +++ b/sample.env @@ -0,0 +1,10 @@ +DJANGO_SETTINGS_MODULE=UlasKelas.settings +SECRET_KEY=xxx +SENTRY_DSN=https://xxx.ingest.sentry.io/xxx +DEBUG= +PYTHONPATH=./env + +POSTGRES_HOST= +POSTGRES_DB= +POSTGRES_USER= +POSTGRES_PASSWORD= \ No newline at end of file