wbhuman_resources 1.58.4__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. wbhuman_resources/__init__.py +1 -0
  2. wbhuman_resources/admin/__init__.py +5 -0
  3. wbhuman_resources/admin/absence.py +113 -0
  4. wbhuman_resources/admin/calendars.py +37 -0
  5. wbhuman_resources/admin/employee.py +109 -0
  6. wbhuman_resources/admin/kpi.py +21 -0
  7. wbhuman_resources/admin/review.py +157 -0
  8. wbhuman_resources/apps.py +23 -0
  9. wbhuman_resources/dynamic_preferences_registry.py +119 -0
  10. wbhuman_resources/factories/__init__.py +38 -0
  11. wbhuman_resources/factories/absence.py +109 -0
  12. wbhuman_resources/factories/calendars.py +60 -0
  13. wbhuman_resources/factories/employee.py +80 -0
  14. wbhuman_resources/factories/kpi.py +155 -0
  15. wbhuman_resources/filters/__init__.py +20 -0
  16. wbhuman_resources/filters/absence.py +109 -0
  17. wbhuman_resources/filters/absence_graphs.py +85 -0
  18. wbhuman_resources/filters/calendars.py +28 -0
  19. wbhuman_resources/filters/employee.py +81 -0
  20. wbhuman_resources/filters/kpi.py +35 -0
  21. wbhuman_resources/filters/review.py +134 -0
  22. wbhuman_resources/filters/signals.py +27 -0
  23. wbhuman_resources/locale/de/LC_MESSAGES/django.mo +0 -0
  24. wbhuman_resources/locale/de/LC_MESSAGES/django.po +2207 -0
  25. wbhuman_resources/locale/de/LC_MESSAGES/django.po.translated +2456 -0
  26. wbhuman_resources/locale/en/LC_MESSAGES/django.mo +0 -0
  27. wbhuman_resources/locale/en/LC_MESSAGES/django.po +2091 -0
  28. wbhuman_resources/locale/fr/LC_MESSAGES/django.mo +0 -0
  29. wbhuman_resources/locale/fr/LC_MESSAGES/django.po +2093 -0
  30. wbhuman_resources/management/__init__.py +23 -0
  31. wbhuman_resources/migrations/0001_initial_squashed_squashed_0015_alter_absencerequest_calendaritem_ptr_and_more.py +949 -0
  32. wbhuman_resources/migrations/0016_alter_employeehumanresource_options.py +20 -0
  33. wbhuman_resources/migrations/0017_absencerequest_crossborder_country_and_more.py +55 -0
  34. wbhuman_resources/migrations/0018_remove_position_group_position_groups.py +32 -0
  35. wbhuman_resources/migrations/0019_alter_absencerequest_options_alter_kpi_options_and_more.py +44 -0
  36. wbhuman_resources/migrations/0020_alter_employeeyearbalance_year_alter_review_year.py +27 -0
  37. wbhuman_resources/migrations/0021_alter_position_color.py +18 -0
  38. wbhuman_resources/migrations/0022_remove_review_editable_mode.py +64 -0
  39. wbhuman_resources/migrations/__init__.py +0 -0
  40. wbhuman_resources/models/__init__.py +23 -0
  41. wbhuman_resources/models/absence.py +903 -0
  42. wbhuman_resources/models/calendars.py +370 -0
  43. wbhuman_resources/models/employee.py +1241 -0
  44. wbhuman_resources/models/kpi.py +199 -0
  45. wbhuman_resources/models/preferences.py +40 -0
  46. wbhuman_resources/models/review.py +982 -0
  47. wbhuman_resources/permissions/__init__.py +0 -0
  48. wbhuman_resources/permissions/backend.py +26 -0
  49. wbhuman_resources/serializers/__init__.py +49 -0
  50. wbhuman_resources/serializers/absence.py +308 -0
  51. wbhuman_resources/serializers/calendars.py +73 -0
  52. wbhuman_resources/serializers/employee.py +267 -0
  53. wbhuman_resources/serializers/kpi.py +80 -0
  54. wbhuman_resources/serializers/review.py +415 -0
  55. wbhuman_resources/signals.py +4 -0
  56. wbhuman_resources/tasks.py +195 -0
  57. wbhuman_resources/templates/review/review_report.html +322 -0
  58. wbhuman_resources/tests/__init__.py +1 -0
  59. wbhuman_resources/tests/conftest.py +96 -0
  60. wbhuman_resources/tests/models/__init__.py +0 -0
  61. wbhuman_resources/tests/models/test_absences.py +478 -0
  62. wbhuman_resources/tests/models/test_calendars.py +209 -0
  63. wbhuman_resources/tests/models/test_employees.py +502 -0
  64. wbhuman_resources/tests/models/test_review.py +103 -0
  65. wbhuman_resources/tests/models/test_utils.py +110 -0
  66. wbhuman_resources/tests/signals.py +108 -0
  67. wbhuman_resources/tests/test_permission.py +64 -0
  68. wbhuman_resources/tests/test_tasks.py +74 -0
  69. wbhuman_resources/urls.py +221 -0
  70. wbhuman_resources/utils.py +43 -0
  71. wbhuman_resources/viewsets/__init__.py +61 -0
  72. wbhuman_resources/viewsets/absence.py +312 -0
  73. wbhuman_resources/viewsets/absence_charts.py +328 -0
  74. wbhuman_resources/viewsets/buttons/__init__.py +7 -0
  75. wbhuman_resources/viewsets/buttons/absence.py +32 -0
  76. wbhuman_resources/viewsets/buttons/employee.py +44 -0
  77. wbhuman_resources/viewsets/buttons/kpis.py +16 -0
  78. wbhuman_resources/viewsets/buttons/review.py +195 -0
  79. wbhuman_resources/viewsets/calendars.py +103 -0
  80. wbhuman_resources/viewsets/display/__init__.py +39 -0
  81. wbhuman_resources/viewsets/display/absence.py +334 -0
  82. wbhuman_resources/viewsets/display/calendars.py +83 -0
  83. wbhuman_resources/viewsets/display/employee.py +254 -0
  84. wbhuman_resources/viewsets/display/kpis.py +92 -0
  85. wbhuman_resources/viewsets/display/review.py +429 -0
  86. wbhuman_resources/viewsets/employee.py +210 -0
  87. wbhuman_resources/viewsets/endpoints/__init__.py +42 -0
  88. wbhuman_resources/viewsets/endpoints/absence.py +57 -0
  89. wbhuman_resources/viewsets/endpoints/calendars.py +18 -0
  90. wbhuman_resources/viewsets/endpoints/employee.py +51 -0
  91. wbhuman_resources/viewsets/endpoints/kpis.py +53 -0
  92. wbhuman_resources/viewsets/endpoints/review.py +191 -0
  93. wbhuman_resources/viewsets/kpi.py +280 -0
  94. wbhuman_resources/viewsets/menu/__init__.py +22 -0
  95. wbhuman_resources/viewsets/menu/absence.py +50 -0
  96. wbhuman_resources/viewsets/menu/administration.py +15 -0
  97. wbhuman_resources/viewsets/menu/calendars.py +33 -0
  98. wbhuman_resources/viewsets/menu/employee.py +44 -0
  99. wbhuman_resources/viewsets/menu/kpis.py +18 -0
  100. wbhuman_resources/viewsets/menu/review.py +97 -0
  101. wbhuman_resources/viewsets/mixins.py +14 -0
  102. wbhuman_resources/viewsets/review.py +837 -0
  103. wbhuman_resources/viewsets/titles/__init__.py +18 -0
  104. wbhuman_resources/viewsets/titles/absence.py +30 -0
  105. wbhuman_resources/viewsets/titles/employee.py +18 -0
  106. wbhuman_resources/viewsets/titles/kpis.py +15 -0
  107. wbhuman_resources/viewsets/titles/review.py +62 -0
  108. wbhuman_resources/viewsets/utils.py +28 -0
  109. wbhuman_resources-1.58.4.dist-info/METADATA +8 -0
  110. wbhuman_resources-1.58.4.dist-info/RECORD +111 -0
  111. wbhuman_resources-1.58.4.dist-info/WHEEL +5 -0
@@ -0,0 +1,837 @@
1
+ import pandas as pd
2
+ from django.contrib.messages import info, warning
3
+ from django.db.models import ExpressionWrapper, F, Q, Value, functions
4
+ from django.db.models.fields import CharField
5
+ from django.shortcuts import get_object_or_404
6
+ from django.utils import timezone
7
+ from django.utils.functional import cached_property
8
+ from django.utils.translation import gettext
9
+ from django.utils.translation import gettext_lazy as _
10
+ from rest_framework import filters, status
11
+ from rest_framework.decorators import action
12
+ from rest_framework.response import Response
13
+ from wbcore import viewsets
14
+ from wbcore.contrib.authentication.authentication import JWTCookieAuthentication
15
+ from wbcore.pandas import fields as pf
16
+ from wbcore.pandas.views import PandasAPIViewSet
17
+ from wbcore.utils.strings import format_number
18
+ from wbcore.utils.views import CloneMixin
19
+ from wbcore.viewsets.mixins import OrderableMixin
20
+
21
+ from wbhuman_resources.filters import (
22
+ RatingReviewAnswerReviewFilter,
23
+ ReviewAnswerFilter,
24
+ ReviewFilter,
25
+ ReviewGroupFilter,
26
+ ReviewProgressReviewFilter,
27
+ ReviewQuestionCategoryFilter,
28
+ ReviewQuestionFilter,
29
+ ReviewTemplateFilter,
30
+ )
31
+ from wbhuman_resources.models import (
32
+ Review,
33
+ ReviewAnswer,
34
+ ReviewGroup,
35
+ ReviewQuestion,
36
+ ReviewQuestionCategory,
37
+ create_review_from_template,
38
+ send_review_report_via_mail,
39
+ submit_reviews_from_group,
40
+ )
41
+ from wbhuman_resources.serializers import (
42
+ ReviewAnswerModelSerializer,
43
+ ReviewAnswerRepresentationSerializer,
44
+ ReviewGroupModelSerializer,
45
+ ReviewGroupRepresentationSerializer,
46
+ ReviewListModelSerializer,
47
+ ReviewModelSerializer,
48
+ ReviewQuestionCategoryModelSerializer,
49
+ ReviewQuestionCategoryRepresentationSerializer,
50
+ ReviewQuestionModelSerializer,
51
+ ReviewQuestionRepresentationSerializer,
52
+ ReviewReadOnlyModelSerializer,
53
+ ReviewRepresentationSerializer,
54
+ )
55
+
56
+ from .buttons import ReviewButtonConfig, ReviewGroupButtonConfig
57
+ from .display import (
58
+ ReviewAnswerDisplayConfig,
59
+ ReviewAnswerReviewPandasDisplayConfig,
60
+ ReviewDisplayConfig,
61
+ ReviewGroupDisplayConfig,
62
+ ReviewProgressPandasDisplayConfig,
63
+ ReviewProgressReviewPandasDisplayConfig,
64
+ ReviewQuestionCategoryDisplayConfig,
65
+ ReviewQuestionDisplayConfig,
66
+ ReviewQuestionReviewDisplayConfig,
67
+ ReviewTemplateDisplayConfig,
68
+ )
69
+ from .endpoints import (
70
+ ReviewAnswerEndpointConfig,
71
+ ReviewAnswerReviewNoCategoryEndpointConfig,
72
+ ReviewAnswerReviewPandasEndpointConfig,
73
+ ReviewAnswerReviewQuestionCategoryEndpointConfig,
74
+ ReviewEndpointConfig,
75
+ ReviewGroupEndpointConfig,
76
+ ReviewProgressPandasEndpointConfig,
77
+ ReviewProgressReviewPandasEndpointConfig,
78
+ ReviewQuestionCategoryEndpointConfig,
79
+ ReviewQuestionEndpointConfig,
80
+ ReviewQuestionReviewCategoryEndpointConfig,
81
+ ReviewQuestionReviewEndpointConfig,
82
+ ReviewQuestionReviewNoCategoryEndpointConfig,
83
+ ReviewQuestionReviewQuestionCategoryEndpointConfig,
84
+ ReviewReviewGroupEndpointConfig,
85
+ )
86
+ from .titles import (
87
+ ReviewAnswerReviewPandasTitleConfig,
88
+ ReviewAnswerReviewTitleConfig,
89
+ ReviewProgressPandasTitleConfig,
90
+ ReviewProgressReviewPandasTitleConfig,
91
+ ReviewQuestionReviewQuestionCategoryTitleConfig,
92
+ ReviewQuestionReviewTitleConfig,
93
+ ReviewReviewGroupTitleConfig,
94
+ ReviewTemplateTitleConfig,
95
+ )
96
+
97
+
98
+ def apply_group_anonymized(x):
99
+ if x["answered_by_id"] == x["reviewee"]:
100
+ x["group_anonymized"] = "reviewee"
101
+ elif x["answered_by_id"] == x["reviewer"]:
102
+ x["group_anonymized"] = "reviewer"
103
+ else:
104
+ x["group_anonymized"] = "peers"
105
+ return x
106
+
107
+
108
+ class ReviewGroupRepresentationViewSet(viewsets.RepresentationViewSet):
109
+ IDENTIFIER = "wbhuman_resources:reviewgrouprepresentation"
110
+
111
+ filter_backends = (filters.OrderingFilter, filters.SearchFilter)
112
+ ordering_fields = ordering = ("name",)
113
+ search_fields = ("name",)
114
+
115
+ queryset = ReviewGroup.objects.all()
116
+ serializer_class = ReviewGroupRepresentationSerializer
117
+
118
+
119
+ class ReviewGroupModelViewSet(viewsets.ModelViewSet):
120
+ IDENTIFIER = "wbhuman_resources:reviewgroup"
121
+ display_config_class = ReviewGroupDisplayConfig
122
+ endpoint_config_class = ReviewGroupEndpointConfig
123
+ button_config_class = ReviewGroupButtonConfig
124
+ search_fields = ["name"]
125
+ ordering_fields = ordering = ["name"]
126
+
127
+ filterset_class = ReviewGroupFilter
128
+
129
+ serializer_class = ReviewGroupModelSerializer
130
+
131
+ queryset = ReviewGroup.objects.all()
132
+
133
+ @action(detail=True, methods=["PATCH"], authentication_classes=[JWTCookieAuthentication])
134
+ def submitreviews(self, request, pk):
135
+ submit_reviews_from_group.delay(pk, request.user.id)
136
+ return Response(
137
+ {"__notification": {"title": gettext("Reviews successfully submitted")}},
138
+ status=status.HTTP_200_OK,
139
+ )
140
+
141
+
142
+ class ReviewRepresentationViewSet(viewsets.RepresentationViewSet):
143
+ IDENTIFIER = "wbhuman_resources:reviewrepresentation"
144
+ queryset = Review.objects.all()
145
+ search_fields = [
146
+ "computed_str",
147
+ "reviewer__computed_str",
148
+ "reviewee__computed_str",
149
+ "moderator__computed_str",
150
+ "review_group__name",
151
+ ]
152
+ ordering = ["-is_template", "-year", "computed_str"]
153
+ serializer_class = ReviewRepresentationSerializer
154
+
155
+
156
+ class ReviewModelViewSet(CloneMixin, viewsets.ModelViewSet):
157
+ IDENTIFIER = "wbhuman_resources:review"
158
+ button_config_class = ReviewButtonConfig
159
+ display_config_class = ReviewDisplayConfig
160
+ endpoint_config_class = ReviewEndpointConfig
161
+ queryset = Review.objects.all()
162
+ search_fields = [
163
+ "computed_str",
164
+ "reviewer__computed_str",
165
+ "reviewee__computed_str",
166
+ "moderator__computed_str",
167
+ "review_group__name",
168
+ ]
169
+ ordering = ["-year", "-to_date", "-changed"]
170
+ ordering_fields = [
171
+ "from_date",
172
+ "to_date",
173
+ "review_deadline",
174
+ "review",
175
+ "auto_apply_deadline",
176
+ "status",
177
+ "reviewee",
178
+ "reviewer",
179
+ "moderator",
180
+ "review_group",
181
+ "is_template",
182
+ "year",
183
+ "type",
184
+ "changed",
185
+ ]
186
+ filterset_class = ReviewFilter
187
+
188
+ serializer_class = ReviewModelSerializer
189
+
190
+ def get_serializer_class(self):
191
+ if hasattr(self, "kwargs"):
192
+ if review_id := self.kwargs.get("pk"):
193
+ obj = get_object_or_404(Review, pk=review_id)
194
+ if (
195
+ self.request.user.profile in [obj.moderator, obj.reviewer, obj.reviewee]
196
+ or self.request.user in Review.get_administrators()
197
+ ):
198
+ return ReviewModelSerializer
199
+ else:
200
+ return ReviewReadOnlyModelSerializer
201
+ return ReviewListModelSerializer
202
+
203
+ queryset = Review.objects.all()
204
+
205
+ def get_queryset(self):
206
+ qs = (
207
+ super()
208
+ .get_queryset()
209
+ .annotate(
210
+ related_to_me=Review.get_subquery_review_related_to(self.request.user.profile),
211
+ global_rating=Review.subquery_global_rating(self.request.user.profile),
212
+ )
213
+ )
214
+ access_condition = (
215
+ Q(related_to_me__isnull=False)
216
+ | Q(moderator=self.request.user.profile)
217
+ | Q(reviewer=self.request.user.profile)
218
+ | Q(reviewee=self.request.user.profile)
219
+ | (Q(related_to_me__isnull=True) & Q(moderator=None))
220
+ )
221
+ if self.request.user.has_perm("wbhuman_resources.administrate_review"):
222
+ access_condition |= Q(status=Review.Status.VALIDATION)
223
+ if self.request.user in Review.get_administrators():
224
+ access_condition |= ~Q(status=Review.Status.PREPARATION_OF_REVIEW)
225
+ return qs.filter(access_condition)
226
+
227
+ @cached_property
228
+ def is_modifiable(self):
229
+ if "pk" in self.kwargs and (obj := self.get_object()):
230
+ return obj.moderator == self.request.user.profile and obj.status in [
231
+ Review.Status.FILL_IN_REVIEW,
232
+ Review.Status.PREPARATION_OF_REVIEW,
233
+ ]
234
+ return self.new_mode
235
+
236
+ @action(detail=True, methods=["PATCH"], authentication_classes=[JWTCookieAuthentication])
237
+ def generate(self, request, pk):
238
+ from_date = request.POST.get("from_date", None)
239
+ to_date = request.POST.get("to_date", None)
240
+ review_deadline = request.POST.get("review_deadline", None)
241
+ auto_apply_deadline = request.POST.get("auto_apply_deadline", None)
242
+ employees = request.POST.get("employees", None)
243
+ include_kpi = request.POST.get("include_kpi", None)
244
+ create_review_from_template.delay(
245
+ pk, from_date, to_date, review_deadline, auto_apply_deadline, employees, include_kpi
246
+ )
247
+
248
+ return Response(
249
+ {"__notification": {"title": gettext("Review is going to be created from template: ") + str(pk)}},
250
+ status=status.HTTP_200_OK,
251
+ )
252
+
253
+ @action(detail=True, methods=["PATCH"], authentication_classes=[JWTCookieAuthentication])
254
+ def generate_pdf(self, request, pk):
255
+ review = Review.objects.get(id=pk)
256
+
257
+ if review.moderator != request.user.profile and not request.user.has_perm(
258
+ "wbhuman_resources.administrate_review"
259
+ ):
260
+ return Response({}, status=status.HTTP_403_FORBIDDEN)
261
+ send_review_report_via_mail.delay(request.user.id, pk)
262
+
263
+ return Response(
264
+ {"__notification": {"title": gettext("PDF is going to be created and sent to you.")}},
265
+ status=status.HTTP_200_OK,
266
+ )
267
+
268
+ @action(detail=True, methods=["PATCH"], authentication_classes=[JWTCookieAuthentication])
269
+ def signature_reviewee(self, request, pk=None):
270
+ review = Review.objects.get(id=pk)
271
+ review.signed_reviewee = timezone.now()
272
+ if feedback_reviewee := request.POST.get("feedback_reviewee", ""):
273
+ review.feedback_reviewee = feedback_reviewee
274
+ review.save()
275
+ return Response(
276
+ {"__notification": {"title": gettext("Review is acknowledged by the reviewee")}},
277
+ status=status.HTTP_200_OK,
278
+ )
279
+
280
+ @action(detail=True, methods=["PATCH"], authentication_classes=[JWTCookieAuthentication])
281
+ def signature_reviewer(self, request, pk=None):
282
+ review = Review.objects.get(id=pk)
283
+ review.signed_reviewer = timezone.now()
284
+ if feedback_reviewer := request.POST.get("feedback_reviewer", ""):
285
+ review.feedback_reviewer = feedback_reviewer
286
+ review.save()
287
+ return Response(
288
+ {"__notification": {"title": gettext("Review is acknowledged by the reviewer")}},
289
+ status=status.HTTP_200_OK,
290
+ )
291
+
292
+ @action(detail=True, methods=["PATCH"], authentication_classes=[JWTCookieAuthentication])
293
+ def completelyfilled_reviewee(self, request, pk=None):
294
+ review = Review.objects.get(id=pk)
295
+ review.completely_filled_reviewee = timezone.now()
296
+ if review.completely_filled_reviewer:
297
+ review.finalize()
298
+ review.save()
299
+ return Response(
300
+ {"__notification": {"title": gettext("Reviewee has finished answering the review questions")}},
301
+ status=status.HTTP_200_OK,
302
+ )
303
+
304
+ @action(detail=True, methods=["PATCH"], authentication_classes=[JWTCookieAuthentication])
305
+ def completelyfilled_reviewer(self, request, pk=None):
306
+ review = Review.objects.get(id=pk)
307
+ review.completely_filled_reviewer = timezone.now()
308
+ if review.completely_filled_reviewee:
309
+ review.finalize()
310
+ review.save()
311
+ return Response(
312
+ {"__notification": {"title": gettext("Reviewer has finished answering the review questions")}},
313
+ status=status.HTTP_200_OK,
314
+ )
315
+
316
+
317
+ class ReviewReviewGroupModelViewSet(ReviewModelViewSet):
318
+ title_config_class = ReviewReviewGroupTitleConfig
319
+ endpoint_config_class = ReviewReviewGroupEndpointConfig
320
+
321
+ def get_queryset(self):
322
+ return super().get_queryset().filter(review_group__id=self.kwargs["review_group_id"])
323
+
324
+
325
+ class ReviewTemplateModelViewSet(ReviewModelViewSet):
326
+ title_config_class = ReviewTemplateTitleConfig
327
+ display_config_class = ReviewTemplateDisplayConfig
328
+
329
+ filterset_class = ReviewTemplateFilter
330
+
331
+ def get_queryset(self):
332
+ return super().get_queryset().filter(is_template=True)
333
+
334
+ def add_messages(
335
+ self,
336
+ request,
337
+ queryset=None,
338
+ paginated_queryset=None,
339
+ instance=None,
340
+ initial=False,
341
+ ):
342
+ message = gettext(
343
+ "The fields <b>From</b>, <b>To</b>, <b>Deadline</b>, <b>Reviewee</b> and <b>Reviewer</b> have to be empty if <u><b> template is true </b></u> within a <b>Review Group</b>. They get filled in once the <b>Review Group</b> creates them."
344
+ )
345
+ if not instance:
346
+ # TODO Show the message during the creation
347
+ info(request, message)
348
+
349
+
350
+ class ReviewQuestionCategoryRepresentationViewSet(viewsets.RepresentationViewSet):
351
+ IDENTIFIER = "wbhuman_resources:reviewquestioncategoryrepresentation"
352
+
353
+ filter_backends = (filters.OrderingFilter, filters.SearchFilter)
354
+ ordering_fields = ordering = ("name",)
355
+ search_fields = ("name",)
356
+
357
+ queryset = ReviewQuestionCategory.objects.all()
358
+ serializer_class = ReviewQuestionCategoryRepresentationSerializer
359
+
360
+
361
+ class ReviewQuestionCategoryModelViewSet(OrderableMixin, viewsets.ModelViewSet):
362
+ IDENTIFIER = "wbhuman_resources:reviewquestioncategory"
363
+ display_config_class = ReviewQuestionCategoryDisplayConfig
364
+ endpoint_config_class = ReviewQuestionCategoryEndpointConfig
365
+
366
+ search_fields = ["name"]
367
+ ordering = ["order", "name"]
368
+ ordering_fields = ["name", "weight"]
369
+
370
+ filterset_class = ReviewQuestionCategoryFilter
371
+
372
+ serializer_class = ReviewQuestionCategoryModelSerializer
373
+
374
+ queryset = ReviewQuestionCategory.objects.all()
375
+
376
+
377
+ class ReviewQuestionRepresentationViewSet(viewsets.RepresentationViewSet):
378
+ IDENTIFIER = "wbhuman_resources:reviewquestionrepresentation"
379
+ search_fields = ["question"]
380
+
381
+ queryset = ReviewQuestion.objects.all()
382
+ serializer_class = ReviewQuestionRepresentationSerializer
383
+
384
+
385
+ class ReviewQuestionModelViewSet(OrderableMixin, viewsets.ModelViewSet):
386
+ IDENTIFIER = "wbhuman_resources:reviewquestion"
387
+ display_config_class = ReviewQuestionDisplayConfig
388
+ endpoint_config_class = ReviewQuestionEndpointConfig
389
+
390
+ search_fields = ["question"]
391
+ ordering = ["order"]
392
+ ordering_fields = [
393
+ "review",
394
+ "category",
395
+ "mandatory",
396
+ "answer_type",
397
+ "for_reviewee",
398
+ "for_reviewer",
399
+ "for_department_peers",
400
+ "for_company_peers",
401
+ "weight",
402
+ ]
403
+
404
+ filterset_class = ReviewQuestionFilter
405
+
406
+ serializer_class = ReviewQuestionModelSerializer
407
+
408
+ queryset = ReviewQuestion.objects.all()
409
+
410
+ def get_queryset(self):
411
+ return (
412
+ super()
413
+ .get_queryset()
414
+ .annotate(
415
+ review_for=ExpressionWrapper(
416
+ functions.Concat(
417
+ functions.Cast("for_reviewee", output_field=CharField()),
418
+ Value(" / "),
419
+ functions.Cast("for_reviewer", output_field=CharField()),
420
+ Value(" / "),
421
+ functions.Cast("for_department_peers", output_field=CharField()),
422
+ Value(" / "),
423
+ functions.Cast("for_company_peers", output_field=CharField()),
424
+ ),
425
+ output_field=CharField(),
426
+ )
427
+ )
428
+ )
429
+
430
+
431
+ class ReviewQuestionReviewModelViewSet(ReviewQuestionModelViewSet):
432
+ title_config_class = ReviewQuestionReviewTitleConfig
433
+ endpoint_config_class = ReviewQuestionReviewEndpointConfig
434
+ display_config_class = ReviewQuestionReviewDisplayConfig
435
+
436
+ @cached_property
437
+ def review(self):
438
+ return get_object_or_404(Review, pk=self.kwargs["review_id"])
439
+
440
+ def get_queryset(self):
441
+ return super().get_queryset().filter(review=self.review)
442
+
443
+
444
+ class ReviewQuestionReviewNoCategoryModelViewSet(ReviewQuestionReviewModelViewSet):
445
+ endpoint_config_class = ReviewQuestionReviewNoCategoryEndpointConfig
446
+
447
+ def get_queryset(self):
448
+ return super().get_queryset().filter(Q(review__id=self.kwargs["review_id"]) & Q(category=None))
449
+
450
+
451
+ class ReviewQuestionReviewCategoryModelViewSet(ReviewQuestionReviewModelViewSet):
452
+ endpoint_config_class = ReviewQuestionReviewCategoryEndpointConfig
453
+
454
+ def get_queryset(self):
455
+ return (
456
+ super()
457
+ .get_queryset()
458
+ .filter(Q(review__id=self.kwargs["review_id"]) & Q(category=self.kwargs["category_id"]))
459
+ )
460
+
461
+
462
+ class ReviewQuestionReviewQuestionCategoryModelViewSet(ReviewQuestionModelViewSet):
463
+ title_config_class = ReviewQuestionReviewQuestionCategoryTitleConfig
464
+ endpoint_config_class = ReviewQuestionReviewQuestionCategoryEndpointConfig
465
+
466
+ def get_queryset(self):
467
+ return super().get_queryset().filter(category__id=self.kwargs["category_id"])
468
+
469
+
470
+ class ReviewAnswerRepresentationViewSet(viewsets.RepresentationViewSet):
471
+ IDENTIFIER = "wbhuman_resources:reviewanswerrepresentation"
472
+ queryset = ReviewAnswer.objects.all()
473
+ serializer_class = ReviewAnswerRepresentationSerializer
474
+
475
+
476
+ class ReviewAnswerModelViewSet(viewsets.ModelViewSet):
477
+ IDENTIFIER = "wbhuman_resources:reviewanswer"
478
+ display_config_class = ReviewAnswerDisplayConfig
479
+ endpoint_config_class = ReviewAnswerEndpointConfig
480
+
481
+ search_fields = ["question__question", "answered_by__computed_str", "answer_text"]
482
+ ordering_fields = ["question", "answer_number", "answer_text", "answered_by"]
483
+
484
+ filterset_class = ReviewAnswerFilter
485
+
486
+ serializer_class = ReviewAnswerModelSerializer
487
+
488
+ queryset = ReviewAnswer.objects.select_related("question").annotate(
489
+ mandatory=F("question__mandatory"),
490
+ weight=F("question__weight"),
491
+ question_name=F("question__computed_str"),
492
+ )
493
+
494
+ def add_messages(
495
+ self,
496
+ request,
497
+ queryset=None,
498
+ paginated_queryset=None,
499
+ instance=None,
500
+ initial=False,
501
+ ):
502
+ message = gettext(
503
+ "It is recommended to add a comment for the answers whose rating is 'very bad', 'bad' or 'very good'"
504
+ )
505
+ error_found = False
506
+ qs = [instance] if instance else queryset
507
+ for instance in qs:
508
+ if instance.answer_number in [1, 2, 4] and not instance.answer_text:
509
+ error_found = True
510
+ if error_found:
511
+ warning(request, message)
512
+
513
+
514
+ class ReviewAnswerReviewQuestionCategoryModelViewSet(ReviewAnswerModelViewSet):
515
+ ordering = ["question__order", "question_name", "answered_by"]
516
+ title_config_class = ReviewAnswerReviewTitleConfig
517
+ endpoint_config_class = ReviewAnswerReviewQuestionCategoryEndpointConfig
518
+
519
+ @cached_property
520
+ def review(self):
521
+ return get_object_or_404(Review, pk=self.kwargs["review_id"])
522
+
523
+ def get_queryset(self):
524
+ qs = (
525
+ super()
526
+ .get_queryset()
527
+ .filter(Q(question__review=self.review) & Q(question__category=self.kwargs.get("category_id")))
528
+ )
529
+ if self.review.status in [Review.Status.REVIEW, Review.Status.FILL_IN_REVIEW]:
530
+ qs = qs.filter(answered_by=self.request.user.profile)
531
+ elif self.review.status in [Review.Status.EVALUATION, Review.Status.VALIDATION]:
532
+ qs = (
533
+ qs.filter(question__answer_type=ReviewQuestion.ANSWERTYPE.TEXT)
534
+ .exclude(~Q(answered_by=F("question__review__reviewee")) & Q(answer_text=None))
535
+ .order_by("question_name")
536
+ )
537
+ return qs
538
+
539
+
540
+ class ReviewAnswerReviewNoCategoryModelViewSet(ReviewAnswerReviewQuestionCategoryModelViewSet):
541
+ endpoint_config_class = ReviewAnswerReviewNoCategoryEndpointConfig
542
+
543
+
544
+ class ReviewProgressReviewPandasViewSet(PandasAPIViewSet):
545
+ IDENTIFIER = "wbhuman_resources:review-progress"
546
+ queryset = ReviewAnswer.objects.all()
547
+
548
+ display_config_class = ReviewProgressReviewPandasDisplayConfig
549
+ title_config_class = ReviewProgressReviewPandasTitleConfig
550
+ endpoint_config_class = ReviewProgressReviewPandasEndpointConfig
551
+
552
+ ordering_fields = ["answered_by"]
553
+
554
+ filterset_class = ReviewProgressReviewFilter
555
+
556
+ def get_queryset(self):
557
+ return (
558
+ super()
559
+ .get_queryset()
560
+ .filter(question__review=self.kwargs["review_id"])
561
+ .annotate(answer_type=F("question__answer_type"), answered_by_name=F("answered_by__computed_str"))
562
+ )
563
+
564
+ def get_pandas_fields(self, request):
565
+ fields = [
566
+ pf.PKField("id", label=_("ID")),
567
+ pf.CharField(key="answered_by_name", label=_("Answered By")),
568
+ pf.FloatField(key="progress", label=_("Progress"), percent=True),
569
+ pf.CharField(key="answered_by", label=_("Answered By")),
570
+ pf.CharField(key="answer_type_y", label=_("Total")),
571
+ pf.CharField(key="answer_number", label=_("Answer Number")),
572
+ pf.TextField(key="answer_text", label=_("Answer Text")),
573
+ ]
574
+ return pf.PandasFields(fields=tuple(fields))
575
+
576
+ def get_dataframe(self, request, queryset, **kwargs):
577
+ df = pd.DataFrame()
578
+ if queryset.exists():
579
+ df0 = pd.DataFrame(
580
+ queryset.values(
581
+ "answered_anonymized", "answered_by_name", "answer_type", "answer_number", "answer_text"
582
+ ),
583
+ columns=["answered_anonymized", "answered_by_name", "answer_type", "answer_number", "answer_text"],
584
+ )
585
+ df_rating = df0[df0["answer_type"] == "RATING"].groupby(["answered_by_name"]).count()
586
+ df_text = df0[df0["answer_type"] == "TEXT"].groupby(["answered_by_name"]).count()
587
+ df = pd.merge(
588
+ df_rating[["answer_type", "answer_number"]],
589
+ df_text[["answer_type", "answer_text"]],
590
+ on="answered_by_name",
591
+ how="outer",
592
+ )
593
+ df.fillna(0, inplace=True)
594
+ df["progress"] = (df["answer_number"] + df["answer_text"]) / (df["answer_type_x"] + df["answer_type_y"])
595
+ df.progress.fillna(0, inplace=True)
596
+ df["answered_by_name"] = df.index
597
+ df = df.sort_values(by=["progress"], ascending=False)
598
+ df.reset_index(drop=True, inplace=True)
599
+ df["id"] = df.index
600
+ return df
601
+
602
+
603
+ class ReviewAnswerReviewPandasViewSet(PandasAPIViewSet):
604
+ IDENTIFIER = "wbhuman_resources:review-reviewanswerpandasview"
605
+ queryset = ReviewAnswer.objects.all()
606
+
607
+ ordering_fields = ["answered_by"]
608
+
609
+ display_config_class = ReviewAnswerReviewPandasDisplayConfig
610
+ title_config_class = ReviewAnswerReviewPandasTitleConfig
611
+ endpoint_config_class = ReviewAnswerReviewPandasEndpointConfig
612
+
613
+ filterset_class = RatingReviewAnswerReviewFilter
614
+
615
+ @cached_property
616
+ def review(self):
617
+ return get_object_or_404(Review, pk=self.kwargs["review_id"])
618
+
619
+ def get_queryset(self):
620
+ return (
621
+ super()
622
+ .get_queryset()
623
+ .filter(question__review=self.review)
624
+ .annotate(
625
+ answer_type=F("question__answer_type"),
626
+ category_question=F("question__category"),
627
+ category_question_name=F("question__category__name"),
628
+ category_question_order=F("question__category__order"),
629
+ question_name=F("question__question"),
630
+ question_order=F("question__order"),
631
+ reviewee=F("question__review__reviewee"),
632
+ reviewer=F("question__review__reviewer"),
633
+ weight=F("question__weight"),
634
+ )
635
+ )
636
+
637
+ def get_pandas_fields(self, request):
638
+ fields = [
639
+ pf.PKField("id", label=_("ID")),
640
+ pf.CharField(key="answered_by", label=_("Answered By")),
641
+ pf.TextField(key="category_question_name", label=_("Category")),
642
+ pf.TextField(key="question_name", label=_("Question")),
643
+ pf.FloatField(key="weight", label=_("Weight")),
644
+ pf.EmojiRatingField(key="reviewee", label=_("Reviewee")),
645
+ pf.EmojiRatingField(key="reviewer", label=_("Reviewer")),
646
+ pf.EmojiRatingField(key="peers", label=_("Peers")),
647
+ pf.CharField(key="deviation", label=_("Deviation")),
648
+ pf.TextField(key="comment_reviewee", label=_("Comment Reviewee")),
649
+ pf.TextField(key="comment_reviewer", label=_("Comment Reviewer")),
650
+ pf.TextField(key="comment_peers", label=_("Comment Peers")),
651
+ ]
652
+ return pf.PandasFields(fields=tuple(fields))
653
+
654
+ def get_aggregates(self, request, df):
655
+ if df.empty:
656
+ return {}
657
+ aggregates = {}
658
+ if not df["reviewee"].isnull().all():
659
+ index = pd.notnull(df["reviewee"])
660
+ aggregates["reviewee"] = {
661
+ "μ": format_number(
662
+ round((df["reviewee"][index] * df["weight"][index]).sum() / df["weight"][index].sum())
663
+ ),
664
+ }
665
+ if not df["reviewer"].isnull().all():
666
+ index = pd.notnull(df["reviewer"])
667
+ aggregates["reviewer"] = {
668
+ "μ": format_number(
669
+ round((df["reviewer"][index] * df["weight"][index]).sum() / df["weight"][index].sum())
670
+ ),
671
+ }
672
+ if not df["peers"].isnull().all():
673
+ index = pd.notnull(df["peers"])
674
+ aggregates["peers"] = {
675
+ "μ": format_number(
676
+ round((df["peers"][index] * df["weight"][index]).sum() / df["weight"][index].sum())
677
+ ),
678
+ }
679
+
680
+ return aggregates
681
+
682
+ def get_dataframe(self, request, queryset, **kwargs):
683
+ df1 = pd.DataFrame()
684
+ if queryset.exists():
685
+ if _category_question_name := request.GET.get("category_question_name", None):
686
+ queryset = queryset.filter(category_question_name__icontains=_category_question_name)
687
+ if _question_name := request.GET.get("question_name", None):
688
+ queryset = queryset.filter(question_name__icontains=_question_name)
689
+
690
+ df = pd.DataFrame(queryset.order_by("question__category__order", "question__order").values())
691
+ df = df.apply(lambda x: apply_group_anonymized(x), axis=1)
692
+ df["answer_number"] = pd.to_numeric(df["answer_number"], downcast="float")
693
+ dfx = df[(pd.isnull(df["answer_number"])) & (df["group_anonymized"] == "peers")]
694
+ df.drop(dfx.index, inplace=True)
695
+ df["answer_number"] = df["answer_number"].where(pd.notnull(df["answer_number"]), 0.0)
696
+ df["weighted_score"] = df.weight.astype("float") * df.answer_number
697
+ df["category_question_name"] = df["category_question_name"].where(
698
+ pd.notnull(df["category_question_name"]), ""
699
+ )
700
+ df["category_question_order"] = df["category_question_order"].where(
701
+ pd.notnull(df["category_question_order"]), 0
702
+ )
703
+ df["question_order"] = df["question_order"].where(pd.notnull(df["question_order"]), 0)
704
+ df1 = df.pivot_table(
705
+ index=[
706
+ "category_question_order",
707
+ "category_question_name",
708
+ "question_order",
709
+ "question_id",
710
+ "question_name",
711
+ ],
712
+ columns="group_anonymized",
713
+ values="answer_number",
714
+ aggfunc="mean",
715
+ )
716
+ df2 = df.pivot_table(
717
+ index=[
718
+ "category_question_order",
719
+ "category_question_name",
720
+ "question_order",
721
+ "question_id",
722
+ "question_name",
723
+ ],
724
+ columns="group_anonymized",
725
+ values=["weight", "answer_text"],
726
+ aggfunc="sum",
727
+ )
728
+ df1["weight"] = df2["weight"]["reviewee"].astype(float)
729
+ _temporary_dict = {
730
+ "reviewee": "comment_reviewee",
731
+ "reviewer": "comment_reviewer",
732
+ "peers": "comment_peers",
733
+ }
734
+ for _key, _value in _temporary_dict.items():
735
+ if _key in df1.columns.tolist():
736
+ df1[_value] = df2["answer_text"][_key].apply(lambda x: x if x not in ["0", "nan"] else None)
737
+ else:
738
+ df1[_key] = None
739
+ df1[_value] = None
740
+ df1["deviation_value"] = abs(df1["reviewer"] - df1["reviewee"])
741
+ df1["deviation"] = df1["deviation_value"].apply(
742
+ lambda x: "EQUAL" if x == 0 else "LESS" if x <= 1 else "GREAT"
743
+ )
744
+ df1 = df1.reset_index()
745
+ df1["id"] = df1["question_id"]
746
+ if _deviation := request.GET.get("deviation"):
747
+ df1 = df1[df1["deviation"] == _deviation]
748
+ df1 = df1.reset_index()
749
+ df1.sort_values(by=["category_question_order", "question_order"])
750
+
751
+ def convert_to_int(x):
752
+ try:
753
+ return int(x)
754
+ except (ValueError, TypeError):
755
+ return x
756
+
757
+ df1[["reviewee", "reviewer", "peers"]] = df1[["reviewee", "reviewer", "peers"]].applymap(convert_to_int)
758
+ return df1
759
+
760
+
761
+ class ReviewProgressPandasViewSet(PandasAPIViewSet):
762
+ IDENTIFIER = "wbhuman_resources:reviewprogress"
763
+ queryset = ReviewAnswer.objects.all()
764
+ display_config_class = ReviewProgressPandasDisplayConfig
765
+ title_config_class = ReviewProgressPandasTitleConfig
766
+ endpoint_config_class = ReviewProgressPandasEndpointConfig
767
+
768
+ ordering_fields = ["reviewee", "reviewer"]
769
+
770
+ def get_queryset(self):
771
+ return (
772
+ super()
773
+ .get_queryset()
774
+ .filter(question__review__status=Review.Status.FILL_IN_REVIEW)
775
+ .annotate(
776
+ answer_type=F("question__answer_type"),
777
+ answered_by_name=F("answered_by__computed_str"),
778
+ reviewee=F("question__review__reviewee"),
779
+ reviewer=F("question__review__reviewer"),
780
+ review=F("question__review"),
781
+ review_name=F("question__review__computed_str"),
782
+ )
783
+ )
784
+
785
+ def get_pandas_fields(self, request):
786
+ fields = [
787
+ pf.PKField("id", label=_("ID")),
788
+ pf.CharField(key="review_name", label=_("Answered By")),
789
+ pf.FloatField(key="reviewee", label=_("Reviewee"), percent=True),
790
+ pf.FloatField(key="reviewer", label=_("Reviewer"), percent=True),
791
+ pf.FloatField(key="peers", label=_("Peers"), percent=True),
792
+ ]
793
+ return pf.PandasFields(fields=tuple(fields))
794
+
795
+ def get_dataframe(self, request, queryset, **kwargs):
796
+ df = pd.DataFrame()
797
+ if queryset.exists():
798
+ df0 = pd.DataFrame(
799
+ queryset.values(
800
+ "review",
801
+ "review_name",
802
+ "reviewee",
803
+ "reviewer",
804
+ "answer_type",
805
+ "answered_by_name",
806
+ "answered_by_id",
807
+ "answer_text",
808
+ "answer_number",
809
+ )
810
+ )
811
+ df0 = df0.apply(lambda x: apply_group_anonymized(x), axis=1)
812
+ df_rating = (
813
+ df0[df0["answer_type"] == "RATING"]
814
+ .groupby(["review", "review_name", "group_anonymized", "answered_by_name"])
815
+ .count()
816
+ )
817
+ df_text = (
818
+ df0[df0["answer_type"] == "TEXT"]
819
+ .groupby(["review", "review_name", "group_anonymized", "answered_by_name"])
820
+ .count()
821
+ )
822
+ df = pd.merge(
823
+ df_rating[["answer_type", "answer_number"]],
824
+ df_text[["answer_type", "answer_text"]],
825
+ on=["review", "review_name", "group_anonymized", "answered_by_name"],
826
+ how="outer",
827
+ )
828
+ df.fillna(0, inplace=True)
829
+
830
+ df["progress"] = (df["answer_number"] + df["answer_text"]) / (df["answer_type_x"] + df["answer_type_y"])
831
+ df.progress.fillna(0, inplace=True)
832
+ df = df.pivot_table(
833
+ index=["review", "review_name"], columns="group_anonymized", values="progress", aggfunc="mean"
834
+ )
835
+ df = df.reset_index()
836
+ df["id"] = df["review"]
837
+ return df