Commit de3c1897 authored by Florent Chehab's avatar Florent Chehab
Browse files

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): ...@@ -80,9 +80,9 @@ class LoadUniversityEx(LoadGeneric):
self.add_info_and_save(univ_tag_1, self.admin) self.add_info_and_save(univ_tag_1, self.admin)
exchange1 = Exchange.objects.create( exchange1 = Exchange.objects.create(
utc_univ_id=EPFL, university=EPFL,
utc_departure_id=1, utc_departure_id=1,
user=self.admin, student=self.admin,
year=2019, year=2019,
semester="a", semester="a",
duration=1, duration=1,
...@@ -95,7 +95,7 @@ class LoadUniversityEx(LoadGeneric): ...@@ -95,7 +95,7 @@ class LoadUniversityEx(LoadGeneric):
utc_allow_login=False, utc_allow_login=False,
) )
ExchangeFeedback.objects.create( ef = ExchangeFeedback.objects.create(
university=EPFL, university=EPFL,
exchange=exchange1, exchange=exchange1,
general_comment="Very good", general_comment="Very good",
...@@ -103,11 +103,12 @@ class LoadUniversityEx(LoadGeneric): ...@@ -103,11 +103,12 @@ class LoadUniversityEx(LoadGeneric):
foreign_student_welcome=5, foreign_student_welcome=5,
cultural_interest=5, cultural_interest=5,
) )
self.add_info_and_save(ef, self.admin)
exchange2 = Exchange.objects.create( exchange2 = Exchange.objects.create(
utc_univ_id=EPFL, university=EPFL,
utc_departure_id=2, utc_departure_id=2,
user=self.admin, student=self.admin,
year=2018, year=2018,
semester="a", semester="a",
duration=1, duration=1,
...@@ -120,7 +121,7 @@ class LoadUniversityEx(LoadGeneric): ...@@ -120,7 +121,7 @@ class LoadUniversityEx(LoadGeneric):
utc_allow_login=True, utc_allow_login=True,
) )
ExchangeFeedback.objects.create( ef = ExchangeFeedback.objects.create(
university=EPFL, university=EPFL,
exchange=exchange2, exchange=exchange2,
general_comment="Very good trop bien", general_comment="Very good trop bien",
...@@ -128,6 +129,7 @@ class LoadUniversityEx(LoadGeneric): ...@@ -128,6 +129,7 @@ class LoadUniversityEx(LoadGeneric):
foreign_student_welcome=3, foreign_student_welcome=3,
cultural_interest=4, cultural_interest=4,
) )
self.add_info_and_save(ef, self.admin)
course1 = Course.objects.create( course1 = Course.objects.create(
exchange=exchange1, exchange=exchange1,
...@@ -140,7 +142,7 @@ class LoadUniversityEx(LoadGeneric): ...@@ -140,7 +142,7 @@ class LoadUniversityEx(LoadGeneric):
category="TM", category="TM",
profile="PSF", profile="PSF",
tsh_profile="", tsh_profile="",
student_login="chehabfl", student_login="admin",
) )
Course.objects.create( Course.objects.create(
...@@ -154,16 +156,17 @@ class LoadUniversityEx(LoadGeneric): ...@@ -154,16 +156,17 @@ class LoadUniversityEx(LoadGeneric):
category="TM", category="TM",
profile="PSF", profile="PSF",
tsh_profile="", tsh_profile="",
student_login="chehabfl", student_login="admin",
) )
CourseFeedback.objects.update_or_create( cf = CourseFeedback.objects.update_or_create(
course=course1, course=course1,
defaults=dict( defaults=dict(
language=Language.objects.first(), language=Language.objects.first(),
comment="Trop bien", comment="Trop bien",
adequation=5, adequation=5,
working_dose=4, 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.conf import settings
from django.db import models from django.db import models
from rest_framework import serializers, viewsets from rest_framework import serializers, viewsets
from rest_framework.response import Response
from backend_app.custom.mySerializerWithJSON import MySerializerWithJSON from backend_app.custom.mySerializerWithJSON import MySerializerWithJSON
from backend_app.permissions.default import DEFAULT_VIEWSET_PERMISSIONS from backend_app.permissions.default import DEFAULT_VIEWSET_PERMISSIONS
...@@ -37,6 +38,24 @@ class BaseModelSerializer(MySerializerWithJSON): ...@@ -37,6 +38,24 @@ class BaseModelSerializer(MySerializerWithJSON):
# this is useful when a model has a dedicated primary key # this is useful when a model has a dedicated primary key
id = serializers.SerializerMethodField() 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: def get_obj_info(self, obj) -> dict:
""" """
Serializer for the `obj_info` *dynamic* field. Serializer for the `obj_info` *dynamic* field.
...@@ -48,7 +67,7 @@ class BaseModelSerializer(MySerializerWithJSON): ...@@ -48,7 +67,7 @@ class BaseModelSerializer(MySerializerWithJSON):
This methods is overrided in EssentialModuleSerializer for This methods is overrided in EssentialModuleSerializer for
a smarter behavior. 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): def get_id(self, obj: BaseModel):
""" """
...@@ -90,3 +109,17 @@ class BaseModelViewSet(viewsets.ModelViewSet): ...@@ -90,3 +109,17 @@ class BaseModelViewSet(viewsets.ModelViewSet):
return [ return [
(DEFAULT_VIEWSET_PERMISSIONS & p)() for p in self.permission_classes (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): ...@@ -46,7 +46,7 @@ def validate_obj_model_lv(value):
class EssentialModule(BaseModel): 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. It contains the required attributes for managing optional data moderation.
All the logic behind moderation is done in EssentialModuleSerializer All the logic behind moderation is done in EssentialModuleSerializer
...@@ -136,18 +136,6 @@ class EssentialModuleSerializer(BaseModelSerializer): ...@@ -136,18 +136,6 @@ class EssentialModuleSerializer(BaseModelSerializer):
# Add a content_type_id field to be able to find versions # Add a content_type_id field to be able to find versions
content_type_id = serializers.SerializerMethodField() 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): def get_updated_by(self, obj):
return self.get_user_related_field(obj.updated_by) return self.get_user_related_field(obj.updated_by)
...@@ -164,6 +152,7 @@ class EssentialModuleSerializer(BaseModelSerializer): ...@@ -164,6 +152,7 @@ class EssentialModuleSerializer(BaseModelSerializer):
""" """
Serializer for the `obj_info` *dynamic* field. Redefined. Serializer for the `obj_info` *dynamic* field. Redefined.
""" """
obj_info = super().get_obj_info(obj)
try: try:
user_can_edit = self.context["user_can_edit"] user_can_edit = self.context["user_can_edit"]
except KeyError: except KeyError:
...@@ -172,12 +161,11 @@ class EssentialModuleSerializer(BaseModelSerializer): ...@@ -172,12 +161,11 @@ class EssentialModuleSerializer(BaseModelSerializer):
# Anyway, those Viewsets should be readonly, so we can return false. # Anyway, those Viewsets should be readonly, so we can return false.
user_can_edit = False user_can_edit = False
return { obj_info["user_can_edit"] = user_can_edit
"user_can_edit": user_can_edit, obj_info["user_can_moderate"] = not is_moderation_required(
"user_can_moderate": not is_moderation_required( self.Meta.model, obj, self.get_user_from_request()
self.Meta.model, obj, self.get_user_from_request() )
), return obj_info
}
class Meta: class Meta:
model = EssentialModule model = EssentialModule
......
...@@ -52,6 +52,14 @@ class VersionedEssentialModuleSerializer(EssentialModuleSerializer): ...@@ -52,6 +52,14 @@ class VersionedEssentialModuleSerializer(EssentialModuleSerializer):
new_revision_saved.send(sender=self.__class__, obj=self.instance) new_revision_saved.send(sender=self.__class__, obj=self.instance)
return res 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: class Meta:
model = VersionedEssentialModule model = VersionedEssentialModule
fields = EssentialModuleSerializer.Meta.fields + ("nb_version",) fields = EssentialModuleSerializer.Meta.fields + ("nb_version",)
......
...@@ -4,9 +4,12 @@ from django.db import models ...@@ -4,9 +4,12 @@ from django.db import models
from backend_app.models.abstract.essentialModule import EssentialModule from backend_app.models.abstract.essentialModule import EssentialModule
from backend_app.models.course import Course from backend_app.models.course import Course
from backend_app.models.language import Language from backend_app.models.language import Language
from backend_app.permissions.moderation import ModerationLevels
class CourseFeedback(EssentialModule): class CourseFeedback(EssentialModule):
moderation_level = ModerationLevels.DEPENDING_ON_SITE_SETTINGS
course = models.OneToOneField( course = models.OneToOneField(
Course, on_delete=models.CASCADE, default=0, related_name="course_feedback" Course, on_delete=models.CASCADE, default=0, related_name="course_feedback"
) )
...@@ -17,9 +20,12 @@ class CourseFeedback(EssentialModule): ...@@ -17,9 +20,12 @@ class CourseFeedback(EssentialModule):
adequation = models.IntegerField( adequation = models.IntegerField(
default=0, validators=[MinValueValidator(-5), MaxValueValidator(5)] default=0, validators=[MinValueValidator(-5), MaxValueValidator(5)]
) )
would_recommend = models.IntegerField(
default=0, validators=[MinValueValidator(-5), MaxValueValidator(5)]
)
working_dose = models.IntegerField( working_dose = models.IntegerField(
default=0, validators=[MinValueValidator(-5), MaxValueValidator(5)] default=0, validators=[MinValueValidator(-5), MaxValueValidator(5)]
) )
language_following_ease = models.IntegerField( following_ease = models.IntegerField(
default=0, validators=[MinValueValidator(-5), MaxValueValidator(5)] default=0, validators=[MinValueValidator(-5), MaxValueValidator(5)]
) )
...@@ -8,9 +8,9 @@ from base_app.models import User ...@@ -8,9 +8,9 @@ from base_app.models import User
class Exchange(BaseModel): class Exchange(BaseModel):
# This model should be filled with data from the ENT # 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() 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) year = models.PositiveIntegerField(default=2018)
semester = models.CharField(max_length=5, choices=SEMESTER_OPTIONS, default="a") 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.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from rest_framework.permissions import BasePermission
from backend_app.models.abstract.essentialModule import ( from backend_app.models.abstract.essentialModule import (
EssentialModule, EssentialModule,
...@@ -7,11 +8,14 @@ from backend_app.models.abstract.essentialModule import ( ...@@ -7,11 +8,14 @@ from backend_app.models.abstract.essentialModule import (
EssentialModuleViewSet, EssentialModuleViewSet,
) )
from backend_app.models.exchange import Exchange from backend_app.models.exchange import Exchange
from backend_app.serializers import ExchangeSerializer
from backend_app.models.university import University 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): class ExchangeFeedback(EssentialModule):
moderation_level = ModerationLevels.DEPENDING_ON_SITE_SETTINGS
university = models.ForeignKey(University, on_delete=models.PROTECT, default=0) university = models.ForeignKey(University, on_delete=models.PROTECT, default=0)
exchange = models.OneToOneField( exchange = models.OneToOneField(
Exchange, Exchange,
...@@ -25,10 +29,12 @@ class ExchangeFeedback(EssentialModule): ...@@ -25,10 +29,12 @@ class ExchangeFeedback(EssentialModule):
academical_level_appreciation = models.IntegerField( academical_level_appreciation = models.IntegerField(
validators=[MinValueValidator(-5), MaxValueValidator(5)] validators=[MinValueValidator(-5), MaxValueValidator(5)]
) )
foreign_student_welcome = models.PositiveIntegerField( foreign_student_welcome = models.IntegerField(
validators=[MaxValueValidator(10)] validators=[MinValueValidator(-5), MaxValueValidator(5)]
)
cultural_interest = models.IntegerField(
validators=[MinValueValidator(-5), MaxValueValidator(5)]
) )
cultural_interest = models.PositiveIntegerField(validators=[MaxValueValidator(10)])
class ExchangeFeedbackSerializer(EssentialModuleSerializer): class ExchangeFeedbackSerializer(EssentialModuleSerializer):
...@@ -36,15 +42,41 @@ class ExchangeFeedbackSerializer(EssentialModuleSerializer): ...@@ -36,15 +42,41 @@ class ExchangeFeedbackSerializer(EssentialModuleSerializer):
class Meta: class Meta:
model = ExchangeFeedback 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): class ExchangeFeedbackViewSet(EssentialModuleViewSet):
queryset = ExchangeFeedback.objects.all().prefetch_related( permission_classes = (
"exchange", NoDelete & NoPost & (ReadOnly | IsStaff | ExchangePermission),
"exchange__exchange_courses", )
"exchange__exchange_courses__course_feedback", queryset = (
) # pylint: disable=E1101 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 serializer_class = ExchangeFeedbackSerializer
end_point_route = "exchangeFeedbacks" end_point_route = "exchangeFeedbacks"
filterset_fields = ("university",) filterset_fields = ("university",)
required_filterset_fields = ("university",)
...@@ -5,6 +5,7 @@ from backend_app.models.abstract.essentialModule import ( ...@@ -5,6 +5,7 @@ from backend_app.models.abstract.essentialModule import (
EssentialModuleSerializer, EssentialModuleSerializer,
EssentialModuleViewSet, EssentialModuleViewSet,
) )
from backend_app.permissions.moderation import ModerationLevels
class ForTestingModeration(EssentialModule): class ForTestingModeration(EssentialModule):
...@@ -12,7 +13,7 @@ class ForTestingModeration(EssentialModule): ...@@ -12,7 +13,7 @@ class ForTestingModeration(EssentialModule):
Simple model for testing purposes Simple model for testing purposes
""" """
moderation_level = 1 moderation_level = ModerationLevels.DEPENDING_ON_SITE_SETTINGS
aaa = models.CharField(max_length=100) aaa = models.CharField(max_length=100)
......
from django.db import models
import reversion import reversion
from django.db import models
from backend_app.models.abstract.versionedEssentialModule import ( from backend_app.models.abstract.versionedEssentialModule import (
VersionedEssentialModule, VersionedEssentialModule,
VersionedEssentialModuleSerializer, VersionedEssentialModuleSerializer,
VersionedEssentialModuleViewSet, VersionedEssentialModuleViewSet,
) )
from backend_app.permissions.moderation import ModerationLevels
@reversion.register() @reversion.register()
...@@ -15,7 +15,7 @@ class ForTestingVersioning(VersionedEssentialModule): ...@@ -15,7 +15,7 @@ class ForTestingVersioning(VersionedEssentialModule):
Simple model for testing purposes (versioning) Simple model for testing purposes (versioning)
""" """
moderation_level = 1 moderation_level = ModerationLevels.DEPENDING_ON_SITE_SETTINGS
bbb = models.CharField(max_length=100) bbb = models.CharField(max_length=100)
......
...@@ -10,6 +10,7 @@ from backend_app.models.abstract.base import ( ...@@ -10,6 +10,7 @@ from backend_app.models.abstract.base import (
) )
from backend_app.models.university import University from backend_app.models.university import University
from backend_app.permissions.app_permissions import IsOwner, ReadOnly, IsPublic from backend_app.permissions.app_permissions import IsOwner, ReadOnly, IsPublic
from backend_app.permissions.moderation import ModerationLevels