Commit de3c1897 authored by Florent Chehab's avatar Florent Chehab

feat(previous departure feedbacks): presentation & edit done 馃帀 | Tones of tweaks

Previous exchange feedbacks:
* renamed some fields
* added would recommend field in course feedback
* uniformized grading scheme
* Added viewsets/serializers in the back (+permissions & performance concerns)
* Support edit in the front

Tweaks:
* directly return in the `obj_info` if the model is versioned or not
* Added support for required get parameters
* enum for model moderation level
* More versatile metric feedback component
* Fixed how the editor (frontend component) was telling if something has been moderated
* Added a CURRENT_USER variable instead of using the one from html directly
* Diminished website font-size
* Better proptypes / defaultProps in fields
* Added optionnal comment text on fields
* Fixed the number field
* Added Helper classes to centralize the manipulation of redux store data
* Teaked pseudo/username returned by the api

Closes #29 #32
parent 2a5cc832
Pipeline #42347 passed with stages
in 4 minutes and 34 seconds
......@@ -80,9 +80,9 @@ class LoadUniversityEx(LoadGeneric):
self.add_info_and_save(univ_tag_1, self.admin)
exchange1 = Exchange.objects.create(
utc_univ_id=EPFL,
university=EPFL,
utc_departure_id=1,
user=self.admin,
student=self.admin,
year=2019,
semester="a",
duration=1,
......@@ -95,7 +95,7 @@ class LoadUniversityEx(LoadGeneric):
utc_allow_login=False,
)
ExchangeFeedback.objects.create(
ef = ExchangeFeedback.objects.create(
university=EPFL,
exchange=exchange1,
general_comment="Very good",
......@@ -103,11 +103,12 @@ class LoadUniversityEx(LoadGeneric):
foreign_student_welcome=5,
cultural_interest=5,
)
self.add_info_and_save(ef, self.admin)
exchange2 = Exchange.objects.create(
utc_univ_id=EPFL,
university=EPFL,
utc_departure_id=2,
user=self.admin,
student=self.admin,
year=2018,
semester="a",
duration=1,
......@@ -120,7 +121,7 @@ class LoadUniversityEx(LoadGeneric):
utc_allow_login=True,
)
ExchangeFeedback.objects.create(
ef = ExchangeFeedback.objects.create(
university=EPFL,
exchange=exchange2,
general_comment="Very good trop bien",
......@@ -128,6 +129,7 @@ class LoadUniversityEx(LoadGeneric):
foreign_student_welcome=3,
cultural_interest=4,
)
self.add_info_and_save(ef, self.admin)
course1 = Course.objects.create(
exchange=exchange1,
......@@ -140,7 +142,7 @@ class LoadUniversityEx(LoadGeneric):
category="TM",
profile="PSF",
tsh_profile="",
student_login="chehabfl",
student_login="admin",
)
Course.objects.create(
......@@ -154,16 +156,17 @@ class LoadUniversityEx(LoadGeneric):
category="TM",
profile="PSF",
tsh_profile="",
student_login="chehabfl",
student_login="admin",
)
CourseFeedback.objects.update_or_create(
cf = CourseFeedback.objects.update_or_create(
course=course1,
defaults=dict(
language=Language.objects.first(),
comment="Trop bien",
adequation=5,
working_dose=4,
language_following_ease=3,
following_ease=3,
),
)
)[0]
self.add_info_and_save(cf, self.admin)
# Generated by Django 2.1.7 on 2019-06-23 13:57
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("backend_app", "0011_auto_20190615_2212")]
operations = [
migrations.RenameField(
model_name="coursefeedback",
old_name="language_following_ease",
new_name="following_ease",
),
migrations.RenameField(
model_name="exchange", old_name="user", new_name="student"
),
migrations.RenameField(
model_name="exchange", old_name="utc_univ_id", new_name="university"
),
migrations.AddField(
model_name="coursefeedback",
name="would_recommend",
field=models.IntegerField(
default=0,
validators=[
django.core.validators.MinValueValidator(-5),
django.core.validators.MaxValueValidator(5),
],
),
),
migrations.AlterField(
model_name="exchangefeedback",
name="cultural_interest",
field=models.IntegerField(
validators=[
django.core.validators.MinValueValidator(-5),
django.core.validators.MaxValueValidator(5),
]
),
),
migrations.AlterField(
model_name="exchangefeedback",
name="foreign_student_welcome",
field=models.IntegerField(
validators=[
django.core.validators.MinValueValidator(-5),
django.core.validators.MaxValueValidator(5),
]
),
),
]
from django.conf import settings
from django.db import models
from rest_framework import serializers, viewsets
from rest_framework.response import Response
from backend_app.custom.mySerializerWithJSON import MySerializerWithJSON
from backend_app.permissions.default import DEFAULT_VIEWSET_PERMISSIONS
......@@ -37,6 +38,24 @@ class BaseModelSerializer(MySerializerWithJSON):
# this is useful when a model has a dedicated primary key
id = serializers.SerializerMethodField()
@classmethod
def get_user_related_field(cls, user):
"""
Generic function to make sure we return only the pseudo and the id".
:param user: user associated with object
:return: dict
"""
if user is None:
# In testing env or if data is not perfectly initialised
return dict(user_id=None, user_goes_by=None)
else:
user_goes_by = (
"{} {}".format(user.first_name, user.last_name)
if user.allow_sharing_personal_info
else user.pseudo
)
return dict(user_id=user.pk, user_goes_by=user_goes_by)
def get_obj_info(self, obj) -> dict:
"""
Serializer for the `obj_info` *dynamic* field.
......@@ -48,7 +67,7 @@ class BaseModelSerializer(MySerializerWithJSON):
This methods is overrided in EssentialModuleSerializer for
a smarter behavior.
"""
return {"user_can_edit": False, "user_can_moderate": False}
return {"user_can_edit": False, "user_can_moderate": False, "versioned": False}
def get_id(self, obj: BaseModel):
"""
......@@ -90,3 +109,17 @@ class BaseModelViewSet(viewsets.ModelViewSet):
return [
(DEFAULT_VIEWSET_PERMISSIONS & p)() for p in self.permission_classes
]
# Attribute that lists the fields that are expected to be preset in request.GET
required_filterset_fields = tuple()
def list(self, request, *args, **kwargs):
"""
Overrides the default list to enable required filterset_fields
"""
for field_name in self.required_filterset_fields:
if field_name not in request.GET.keys():
return Response(
"Missing get parameter `{}`".format(field_name), status=422
)
return super().list(request, args, kwargs)
......@@ -46,7 +46,7 @@ def validate_obj_model_lv(value):
class EssentialModule(BaseModel):
"""
All models in the app deppend of this one.
All models in the app depend of this one.
It contains the required attributes for managing optional data moderation.
All the logic behind moderation is done in EssentialModuleSerializer
......@@ -136,18 +136,6 @@ class EssentialModuleSerializer(BaseModelSerializer):
# Add a content_type_id field to be able to find versions
content_type_id = serializers.SerializerMethodField()
def get_user_related_field(self, user):
"""
Generic function to make sure we return only the pseudo and the id".
:param user: user associated with object
:return: dict
"""
if user is None:
# In testing env or if data is not perfectly initialised
return dict(user_id=None, user_pseudo=None)
else:
return dict(user_id=user.pk, user_pseudo=user.pseudo)
def get_updated_by(self, obj):
return self.get_user_related_field(obj.updated_by)
......@@ -164,6 +152,7 @@ class EssentialModuleSerializer(BaseModelSerializer):
"""
Serializer for the `obj_info` *dynamic* field. Redefined.
"""
obj_info = super().get_obj_info(obj)
try:
user_can_edit = self.context["user_can_edit"]
except KeyError:
......@@ -172,12 +161,11 @@ class EssentialModuleSerializer(BaseModelSerializer):
# Anyway, those Viewsets should be readonly, so we can return false.
user_can_edit = False
return {
"user_can_edit": user_can_edit,
"user_can_moderate": not is_moderation_required(
self.Meta.model, obj, self.get_user_from_request()
),
}
obj_info["user_can_edit"] = user_can_edit
obj_info["user_can_moderate"] = not is_moderation_required(
self.Meta.model, obj, self.get_user_from_request()
)
return obj_info
class Meta:
model = EssentialModule
......
......@@ -52,6 +52,14 @@ class VersionedEssentialModuleSerializer(EssentialModuleSerializer):
new_revision_saved.send(sender=self.__class__, obj=self.instance)
return res
def get_obj_info(self, obj) -> dict:
"""
Serializer for the `obj_info` *dynamic* field. Redefined.
"""
obj_info = super().get_obj_info(obj)
obj_info["versioned"] = True
return obj_info
class Meta:
model = VersionedEssentialModule
fields = EssentialModuleSerializer.Meta.fields + ("nb_version",)
......
......@@ -4,9 +4,12 @@ from django.db import models
from backend_app.models.abstract.essentialModule import EssentialModule
from backend_app.models.course import Course
from backend_app.models.language import Language
from backend_app.permissions.moderation import ModerationLevels
class CourseFeedback(EssentialModule):
moderation_level = ModerationLevels.DEPENDING_ON_SITE_SETTINGS
course = models.OneToOneField(
Course, on_delete=models.CASCADE, default=0, related_name="course_feedback"
)
......@@ -17,9 +20,12 @@ class CourseFeedback(EssentialModule):
adequation = models.IntegerField(
default=0, validators=[MinValueValidator(-5), MaxValueValidator(5)]
)
would_recommend = models.IntegerField(
default=0, validators=[MinValueValidator(-5), MaxValueValidator(5)]
)
working_dose = models.IntegerField(
default=0, validators=[MinValueValidator(-5), MaxValueValidator(5)]
)
language_following_ease = models.IntegerField(
following_ease = models.IntegerField(
default=0, validators=[MinValueValidator(-5), MaxValueValidator(5)]
)
......@@ -8,9 +8,9 @@ from base_app.models import User
class Exchange(BaseModel):
# This model should be filled with data from the ENT
utc_univ_id = models.ForeignKey(University, on_delete=models.PROTECT)
university = models.ForeignKey(University, on_delete=models.PROTECT)
utc_departure_id = models.IntegerField()
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
student = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
year = models.PositiveIntegerField(default=2018)
semester = models.CharField(max_length=5, choices=SEMESTER_OPTIONS, default="a")
......
from django.db import models
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from rest_framework.permissions import BasePermission
from backend_app.models.abstract.essentialModule import (
EssentialModule,
......@@ -7,11 +8,14 @@ from backend_app.models.abstract.essentialModule import (
EssentialModuleViewSet,
)
from backend_app.models.exchange import Exchange
from backend_app.serializers import ExchangeSerializer
from backend_app.models.university import University
from backend_app.permissions.app_permissions import ReadOnly, IsStaff, NoDelete, NoPost
from backend_app.permissions.moderation import ModerationLevels
from backend_app.serializers import ExchangeSerializer
class ExchangeFeedback(EssentialModule):
moderation_level = ModerationLevels.DEPENDING_ON_SITE_SETTINGS
university = models.ForeignKey(University, on_delete=models.PROTECT, default=0)
exchange = models.OneToOneField(
Exchange,
......@@ -25,10 +29,12 @@ class ExchangeFeedback(EssentialModule):
academical_level_appreciation = models.IntegerField(
validators=[MinValueValidator(-5), MaxValueValidator(5)]
)
foreign_student_welcome = models.PositiveIntegerField(
validators=[MaxValueValidator(10)]
foreign_student_welcome = models.IntegerField(
validators=[MinValueValidator(-5), MaxValueValidator(5)]
)
cultural_interest = models.IntegerField(
validators=[MinValueValidator(-5), MaxValueValidator(5)]
)
cultural_interest = models.PositiveIntegerField(validators=[MaxValueValidator(10)])
class ExchangeFeedbackSerializer(EssentialModuleSerializer):
......@@ -36,15 +42,41 @@ class ExchangeFeedbackSerializer(EssentialModuleSerializer):
class Meta:
model = ExchangeFeedback
fields = "__all__"
fields = EssentialModuleSerializer.Meta.fields + (
"university",
"exchange",
"general_comment",
"academical_level_appreciation",
"foreign_student_welcome",
"cultural_interest",
)
read_only_fields = ("university", "exchange")
class ExchangePermission(BasePermission):
"""
Permission that checks that the requester is the student concern by the exchange.
"""
def has_object_permission(self, request, view, obj):
return request.user.pk == obj.exchange.student.pk
class ExchangeFeedbackViewSet(EssentialModuleViewSet):
queryset = ExchangeFeedback.objects.all().prefetch_related(
"exchange",
"exchange__exchange_courses",
"exchange__exchange_courses__course_feedback",
) # pylint: disable=E1101
permission_classes = (
NoDelete & NoPost & (ReadOnly | IsStaff | ExchangePermission),
)
queryset = (
ExchangeFeedback.objects.all()
.select_related("exchange", "updated_by", "moderated_by", "exchange__student")
.prefetch_related(
"exchange__exchange_courses",
"exchange__exchange_courses__course_feedback",
"exchange__exchange_courses__course_feedback__updated_by",
"exchange__exchange_courses__course_feedback__moderated_by",
)
)
serializer_class = ExchangeFeedbackSerializer
end_point_route = "exchangeFeedbacks"
filterset_fields = ("university",)
required_filterset_fields = ("university",)
......@@ -5,6 +5,7 @@ from backend_app.models.abstract.essentialModule import (
EssentialModuleSerializer,
EssentialModuleViewSet,
)
from backend_app.permissions.moderation import ModerationLevels
class ForTestingModeration(EssentialModule):
......@@ -12,7 +13,7 @@ class ForTestingModeration(EssentialModule):
Simple model for testing purposes
"""
moderation_level = 1
moderation_level = ModerationLevels.DEPENDING_ON_SITE_SETTINGS
aaa = models.CharField(max_length=100)
......
from django.db import models
import reversion
from django.db import models
from backend_app.models.abstract.versionedEssentialModule import (
VersionedEssentialModule,
VersionedEssentialModuleSerializer,
VersionedEssentialModuleViewSet,
)
from backend_app.permissions.moderation import ModerationLevels
@reversion.register()
......@@ -15,7 +15,7 @@ class ForTestingVersioning(VersionedEssentialModule):
Simple model for testing purposes (versioning)
"""
moderation_level = 1
moderation_level = ModerationLevels.DEPENDING_ON_SITE_SETTINGS
bbb = models.CharField(max_length=100)
......
......@@ -10,6 +10,7 @@ from backend_app.models.abstract.base import (
)
from backend_app.models.university import University
from backend_app.permissions.app_permissions import IsOwner, ReadOnly, IsPublic
from backend_app.permissions.moderation import ModerationLevels
from backend_app.validation.validators import (
RecommendationListJsonContentValidator,
RecommendationListUnivValidator,
......@@ -21,7 +22,7 @@ univ_content_validator = RecommendationListUnivValidator()
class RecommendationList(BaseModel):
moderation_level = 0
moderation_level = ModerationLevels.NO_MODERATION
last_update = models.DateTimeField(auto_now=True)
title = models.CharField(max_length=100)
......
......@@ -9,13 +9,14 @@ from backend_app.models.abstract.base import (
BaseModelViewSet,
)
from backend_app.permissions.app_permissions import IsOwner, IsStaff, ReadOnly
from backend_app.permissions.moderation import ModerationLevels
from backend_app.utils import get_user_level, get_default_theme_settings
from backend_app.validation.validators import ThemeValidator
from base_app.models import User
class UserData(BaseModel):
moderation_level = 0
moderation_level = ModerationLevels.NO_MODERATION
owner = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
theme = JSONField(default=get_default_theme_settings, validators=[ThemeValidator()])
......
......@@ -5,12 +5,20 @@
#
from enum import IntEnum
from django.conf import settings
from backend_app.settings.defaults import OBJ_MODERATION_PERMISSIONS
from backend_app.utils import get_user_level
class ModerationLevels(IntEnum):
NO_MODERATION = 0
DEPENDING_ON_SITE_SETTINGS = 1
ENFORCED = 2
def is_moderation_required(model, obj_in_db, user, user_level=None) -> bool:
"""
Function to tell if moderation is required for obj instance given a user.
......@@ -25,24 +33,28 @@ def is_moderation_required(model, obj_in_db, user, user_level=None) -> bool:
# First we retrieve the model moderation level
model_moderation_level = model.moderation_level
if model_moderation_level == 0:
if model_moderation_level == ModerationLevels.NO_MODERATION:
return False
else:
if user_level is None:
user_level = get_user_level(user)
# At this point we have to check the obj_moderation_level
if obj_in_db is not None:
obj_moderation_level = obj_in_db.obj_moderation_level
if user_level < obj_moderation_level:
return True
if model_moderation_level == 1:
if settings.MODERATION_ACTIVATED:
return not user_level >= OBJ_MODERATION_PERMISSIONS["moderator"]
else:
return False
elif model_moderation_level == 2:
if user_level is None:
user_level = get_user_level(user)
# At this point we have to check the obj_moderation_level
if obj_in_db is not None:
obj_moderation_level = obj_in_db.obj_moderation_level
if user_level < obj_moderation_level:
return True
if model_moderation_level == ModerationLevels.DEPENDING_ON_SITE_SETTINGS:
if settings.MODERATION_ACTIVATED:
return not user_level >= OBJ_MODERATION_PERMISSIONS["moderator"]
else:
raise Exception("No other moderation level should be defined...")
return False
elif model_moderation_level == ModerationLevels.ENFORCED:
return not user_level >= OBJ_MODERATION_PERMISSIONS["moderator"]
else:
raise Exception(
"No other moderation level should be defined...: {}".format(
model_moderation_level
)
)
from rest_framework import serializers
from backend_app.models.abstract.base import BaseModelSerializer
from backend_app.models.abstract.essentialModule import EssentialModuleSerializer
from backend_app.models.course import Course
......@@ -6,48 +8,74 @@ from backend_app.models.exchange import Exchange
class CourseFeedbackSerializer(EssentialModuleSerializer):
course_code = serializers.SerializerMethodField() # needed for the front
def get_course_code(self, obj):
return obj.course.code
class Meta:
model = CourseFeedback
fields = EssentialModuleSerializer.Meta.fields + (
"course_code",
"language",
"comment",
"adequation",
"would_recommend",
"working_dose",
"language_following_ease",
"following_ease",
)
class CourseSerializer(BaseModelSerializer):
course_feedback = CourseFeedbackSerializer(many=False, read_only=True)
COURSE_FIELDS = BaseModelSerializer.Meta.fields + (
"course_feedback",
"code",
"title",
"link",
"nb_credit",
"category",
"profile",
"tsh_profile",
)
class CourseSerializerSimple(BaseModelSerializer):
class Meta:
model = Course
fields = BaseModelSerializer.Meta.fields + (
"course_feedback",
"code",
"title",
"link",
"nb_credit",
"category",
"profile",
"tsh_profile",
)
read_only_fields = ("course_feedback",)
fields = COURSE_FIELDS
read_only_fields = COURSE_FIELDS
class ExchangeSerializer(BaseModelSerializer):
exchange_courses = CourseSerializer(many=True, read_only=True)
class CourseSerializer(CourseSerializerSimple):
course_feedback = CourseFeedbackSerializer(many=False, read_only=True)
EXCHANGE_FIELDS = BaseModelSerializer.Meta.fields + (
"year",
"semester",
"duration",
"dual_degree",
"master_obtained",
"student_major",
"student_minor",
"student_option",
"exchange_courses",
"university",
"student",
)
class ExchangeSerializerSimple(BaseModelSerializer):
exchange_courses = CourseSerializerSimple(many=True, read_only=True)
student = serializers.SerializerMethodField()
def get_student(self, obj):
return self.get_user_related_field(obj.student)
class Meta:
model = Exchange
fields = BaseModelSerializer.Meta.fields + (
"year",
"semester",
"duration",
"dual_degree",
"master_obtained",
"student_major",
"student_minor",
"student_option",
"exchange_courses",
)
fields = EXCHANGE_FIELDS
read_only_fields = EXCHANGE_FIELDS
class ExchangeSerializer(ExchangeSerializerSimple):
exchange_courses = CourseSerializer(many=True, read_only=True)
import logging
from django.conf import settings
from rest_framework.permissions import BasePermission
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet
from backend_app.checks import check_viewsets
from backend_app.models.abstract.base import BaseModelViewSet
from backend_app.models.abstract.essentialModule import EssentialModuleViewSet
from backend_app.models.campus import CampusViewSet, MainCampusViewSet
from backend_app.models.campusTaggedItem import CampusTaggedItemViewSet
......@@ -14,9 +16,11 @@ from backend_app.models.country import CountryViewSet
from backend_app.models.countryDri import CountryDriViewSet
from backend_app.models.countryScholarship import CountryScholarshipViewSet
from backend_app.models.countryTaggedItem import CountryTaggedItemViewSet
from backend_app.models.course import Course
from backend_app.models.courseFeedback import CourseFeedback
from backend_app.models.currency import CurrencyViewSet
from backend_app.models.department import DepartmentViewSet
from backend_app.models.exchange import Exchange
from backend_app.models.exchangeFeedback import ExchangeFeedbackViewSet
from backend_app.models.file_picture import FileViewSet, PictureViewSet
from backend_app.models.for_testing.moderation import ForTestingModerationViewSet
......@@ -41,16 +45,66 @@ from backend_app.models.universitySemestersDates import UniversitySemestersDates
from backend_app.models.universityTaggedItem import UniversityTaggedItemViewSet
from backend_app.models.userData import UserDataViewSet
from backend_app.models.version import VersionViewSet
from backend_app.permissions.app_permissions import ReadOnly, IsStaff
from backend_app.serializers import CourseFeedbackSerializer
from backend_app.permissions.app_permissions import ReadOnly, IsStaff, NoDelete, NoPost
from backend_app.serializers import (
CourseFeedbackSerializer,
ExchangeSerializerSimple,
CourseSerializer,
)
from backend_app.settings.defaults import OBJ_MODERATION_PERMISSIONS
from base_app.models import UserViewset, User
class CourseViewSet(BaseModelViewSet):
queryset = Course.objects.all().select_related(
"exchange",
"exchange__feedbacks",
"exchange__feedbacks__moderated_by",