wbcore 2.2.3__py2.py3-none-any.whl → 2.2.5__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 (73) hide show
  1. wbcore/contrib/color/admin.py +28 -0
  2. wbcore/contrib/color/apps.py +5 -0
  3. wbcore/contrib/color/enums.py +17 -0
  4. wbcore/contrib/color/factories.py +10 -0
  5. wbcore/contrib/color/fields.py +29 -0
  6. wbcore/contrib/color/forms.py +13 -0
  7. wbcore/contrib/color/models.py +62 -0
  8. wbcore/contrib/color/tests/conftest.py +10 -0
  9. wbcore/contrib/color/tests/test_color_models.py +61 -0
  10. wbcore/contrib/color/tests/test_fields.py +25 -0
  11. wbcore/contrib/documents/tests/conftest.py +30 -0
  12. wbcore/contrib/documents/tests/test_models.py +144 -0
  13. wbcore/contrib/example_app/tests/test_models/test_event.py +87 -0
  14. wbcore/contrib/example_app/tests/test_models/test_match.py +210 -0
  15. wbcore/contrib/example_app/tests/test_models/test_others.py +159 -0
  16. wbcore/contrib/example_app/tests/test_serializers/test_league_serializer.py +34 -0
  17. wbcore/contrib/example_app/tests/test_serializers/test_match_serializer.py +134 -0
  18. wbcore/contrib/example_app/tests/test_serializers/test_role_serializer.py +13 -0
  19. wbcore/contrib/example_app/tests/test_serializers/test_sport_serializer.py +14 -0
  20. wbcore/contrib/example_app/tests/test_serializers/test_stadium_serializer.py +14 -0
  21. wbcore/contrib/example_app/tests/test_serializers/test_team_result_serializer.py +30 -0
  22. wbcore/contrib/example_app/tests/test_serializers/test_team_serializer.py +70 -0
  23. wbcore/contrib/example_app/tests/test_viewsets/test_event_viewset.py +162 -0
  24. wbcore/contrib/example_app/tests/test_viewsets/test_league_viewset.py +84 -0
  25. wbcore/contrib/example_app/tests/test_viewsets/test_match_viewset.py +65 -0
  26. wbcore/contrib/example_app/tests/test_viewsets/test_person_viewset.py +166 -0
  27. wbcore/contrib/example_app/tests/test_viewsets/test_role_viewset.py +75 -0
  28. wbcore/contrib/example_app/tests/test_viewsets/test_sport_viewset.py +75 -0
  29. wbcore/contrib/example_app/tests/test_viewsets/test_stadium_viewset.py +75 -0
  30. wbcore/contrib/example_app/tests/test_viewsets/test_team_viewset.py +92 -0
  31. wbcore/contrib/example_app/tests/test_viewsets/test_teamresult_viewset.py +58 -0
  32. wbcore/contrib/example_app/tests/test_viewsets/test_utils_viewsets.py +124 -0
  33. wbcore/contrib/guardian/apps.py +6 -0
  34. wbcore/contrib/guardian/configurations.py +3 -0
  35. wbcore/contrib/guardian/filters.py +21 -0
  36. wbcore/contrib/guardian/tasks.py +10 -0
  37. wbcore/contrib/guardian/urls.py +12 -0
  38. wbcore/contrib/guardian/utils.py +124 -0
  39. wbcore/contrib/notifications/viewsets/configs/notification_types.py +27 -0
  40. wbcore/contrib/notifications/viewsets/configs/notifications.py +85 -0
  41. wbcore/contrib/workflow/tests/test_models/step/test_decision_step.py +79 -0
  42. wbcore/contrib/workflow/tests/test_models/step/test_email_step.py +45 -0
  43. wbcore/contrib/workflow/tests/test_models/step/test_finish_step.py +105 -0
  44. wbcore/contrib/workflow/tests/test_models/step/test_join_step.py +127 -0
  45. wbcore/contrib/workflow/tests/test_models/step/test_script_step.py +24 -0
  46. wbcore/contrib/workflow/tests/test_models/step/test_split_step.py +49 -0
  47. wbcore/contrib/workflow/tests/test_models/step/test_step.py +621 -0
  48. wbcore/contrib/workflow/tests/test_models/step/test_user_step.py +225 -0
  49. wbcore/contrib/workflow/tests/test_models/test_condition.py +103 -0
  50. wbcore/contrib/workflow/tests/test_models/test_data.py +134 -0
  51. wbcore/contrib/workflow/tests/test_models/test_process.py +98 -0
  52. wbcore/contrib/workflow/tests/test_models/test_transition.py +128 -0
  53. wbcore/contrib/workflow/tests/test_models/test_workflow.py +358 -0
  54. wbcore/templates/forms.py +0 -0
  55. wbcore/test/e2e_helpers_methods/e2e_checks.py +121 -0
  56. wbcore/test/e2e_helpers_methods/e2e_helper_methods.py +395 -0
  57. wbcore/tests/test_permissions/test_backend.py +29 -0
  58. {wbcore-2.2.3.dist-info → wbcore-2.2.5.dist-info}/METADATA +1 -1
  59. {wbcore-2.2.3.dist-info → wbcore-2.2.5.dist-info}/RECORD +60 -16
  60. wbcore/contrib/agenda/release_notes/1_0_0.md +0 -13
  61. wbcore/contrib/authentication/release_notes/1_0_0.md +0 -13
  62. wbcore/contrib/currency/release_notes/1_0_0.md +0 -13
  63. wbcore/contrib/directory/release_notes/1_0_0.md +0 -13
  64. wbcore/contrib/directory/release_notes/1_0_1.md +0 -13
  65. wbcore/contrib/documents/release_notes/1_0_0.md +0 -13
  66. wbcore/contrib/geography/release_notes/1_0_0.md +0 -13
  67. wbcore/contrib/io/release_notes/1_0_0.md +0 -13
  68. wbcore/contrib/notifications/release_notes/1_0_0.md +0 -13
  69. wbcore/contrib/tags/release_notes/1_0_0.md +0 -13
  70. wbcore/docs/orderable.md +0 -29
  71. wbcore/docs/reparent.md +0 -13
  72. wbcore/templates/reversion/compare_detail.html +0 -19
  73. {wbcore-2.2.3.dist-info → wbcore-2.2.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,75 @@
1
+ import pytest
2
+ from django.test import Client, TestCase
3
+ from django.urls import reverse
4
+ from wbcore.contrib.authentication.factories import SuperUserFactory
5
+ from wbcore.contrib.example_app.factories import StadiumFactory
6
+ from wbcore.contrib.example_app.models import Stadium
7
+ from wbcore.contrib.example_app.serializers import StadiumModelSerializer
8
+ from wbcore.contrib.example_app.tests.test_viewsets.test_utils_viewsets import (
9
+ get_create_view,
10
+ get_delete_view,
11
+ get_detail_view,
12
+ get_partial_view,
13
+ get_update_view,
14
+ )
15
+ from wbcore.contrib.example_app.viewsets import StadiumModelViewSet
16
+
17
+
18
+ @pytest.mark.django_db
19
+ class TestStadiumModelViewSet(TestCase):
20
+ def setUp(self):
21
+ self.user = SuperUserFactory()
22
+ self.client = Client()
23
+ self.client.force_login(user=self.user)
24
+ self.list_url = reverse("example_app:stadium-list")
25
+ self.detail_url_str = "example_app:stadium-detail"
26
+
27
+ def test_list_view(self):
28
+ response = self.client.get(self.list_url)
29
+ self.assertEqual(response.status_code, 200)
30
+
31
+ def test_create_view(self):
32
+ stadium = StadiumFactory()
33
+ response = get_create_view(self.client, stadium, self.user, self.list_url, StadiumModelViewSet)
34
+ self.assertEqual(response.status_code, 201)
35
+ self.assertTrue(Stadium.objects.filter(name=stadium.name).exists())
36
+
37
+ def test_detail_view(self):
38
+ instance = StadiumFactory()
39
+ response = get_detail_view(self.client, instance.pk, self.detail_url_str)
40
+ self.assertEqual(response.status_code, 200)
41
+ self.assertEqual(response.data["instance"]["name"], instance.name)
42
+
43
+ def test_update_view(self):
44
+ instance = StadiumFactory()
45
+ instance.name = "Updated Instance"
46
+ response = get_update_view(self.client, instance, StadiumModelSerializer, self.detail_url_str)
47
+ instance.refresh_from_db()
48
+ self.assertEqual(response.status_code, 200)
49
+ self.assertEqual(response.data["instance"]["name"], instance.name)
50
+
51
+ def test_partial_update_view(self):
52
+ instance = StadiumFactory()
53
+ response = get_partial_view(self.client, instance.id, {"name": "Updated Instance"}, self.detail_url_str)
54
+ instance.refresh_from_db()
55
+ self.assertEqual(response.status_code, 200)
56
+ self.assertEqual(response.data["instance"]["name"], instance.name)
57
+
58
+ def test_delete_view(self):
59
+ instance = StadiumFactory()
60
+ response = get_delete_view(self.client, self.detail_url_str, instance.pk)
61
+ self.assertEqual(response.status_code, 204)
62
+ self.assertFalse(Stadium.objects.filter(pk=instance.pk).exists())
63
+
64
+ def test_ordering_fields(self):
65
+ first_name, second_name, third_name = "Stadium A", "Stadium B", "Stadium C"
66
+ StadiumFactory(name=second_name)
67
+ StadiumFactory(name=first_name)
68
+ StadiumFactory(name=third_name)
69
+
70
+ response = self.client.get(self.list_url)
71
+ self.assertEqual(response.status_code, 200)
72
+ self.assertEqual(response.data["count"], Stadium.objects.count())
73
+ self.assertEqual(response.data["results"][0]["name"], first_name)
74
+ self.assertEqual(response.data["results"][1]["name"], second_name)
75
+ self.assertEqual(response.data["results"][2]["name"], third_name)
@@ -0,0 +1,92 @@
1
+ import pytest
2
+ from django.test import Client, TestCase
3
+ from django.urls import reverse
4
+ from wbcore.contrib.authentication.factories import SuperUserFactory
5
+ from wbcore.contrib.example_app.factories import TeamFactory
6
+ from wbcore.contrib.example_app.models import Team
7
+ from wbcore.contrib.example_app.serializers import TeamModelSerializer
8
+ from wbcore.contrib.example_app.tests.test_viewsets.test_utils_viewsets import ( # get_update_view,
9
+ find_instances_in_response,
10
+ get_create_view,
11
+ get_delete_view,
12
+ get_detail_view,
13
+ get_partial_view,
14
+ get_update_view,
15
+ )
16
+ from wbcore.contrib.example_app.viewsets import TeamModelViewSet
17
+
18
+
19
+ @pytest.mark.django_db
20
+ class TestTeamModelViewSet(TestCase):
21
+ def setUp(self):
22
+ self.user = SuperUserFactory()
23
+ self.client = Client()
24
+ self.client.force_login(user=self.user)
25
+ self.list_url = reverse("example_app:team-list")
26
+ self.detail_url_str = "example_app:team-detail"
27
+
28
+ def test_list_view(self):
29
+ response = self.client.get(self.list_url)
30
+ self.assertEqual(response.status_code, 200)
31
+
32
+ def test_create_view(self):
33
+ team = TeamFactory()
34
+ response = get_create_view(self.client, team, self.user, self.list_url, TeamModelViewSet)
35
+ self.assertEqual(response.status_code, 201)
36
+ self.assertTrue(Team.objects.filter(name=team.name).exists())
37
+
38
+ def test_detail_view(self):
39
+ team = TeamFactory()
40
+ response = get_detail_view(self.client, team.pk, self.detail_url_str)
41
+ self.assertEqual(response.status_code, 200)
42
+ self.assertEqual(response.data["instance"]["name"], team.name)
43
+
44
+ def test_update_view(self):
45
+ instance = TeamFactory()
46
+ instance.name = "Updated Instance"
47
+ response = get_update_view(self.client, instance, TeamModelSerializer, self.detail_url_str)
48
+ self.assertEqual(response.status_code, 200)
49
+ self.assertEqual(response.data["instance"]["name"], instance.name)
50
+
51
+ def test_partial_update_view(self):
52
+ instance = TeamFactory()
53
+ response = get_partial_view(self.client, instance.id, {"name": "Updated Instance"}, self.detail_url_str)
54
+ instance.refresh_from_db()
55
+ self.assertEqual(response.status_code, 200)
56
+ self.assertEqual(response.data["instance"]["name"], instance.name)
57
+
58
+ def test_delete_view(self):
59
+ team = TeamFactory()
60
+ response = get_delete_view(self.client, self.detail_url_str, team.pk)
61
+ self.assertEqual(response.status_code, 204)
62
+ self.assertFalse(Team.objects.filter(pk=team.pk).exists())
63
+
64
+ def test_ordering_fields(self):
65
+ team_a = TeamFactory(name="BBB", order=1)
66
+ team_b = TeamFactory(name="AAA", order=0)
67
+ team_c = TeamFactory(name="CCC", order=2)
68
+
69
+ response = self.client.get(self.list_url)
70
+ self.assertEqual(response.status_code, 200)
71
+ self.assertEqual(response.data["count"], Team.objects.count())
72
+ self.assertEqual(response.data["results"][0]["id"], team_b.id)
73
+ self.assertEqual(response.data["results"][1]["id"], team_a.id)
74
+ self.assertEqual(response.data["results"][2]["id"], team_c.id)
75
+
76
+ def test_team_stadium(self):
77
+ team_a = TeamFactory()
78
+ team_b = TeamFactory(home_stadium=team_a.home_stadium)
79
+ team_c = TeamFactory()
80
+ expected_number_of_teams = Team.objects.filter(home_stadium=team_a.home_stadium).count()
81
+ team_stadium_url = reverse("example_app:team-stadium-list", args=[team_a.home_stadium.id])
82
+ response = self.client.get(team_stadium_url)
83
+ team_a_found, team_b_found, team_c_found = find_instances_in_response([team_a, team_b, team_c], response)
84
+ self.assertEqual(
85
+ response.data["count"],
86
+ expected_number_of_teams,
87
+ f"The answer should contain {expected_number_of_teams} teams",
88
+ )
89
+ self.assertEqual(response.status_code, 200)
90
+ self.assertTrue(team_a_found, "Team A was not found in Response")
91
+ self.assertTrue(team_b_found, "Team B was not found in Response")
92
+ self.assertFalse(team_c_found, "Team C was found in Response, but should not be found")
@@ -0,0 +1,58 @@
1
+ import pytest
2
+ from django.test import Client, TestCase
3
+ from django.urls import reverse
4
+ from wbcore.contrib.authentication.factories import SuperUserFactory
5
+ from wbcore.contrib.example_app.factories import TeamResultsFactory
6
+ from wbcore.contrib.example_app.serializers import TeamResultsModelSerializer
7
+ from wbcore.contrib.example_app.tests.test_viewsets.test_utils_viewsets import (
8
+ get_create_view,
9
+ get_delete_view,
10
+ get_detail_view,
11
+ get_partial_view,
12
+ get_update_view,
13
+ )
14
+ from wbcore.contrib.example_app.viewsets import TeamResultsModelViewSet
15
+
16
+
17
+ @pytest.mark.django_db
18
+ class TestTeamResultsModelViewSet(TestCase):
19
+ def setUp(self):
20
+ self.user = SuperUserFactory()
21
+ self.client = Client()
22
+ self.client.force_login(user=self.user)
23
+ self.list_url = reverse("example_app:teamresults-list")
24
+ self.detail_url_str = "example_app:teamresults-detail"
25
+
26
+ def test_list_view(self):
27
+ response = self.client.get(self.list_url)
28
+ self.assertEqual(response.status_code, 200)
29
+
30
+ def test_create_view(self):
31
+ team_result = TeamResultsFactory()
32
+ response = get_create_view(self.client, team_result, self.user, self.list_url, TeamResultsModelViewSet)
33
+ # It is not possible to create an team results, since the get_endpoint_url returns None.
34
+ self.assertEqual(response.status_code, 405)
35
+
36
+ def test_detail_view(self):
37
+ instance = TeamResultsFactory()
38
+ response = get_detail_view(self.client, instance.pk, self.detail_url_str)
39
+ self.assertEqual(response.status_code, 200)
40
+ self.assertEqual(response.data["instance"]["id"], instance.id)
41
+
42
+ def test_update_view(self):
43
+ instance = TeamResultsFactory()
44
+ instance.points = 5
45
+ response = get_update_view(self.client, instance, TeamResultsModelSerializer, self.detail_url_str)
46
+ # It is not possible to update an team results, since the get_endpoint_url returns None.
47
+ self.assertEqual(response.status_code, 405)
48
+
49
+ def test_partial_update_view(self):
50
+ instance = TeamResultsFactory()
51
+ response = get_partial_view(self.client, instance.id, {"points": 5}, self.detail_url_str)
52
+ self.assertEqual(response.status_code, 405)
53
+
54
+ def test_delete_view(self):
55
+ instance = TeamResultsFactory()
56
+ response = get_delete_view(self.client, self.detail_url_str, instance.pk)
57
+ # It is not possible to delete an team results, since the get_endpoint_url returns None.
58
+ self.assertEqual(response.status_code, 405)
@@ -0,0 +1,124 @@
1
+ from django.db.models import Model
2
+ from django.http import HttpResponse
3
+ from django.test import Client
4
+ from django.urls import reverse
5
+ from wbcore.contrib.authentication.models import User
6
+ from wbcore.serializers import Serializer
7
+ from wbcore.test.utils import get_data_from_factory
8
+ from wbcore.viewsets import ModelViewSet
9
+
10
+
11
+ def get_create_view(
12
+ client: Client,
13
+ instance: Model,
14
+ superuser: User,
15
+ url: str,
16
+ viewset: ModelViewSet,
17
+ ) -> HttpResponse:
18
+ """
19
+ Create a new instance through a view using the Django REST Framework client.
20
+
21
+ Parameters:
22
+ - client (django.test.Client): The Django test client.
23
+ - instance (django.db.models.Model): The model instance to be created.
24
+ - superuser (django.contrib.auth.models.User): The superuser for authentication.
25
+ - url (str): The URL endpoint for the view.
26
+ - viewset (wbcore.viewsets.ModelViewSet): The viewset for the model.
27
+
28
+ Returns:
29
+ django.http.HttpResponse: The HTTP response from the view.
30
+ """
31
+ instance_data = get_data_from_factory(instance, viewset, delete=True, superuser=superuser)
32
+ return client.post(url, instance_data)
33
+
34
+
35
+ def get_detail_view(client: Client, pk: int, url: str) -> HttpResponse:
36
+ """
37
+ Retrieve the details of a model instance through a detail view using the Django REST Framework client.
38
+
39
+ Parameters:
40
+ - client (django.test.Client): The Django test client.
41
+ - pk (int): The primary key of the model instance to retrieve.
42
+ - url (str): The base URL endpoint for the detail view.
43
+
44
+ Returns:
45
+ django.http.HttpResponse: The HTTP response from the detail view.
46
+ """
47
+ detail_url = reverse(url, args=[pk])
48
+ return client.get(detail_url)
49
+
50
+
51
+ def get_update_view(client: Client, instance: Model, serializer: Serializer, url: str) -> HttpResponse:
52
+ """
53
+ Update a model instance through an update view using the Django REST Framework client.
54
+
55
+ Parameters:
56
+ - client (django.test.Client): The Django test client.
57
+ - instance (django.db.models.Model): The model instance to be updated.
58
+ - serializer (Serializer): The serializer used to serialize the instance.
59
+ - url (str): The base URL endpoint for the update view.
60
+
61
+ Returns:
62
+ django.http.HttpResponse: The HTTP response from the update view.
63
+ """
64
+ update_url = reverse(url, args=[instance.pk])
65
+ serialized_data = serializer(instance).data
66
+ return client.put(update_url, data=serialized_data, content_type="application/json")
67
+
68
+
69
+ def get_partial_view(client: Client, instance_id: int, data: dict, url: str) -> HttpResponse:
70
+ """
71
+ Facilitates partial updates to a specific instance of a model using the Django REST Framework client.
72
+
73
+ Parameters:
74
+ - client (django.test.Client): The Django test client.
75
+ - instance_id (int): The unique identifier (primary key) of the instance to be partially updated.
76
+ - data (dict): A dictionary containing the fields and their respective new values that need to be updated in the instance.
77
+ - url (str): The base URL endpoint for the patch view.
78
+ Returns:
79
+ django.http.HttpResponse: The HTTP response from the patch view.
80
+ """
81
+ update_url = reverse(url, args=[instance_id])
82
+ return client.patch(update_url, data=data, content_type="application/json")
83
+
84
+
85
+ def get_delete_view(client: Client, url: str, pk: int) -> HttpResponse:
86
+ """
87
+ Delete a model instance through a delete view using the Django REST Framework client.
88
+
89
+ Parameters:
90
+ - client (django.test.Client): The Django test client.
91
+ - url (str): The base URL endpoint for the delete view.
92
+ - pk (int): The primary key of the model instance to delete.
93
+
94
+ Returns:
95
+ django.http.HttpResponse: The HTTP response from the delete view.
96
+ """
97
+ delete_url = reverse(url, args=[pk])
98
+ return client.delete(delete_url)
99
+
100
+
101
+ def find_instances_in_response(instances: list[Model], response: HttpResponse) -> tuple:
102
+ """
103
+ Find instances in the data contained in an HTTP response.
104
+
105
+ This method takes a list of instances and an HTTP response object, and it checks if each instance
106
+ is present in the data of the response based on their unique identifier (e.g., 'id').
107
+
108
+ Parameters:
109
+ - instances (list): A list of instances (models) to search for in the response data.
110
+ - response (HttpResponse): An HTTP response object containing data to search within.
111
+
112
+ Returns:
113
+ tuple: A tuple of Boolean values indicating whether each instance was found in the response.
114
+ Each element in the tuple corresponds to an instance in the same order as in the 'instances' list.
115
+ If an instance is found, the corresponding value is True; otherwise, it is False.
116
+ """
117
+ found_instances = [False] * len(instances)
118
+
119
+ for item in response.data["results"]:
120
+ for index, instance in enumerate(instances):
121
+ if item.get("id") == instance.id:
122
+ found_instances[index] = True
123
+
124
+ return tuple(found_instances)
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class GuardianAppConfig(AppConfig):
5
+ name = "wbcore.contrib.guardian"
6
+ label = "wbcore_guardian"
@@ -0,0 +1,3 @@
1
+ class Guardian:
2
+ GUARDIAN_USER_OBJ_PERMS_MODEL = "wbcore_guardian.UserObjectPermission"
3
+ GUARDIAN_GROUP_OBJ_PERMS_MODEL = "wbcore_guardian.GroupObjectPermission"
@@ -0,0 +1,21 @@
1
+ from rest_framework.filters import BaseFilterBackend
2
+
3
+
4
+ class ObjectPermissionsFilter(BaseFilterBackend):
5
+ """
6
+ A filter backend that limits results to those where the requesting user
7
+ has read object level permissions.
8
+ """
9
+
10
+ def filter_queryset(self, request, queryset, view):
11
+ from guardian.shortcuts import get_objects_for_user
12
+ from wbcore.contrib.guardian.models.mixins import PermissionObjectModelMixin
13
+
14
+ model_class = queryset.model
15
+ if issubclass(model_class, PermissionObjectModelMixin):
16
+ user = request.user
17
+
18
+ return get_objects_for_user(
19
+ user, [model_class.view_perm_str], queryset, **model_class.guardian_shortcut_kwargs
20
+ )
21
+ return queryset
@@ -0,0 +1,10 @@
1
+ from celery import shared_task
2
+ from wbcore.contrib.guardian.models.mixins import PermissionObjectModelMixin
3
+ from wbcore.utils.itertools import get_inheriting_subclasses
4
+
5
+
6
+ @shared_task
7
+ def reload_permissions_as_task(prune_existing: bool | None = True, force_pruning: bool | None = False):
8
+ for subclass in get_inheriting_subclasses(PermissionObjectModelMixin):
9
+ for instance in subclass.objects.iterator():
10
+ instance.reload_permissions(prune_existing=prune_existing, force_pruning=force_pruning)
@@ -0,0 +1,12 @@
1
+ from django.urls import include, path
2
+ from wbcore.contrib.guardian.viewsets import PivotUserObjectPermissionModelViewSet
3
+ from wbcore.routers import WBCoreRouter
4
+
5
+ router = WBCoreRouter()
6
+ router.register(
7
+ r"pivoteduserobjectpermission", PivotUserObjectPermissionModelViewSet, basename="pivoteduserobjectpermission"
8
+ )
9
+
10
+ urlpatterns = [
11
+ path("<int:content_type_id>/<int:object_pk>/", include(router.urls)),
12
+ ]
@@ -0,0 +1,124 @@
1
+ from contextlib import suppress
2
+ from datetime import datetime
3
+ from typing import TYPE_CHECKING, Iterable, Iterator
4
+
5
+ from django.contrib.contenttypes.models import ContentType
6
+ from django.db import ProgrammingError
7
+ from django.db.models import Model, Q, QuerySet
8
+ from django.utils import timezone
9
+ from guardian.shortcuts import assign_perm, get_anonymous_user
10
+ from psycopg.errors import InvalidCursorName
11
+ from wbcore.contrib.authentication.models import User
12
+ from wbcore.contrib.guardian.models.models import UserObjectPermission
13
+ from wbcore.permissions.shortcuts import get_internal_users
14
+
15
+ if TYPE_CHECKING:
16
+ from wbcore.contrib.guardian.models.mixins import PermissionObjectModelMixin
17
+
18
+
19
+ def assign_permissions(permissions_map: Iterable[tuple[str, Model, User, bool]]):
20
+ """
21
+ Assigns object-level permissions to users based on the provided permissions map.
22
+
23
+ This method iterates through the permissions_map, assigns the specified permission
24
+ to the user for the model instance, sets the 'editable' flag for the permission,
25
+ and saves the object permission.
26
+ """
27
+ for permission, instance, user, editable in permissions_map:
28
+ object_permissions = assign_perm(permission, user, instance)
29
+
30
+ if object_permissions is None:
31
+ continue
32
+
33
+ if isinstance(object_permissions, Model):
34
+ object_permissions = [object_permissions]
35
+
36
+ for object_permission in object_permissions:
37
+ object_permission.editable = editable # type: ignore -- We have our custom Permission class here
38
+ object_permission.save()
39
+
40
+
41
+ def get_public_user_or_group(only_internal: bool = False) -> QuerySet[User]:
42
+ """
43
+ Retrieves a queryset of active users, optionally filtering to internal users only.
44
+
45
+ This method fetches users based on the `only_internal` flag.
46
+ If set, it filters for internal users, otherwise returns all users.
47
+ The queryset includes active users or the anonymous user and excludes superusers
48
+ from the main set, while later ensuring their inclusion through a union operation.
49
+ """
50
+ users = User.objects.filter(is_active=True)
51
+ if only_internal:
52
+ users = users.filter(
53
+ Q(is_superuser=True) | Q(id=get_anonymous_user().pk) | Q(id__in=get_internal_users().values("id"))
54
+ )
55
+ return users
56
+
57
+
58
+ def get_permission_matrix(
59
+ queryset: QuerySet,
60
+ created: datetime | None = None,
61
+ instance: Model | None = None,
62
+ user: User | None = None,
63
+ ) -> Iterator[tuple[str, Model, User, bool]]:
64
+ """
65
+ Retrieves the permission matrix for all (user, object) pairs
66
+
67
+ If an instance is provided, the queryset is filtered to that specific object. The method determines the
68
+ appropriate set of users based on the permission type of the object (private or public).
69
+
70
+ For each user, the function yields a tuple containing the permission string, the object instance,
71
+ the user, and whether the permission is editable.
72
+ """
73
+
74
+ if instance:
75
+ queryset = queryset.filter(id=instance.id) # type: ignore
76
+
77
+ for _instance in queryset.all():
78
+ if _instance.permission_type is _instance.PermissionType.PUBLIC:
79
+ users = get_public_user_or_group()
80
+ else:
81
+ users = get_public_user_or_group(only_internal=True)
82
+ if user:
83
+ users = users.filter(id=user.id)
84
+ for user in users:
85
+ for permission, editable in _instance.get_permissions_for_user(user, created=created).items():
86
+ yield permission, _instance, user, editable
87
+
88
+
89
+ def prune_permissions(instance: "PermissionObjectModelMixin", force: bool | None = False):
90
+ queryset = UserObjectPermission.objects.filter(
91
+ content_type=ContentType.objects.get_for_model(instance), object_pk=instance.id
92
+ )
93
+ if not force:
94
+ queryset = queryset.exclude(editable=True)
95
+ for permission in queryset:
96
+ permission.delete()
97
+
98
+
99
+ def reload_permissions(
100
+ queryset: QuerySet,
101
+ created: datetime | None = None,
102
+ user: User | None = None,
103
+ instance: "PermissionObjectModelMixin | None" = None,
104
+ prune_existing: bool = True,
105
+ force_pruning: bool = False,
106
+ ):
107
+ """
108
+ Assigns permissions based on a given queryset, with options to prune existing permissions and
109
+ specify the creation timestamp. If no creation time is provided, the current time is used.
110
+
111
+ The function first checks if existing permissions should be pruned, which happens if both
112
+ `prune_existing` and `instance` are provided. It then retrieves the permission matrix
113
+ using `get_permission_matrix()` and assigns the appropriate permissions.
114
+
115
+ Error handling is in place to suppress database-related errors, such as `ProgrammingError`
116
+ and `InvalidCursorName`, which can occur due to unmanaged tables.
117
+ """
118
+ if not created:
119
+ created = timezone.now()
120
+ with suppress(ProgrammingError, InvalidCursorName): # We check this to catch error trigger by unmanaged table
121
+ if prune_existing and instance:
122
+ prune_permissions(instance, force=force_pruning)
123
+ permission_matrix = get_permission_matrix(queryset, created=created, instance=instance, user=user)
124
+ assign_permissions(permission_matrix)
@@ -0,0 +1,27 @@
1
+ from wbcore.metadata.configs.display import Field, ListDisplay
2
+ from wbcore.metadata.configs.display.instance_display import Display
3
+ from wbcore.metadata.configs.display.instance_display.shortcuts import (
4
+ create_simple_display,
5
+ )
6
+ from wbcore.metadata.configs.display.view_config import DisplayViewConfig
7
+
8
+
9
+ class NotificationTypeSettingDisplayConfig(DisplayViewConfig):
10
+ def get_list_display(self) -> ListDisplay:
11
+ return ListDisplay(
12
+ fields=[
13
+ Field(key="notification_type", label="Notification"),
14
+ Field(key="help_text", label="Help Text"),
15
+ Field(key="enable_web", label="Web"),
16
+ Field(key="enable_mobile", label="Mobile"),
17
+ Field(key="enable_email", label="E-Mail"),
18
+ ],
19
+ )
20
+
21
+ def get_instance_display(self) -> Display:
22
+ return create_simple_display(
23
+ [
24
+ ["notification_type", "notification_type", "notification_type"],
25
+ ["enable_web", "enable_mobile", "enable_email"],
26
+ ]
27
+ )
@@ -0,0 +1,85 @@
1
+ from django.utils.translation import gettext as _
2
+ from rest_framework.reverse import reverse
3
+ from wbcore.contrib.icons.icons import WBIcon
4
+ from wbcore.metadata.configs.buttons.buttons import (
5
+ ActionButton,
6
+ HyperlinkButton,
7
+ RequestType,
8
+ WidgetButton,
9
+ )
10
+ from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
11
+ from wbcore.metadata.configs.display import Field, ListDisplay
12
+ from wbcore.metadata.configs.display.instance_display import Display
13
+ from wbcore.metadata.configs.display.instance_display.shortcuts import (
14
+ create_simple_display,
15
+ )
16
+ from wbcore.metadata.configs.display.view_config import DisplayViewConfig
17
+
18
+
19
+ class NotificationButtonConfig(ButtonViewConfig):
20
+ def get_custom_buttons(self) -> set[ActionButton]:
21
+ if not self.view.kwargs.get("pk", None):
22
+ return {
23
+ ActionButton(
24
+ weight=0,
25
+ method=RequestType.PATCH,
26
+ action_label=_("Reading all notifications"),
27
+ endpoint=reverse("wbcore:notifications:notification-read-all", request=self.request),
28
+ description_fields=_("Do you want to mark notifications as read?"),
29
+ label=_("Mark all read"),
30
+ icon=WBIcon.VIEW.icon,
31
+ identifiers=["notifications:notification"],
32
+ ),
33
+ ActionButton(
34
+ weight=1,
35
+ method=RequestType.PATCH,
36
+ action_label=_("Deleting all read notifications"),
37
+ endpoint=reverse("wbcore:notifications:notification-delete-all-read", request=self.request),
38
+ description_fields=_("Do you want delete all read notifications?"),
39
+ label=_("Delete all read"),
40
+ icon=WBIcon.DELETE.icon,
41
+ identifiers=["notifications:notification"],
42
+ ),
43
+ }
44
+ return set()
45
+
46
+ def get_custom_instance_buttons(self) -> set[WidgetButton]:
47
+ return {
48
+ WidgetButton(
49
+ title=_("Open Resource"),
50
+ label=_("Open Resource"),
51
+ icon=WBIcon.LINK.icon,
52
+ key="open_internal_resource",
53
+ ),
54
+ HyperlinkButton(
55
+ title=_("Open Resource"),
56
+ label=_("Open Resource"),
57
+ icon=WBIcon.LINK.icon,
58
+ key="open_external_resource",
59
+ ),
60
+ }
61
+
62
+ def get_custom_list_instance_buttons(self) -> set[WidgetButton]:
63
+ return self.get_custom_instance_buttons()
64
+
65
+
66
+ class NotificationDisplayConfig(DisplayViewConfig):
67
+ def get_list_display(self) -> ListDisplay:
68
+ return ListDisplay(
69
+ fields=[
70
+ Field(key="notification_type", label="Type"),
71
+ Field(key="title", label="Title"),
72
+ Field(key="body", label="body"),
73
+ Field(key="sent", label="Sent"),
74
+ Field(key="read", label="Read"),
75
+ ],
76
+ )
77
+
78
+ def get_instance_display(self) -> Display:
79
+ return create_simple_display(
80
+ [
81
+ ["notification_type", ".", "."],
82
+ ["title", "sent", "read"],
83
+ ["body", "body", "body"],
84
+ ]
85
+ )