wbcrm 1.43.1__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.

Potentially problematic release.


This version of wbcrm might be problematic. Click here for more details.

Files changed (175) hide show
  1. wbcrm/__init__.py +1 -0
  2. wbcrm/admin/__init__.py +5 -0
  3. wbcrm/admin/accounts.py +59 -0
  4. wbcrm/admin/activities.py +102 -0
  5. wbcrm/admin/events.py +42 -0
  6. wbcrm/admin/groups.py +7 -0
  7. wbcrm/admin/products.py +8 -0
  8. wbcrm/apps.py +5 -0
  9. wbcrm/configurations/__init__.py +1 -0
  10. wbcrm/configurations/base.py +16 -0
  11. wbcrm/dynamic_preferences_registry.py +38 -0
  12. wbcrm/factories/__init__.py +14 -0
  13. wbcrm/factories/accounts.py +56 -0
  14. wbcrm/factories/activities.py +125 -0
  15. wbcrm/factories/groups.py +23 -0
  16. wbcrm/factories/products.py +10 -0
  17. wbcrm/filters/__init__.py +10 -0
  18. wbcrm/filters/accounts.py +67 -0
  19. wbcrm/filters/activities.py +199 -0
  20. wbcrm/filters/groups.py +20 -0
  21. wbcrm/filters/products.py +37 -0
  22. wbcrm/filters/signals.py +94 -0
  23. wbcrm/fixtures/wbcrm.json +1215 -0
  24. wbcrm/kpi_handlers/activities.py +171 -0
  25. wbcrm/locale/de/LC_MESSAGES/django.po +1538 -0
  26. wbcrm/migrations/0001_initial_squashed_squashed_0032_productcompanyrelationship_alter_product_prospects_and_more.py +3948 -0
  27. wbcrm/migrations/0002_alter_activity_repeat_choice.py +32 -0
  28. wbcrm/migrations/0003_remove_activity_external_id_and_more.py +63 -0
  29. wbcrm/migrations/0004_alter_activity_status.py +28 -0
  30. wbcrm/migrations/0005_account_accountrole_accountroletype_and_more.py +182 -0
  31. wbcrm/migrations/0006_alter_activity_location.py +17 -0
  32. wbcrm/migrations/0007_alter_account_status.py +23 -0
  33. wbcrm/migrations/0008_alter_activity_options.py +16 -0
  34. wbcrm/migrations/0009_alter_account_is_public.py +19 -0
  35. wbcrm/migrations/0010_alter_account_reference_id.py +17 -0
  36. wbcrm/migrations/0011_activity_summary.py +22 -0
  37. wbcrm/migrations/0012_alter_activity_summary.py +17 -0
  38. wbcrm/migrations/0013_account_action_plan_account_relationship_status_and_more.py +34 -0
  39. wbcrm/migrations/0014_alter_account_relationship_status.py +24 -0
  40. wbcrm/migrations/0015_alter_activity_type.py +23 -0
  41. wbcrm/migrations/0016_auto_20241205_1015.py +106 -0
  42. wbcrm/migrations/0017_event.py +40 -0
  43. wbcrm/migrations/__init__.py +0 -0
  44. wbcrm/models/__init__.py +5 -0
  45. wbcrm/models/accounts.py +637 -0
  46. wbcrm/models/activities.py +1341 -0
  47. wbcrm/models/events.py +12 -0
  48. wbcrm/models/groups.py +118 -0
  49. wbcrm/models/llm/activity_summaries.py +33 -0
  50. wbcrm/models/llm/analyze_relationship.py +54 -0
  51. wbcrm/models/products.py +83 -0
  52. wbcrm/models/recurrence.py +279 -0
  53. wbcrm/preferences.py +14 -0
  54. wbcrm/report/activity_report.py +110 -0
  55. wbcrm/serializers/__init__.py +23 -0
  56. wbcrm/serializers/accounts.py +126 -0
  57. wbcrm/serializers/activities.py +526 -0
  58. wbcrm/serializers/groups.py +30 -0
  59. wbcrm/serializers/products.py +57 -0
  60. wbcrm/serializers/recurrence.py +90 -0
  61. wbcrm/serializers/signals.py +70 -0
  62. wbcrm/static/wbcrm/markdown/documentation/activity.md +86 -0
  63. wbcrm/static/wbcrm/markdown/documentation/activitytype.md +20 -0
  64. wbcrm/static/wbcrm/markdown/documentation/group.md +2 -0
  65. wbcrm/static/wbcrm/markdown/documentation/product.md +11 -0
  66. wbcrm/synchronization/__init__.py +0 -0
  67. wbcrm/synchronization/activity/__init__.py +0 -0
  68. wbcrm/synchronization/activity/admin.py +72 -0
  69. wbcrm/synchronization/activity/backend.py +213 -0
  70. wbcrm/synchronization/activity/backends/__init__.py +0 -0
  71. wbcrm/synchronization/activity/backends/google/__init__.py +2 -0
  72. wbcrm/synchronization/activity/backends/google/google_calendar_backend.py +399 -0
  73. wbcrm/synchronization/activity/backends/google/request_utils/__init__.py +16 -0
  74. wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/create.py +75 -0
  75. wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/delete.py +78 -0
  76. wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/update.py +155 -0
  77. wbcrm/synchronization/activity/backends/google/request_utils/internal_to_external/update.py +180 -0
  78. wbcrm/synchronization/activity/backends/google/tasks.py +21 -0
  79. wbcrm/synchronization/activity/backends/google/tests/__init__.py +0 -0
  80. wbcrm/synchronization/activity/backends/google/tests/conftest.py +1 -0
  81. wbcrm/synchronization/activity/backends/google/tests/test_data.py +81 -0
  82. wbcrm/synchronization/activity/backends/google/tests/test_google_backend.py +319 -0
  83. wbcrm/synchronization/activity/backends/google/tests/test_utils.py +274 -0
  84. wbcrm/synchronization/activity/backends/google/typing_informations.py +139 -0
  85. wbcrm/synchronization/activity/backends/google/utils.py +216 -0
  86. wbcrm/synchronization/activity/backends/outlook/__init__.py +0 -0
  87. wbcrm/synchronization/activity/backends/outlook/backend.py +585 -0
  88. wbcrm/synchronization/activity/backends/outlook/msgraph.py +438 -0
  89. wbcrm/synchronization/activity/backends/outlook/parser.py +431 -0
  90. wbcrm/synchronization/activity/backends/outlook/tests/__init__.py +0 -0
  91. wbcrm/synchronization/activity/backends/outlook/tests/conftest.py +1 -0
  92. wbcrm/synchronization/activity/backends/outlook/tests/fixtures.py +606 -0
  93. wbcrm/synchronization/activity/backends/outlook/tests/test_admin.py +117 -0
  94. wbcrm/synchronization/activity/backends/outlook/tests/test_backend.py +273 -0
  95. wbcrm/synchronization/activity/backends/outlook/tests/test_controller.py +248 -0
  96. wbcrm/synchronization/activity/backends/outlook/tests/test_parser.py +173 -0
  97. wbcrm/synchronization/activity/controller.py +624 -0
  98. wbcrm/synchronization/activity/dynamic_preferences_registry.py +119 -0
  99. wbcrm/synchronization/activity/preferences.py +27 -0
  100. wbcrm/synchronization/activity/shortcuts.py +10 -0
  101. wbcrm/synchronization/activity/tasks.py +21 -0
  102. wbcrm/synchronization/activity/urls.py +6 -0
  103. wbcrm/synchronization/activity/utils.py +46 -0
  104. wbcrm/synchronization/activity/views.py +40 -0
  105. wbcrm/synchronization/admin.py +1 -0
  106. wbcrm/synchronization/apps.py +14 -0
  107. wbcrm/synchronization/dynamic_preferences_registry.py +1 -0
  108. wbcrm/synchronization/management.py +36 -0
  109. wbcrm/synchronization/tasks.py +1 -0
  110. wbcrm/synchronization/urls.py +5 -0
  111. wbcrm/tasks.py +312 -0
  112. wbcrm/templates/email/activity.html +98 -0
  113. wbcrm/templates/email/activity_report.html +6 -0
  114. wbcrm/templates/email/daily_summary.html +72 -0
  115. wbcrm/templates/email/global_daily_summary.html +85 -0
  116. wbcrm/tests/__init__.py +0 -0
  117. wbcrm/tests/accounts/__init__.py +0 -0
  118. wbcrm/tests/accounts/test_models.py +380 -0
  119. wbcrm/tests/accounts/test_viewsets.py +87 -0
  120. wbcrm/tests/conftest.py +76 -0
  121. wbcrm/tests/disable_signals.py +52 -0
  122. wbcrm/tests/e2e/__init__.py +1 -0
  123. wbcrm/tests/e2e/e2e_wbcrm_utility.py +82 -0
  124. wbcrm/tests/e2e/test_e2e.py +369 -0
  125. wbcrm/tests/test_assignee_methods.py +39 -0
  126. wbcrm/tests/test_chartviewsets.py +111 -0
  127. wbcrm/tests/test_dto.py +63 -0
  128. wbcrm/tests/test_filters.py +51 -0
  129. wbcrm/tests/test_models.py +216 -0
  130. wbcrm/tests/test_recurrence.py +291 -0
  131. wbcrm/tests/test_report.py +20 -0
  132. wbcrm/tests/test_serializers.py +170 -0
  133. wbcrm/tests/test_tasks.py +94 -0
  134. wbcrm/tests/test_viewsets.py +967 -0
  135. wbcrm/tests/tests.py +120 -0
  136. wbcrm/typings.py +109 -0
  137. wbcrm/urls.py +67 -0
  138. wbcrm/viewsets/__init__.py +22 -0
  139. wbcrm/viewsets/accounts.py +121 -0
  140. wbcrm/viewsets/activities.py +344 -0
  141. wbcrm/viewsets/buttons/__init__.py +7 -0
  142. wbcrm/viewsets/buttons/accounts.py +27 -0
  143. wbcrm/viewsets/buttons/activities.py +88 -0
  144. wbcrm/viewsets/buttons/signals.py +17 -0
  145. wbcrm/viewsets/display/__init__.py +12 -0
  146. wbcrm/viewsets/display/accounts.py +110 -0
  147. wbcrm/viewsets/display/activities.py +443 -0
  148. wbcrm/viewsets/display/groups.py +22 -0
  149. wbcrm/viewsets/display/products.py +105 -0
  150. wbcrm/viewsets/endpoints/__init__.py +8 -0
  151. wbcrm/viewsets/endpoints/accounts.py +32 -0
  152. wbcrm/viewsets/endpoints/activities.py +30 -0
  153. wbcrm/viewsets/endpoints/groups.py +7 -0
  154. wbcrm/viewsets/endpoints/products.py +9 -0
  155. wbcrm/viewsets/groups.py +37 -0
  156. wbcrm/viewsets/menu/__init__.py +8 -0
  157. wbcrm/viewsets/menu/accounts.py +18 -0
  158. wbcrm/viewsets/menu/activities.py +61 -0
  159. wbcrm/viewsets/menu/groups.py +16 -0
  160. wbcrm/viewsets/menu/products.py +20 -0
  161. wbcrm/viewsets/mixins.py +34 -0
  162. wbcrm/viewsets/previews/__init__.py +1 -0
  163. wbcrm/viewsets/previews/activities.py +10 -0
  164. wbcrm/viewsets/products.py +56 -0
  165. wbcrm/viewsets/recurrence.py +26 -0
  166. wbcrm/viewsets/titles/__init__.py +13 -0
  167. wbcrm/viewsets/titles/accounts.py +22 -0
  168. wbcrm/viewsets/titles/activities.py +61 -0
  169. wbcrm/viewsets/titles/products.py +13 -0
  170. wbcrm/viewsets/titles/utils.py +46 -0
  171. wbcrm/workflows/__init__.py +1 -0
  172. wbcrm/workflows/assignee_methods.py +25 -0
  173. wbcrm-1.43.1.dist-info/METADATA +11 -0
  174. wbcrm-1.43.1.dist-info/RECORD +175 -0
  175. wbcrm-1.43.1.dist-info/WHEEL +5 -0
@@ -0,0 +1,117 @@
1
+ from unittest.mock import patch
2
+
3
+ import pytest
4
+ from django.contrib.admin import AdminSite
5
+ from django.contrib.auth import get_user_model
6
+ from django.contrib.messages import get_messages
7
+ from dynamic_preferences.registries import global_preferences_registry
8
+ from rest_framework import status
9
+ from wbcore.contrib.authentication.factories import UserFactory
10
+ from wbcrm.synchronization.activity.admin import UserSyncAdmin
11
+
12
+ from .fixtures import TestOutlookSyncFixture, msgraph_fixture
13
+
14
+
15
+ @pytest.mark.parametrize(
16
+ "backend, credentials, subscription, tenant_id",
17
+ [
18
+ (False, True, {"id": "fake_id"}, "fake_tenant_id"),
19
+ (True, False, None, None),
20
+ (True, True, None, None),
21
+ (True, True, {"id": "fake_id"}, None),
22
+ (True, True, {"id": "fake_id"}, "fake_tenant_id"),
23
+ ],
24
+ )
25
+ @patch("wbcrm.synchronization.activity.backends.outlook.backend.MicrosoftGraphAPI")
26
+ @pytest.mark.django_db
27
+ class TestAdminUserWebhook(TestOutlookSyncFixture):
28
+ def _initialiation(self, mock_msgraph, backend, credentials, subscription, tenant_id, credentials_fixture):
29
+ preferences = global_preferences_registry.manager()
30
+ preferences["wbactivity_sync__sync_backend_calendar"] = preferences[
31
+ "wbactivity_sync__outlook_sync_credentials"
32
+ ] = ""
33
+ if backend:
34
+ preferences[
35
+ "wbactivity_sync__sync_backend_calendar"
36
+ ] = "wbcrm.synchronization.activity.backends.outlook.backend.OutlookSyncBackend"
37
+ mock_msgraph.return_value.status_code = status.HTTP_200_OK
38
+ mock_msgraph.return_value = msgraph_fixture()
39
+ if credentials:
40
+ preferences["wbactivity_sync__outlook_sync_credentials"] = credentials_fixture
41
+ msgraph_fixture._subscription = subscription
42
+ msgraph_fixture.tenant_id = tenant_id
43
+ else:
44
+ mock_msgraph.side_effect = AssertionError("Invalid URL")
45
+ user = UserFactory(is_superuser=True)
46
+ if subscription:
47
+ user.metadata = {"outlook": {"subscription": subscription}}
48
+ user.save()
49
+ return user
50
+
51
+ def test_set_web_hook(
52
+ self, mock_msgraph, backend, credentials, subscription, tenant_id, fixture_request, credentials_fixture
53
+ ):
54
+ user = self._initialiation(mock_msgraph, backend, credentials, subscription, tenant_id, credentials_fixture)
55
+ fixture_request.user = user
56
+ assert len([m.message for m in get_messages(fixture_request)]) == 0
57
+ user_admin = UserSyncAdmin(UserFactory, AdminSite())
58
+ user_admin.set_web_hook(fixture_request, get_user_model().objects.filter(id=user.id))
59
+ messages = [m.message for m in get_messages(fixture_request)]
60
+ assert len(messages) == 1
61
+ if backend:
62
+ if credentials:
63
+ if subscription or tenant_id:
64
+ assert "Operation completed" in messages[0]
65
+ else:
66
+ assert messages[0] == f"Operation Failed, Outlook TenantId not found for: {user}"
67
+ else:
68
+ assert "Operation Failed, Invalid URL" in messages[0]
69
+ else:
70
+ assert messages[0] == "Operation Failed, No backend set in preferences"
71
+
72
+ def test_stop_web_hook(
73
+ self, mock_msgraph, backend, credentials, subscription, tenant_id, fixture_request, credentials_fixture
74
+ ):
75
+ user = self._initialiation(mock_msgraph, backend, credentials, subscription, tenant_id, credentials_fixture)
76
+ fixture_request.user = user
77
+ assert len([m.message for m in get_messages(fixture_request)]) == 0
78
+ user_admin = UserSyncAdmin(UserFactory, AdminSite())
79
+ user_admin.stop_web_hook(fixture_request, get_user_model().objects.filter(id=user.id))
80
+ messages = [m.message for m in get_messages(fixture_request)]
81
+ assert len(messages) == 1
82
+ if backend:
83
+ if subscription:
84
+ if credentials:
85
+ assert "Operation completed" in messages[0]
86
+ else:
87
+ assert "Operation Failed, Invalid URL" in messages[0]
88
+ else:
89
+ assert f"Operation Failed, {user} has no active webhook"
90
+ else:
91
+ assert messages[0] == "Operation Failed, No backend set in preferences"
92
+
93
+ def test_check_web_hook(
94
+ self, mock_msgraph, backend, credentials, subscription, tenant_id, fixture_request, credentials_fixture
95
+ ):
96
+ user = self._initialiation(mock_msgraph, backend, credentials, subscription, tenant_id, credentials_fixture)
97
+ fixture_request.user = user
98
+ assert len([m.message for m in get_messages(fixture_request)]) == 0
99
+ user_admin = UserSyncAdmin(UserFactory, AdminSite())
100
+ user_admin.check_web_hook(fixture_request, get_user_model().objects.filter(id=user.id))
101
+ messages = [m.message for m in get_messages(fixture_request)]
102
+ assert len(messages) == 1
103
+ if backend:
104
+ if credentials:
105
+ if subscription:
106
+ assert "Operation completed" in messages[0]
107
+ elif tenant_id:
108
+ assert (
109
+ "Operation Failed, Webhook not found. Number of subscriptions found in outlook for"
110
+ in messages[0]
111
+ )
112
+ else:
113
+ assert messages[0] == f"Operation Failed, Webhook not found. TenantId not found for {user}"
114
+ else:
115
+ assert "Operation Failed, Invalid URL" in messages[0]
116
+ else:
117
+ assert messages[0] == "Operation Failed, No backend set in preferences"
@@ -0,0 +1,273 @@
1
+ import json
2
+ from unittest.mock import patch
3
+
4
+ import pytest
5
+ from dynamic_preferences.registries import global_preferences_registry
6
+ from rest_framework.test import APIRequestFactory
7
+ from wbcore.contrib.authentication.factories import UserFactory
8
+ from wbcrm.models.activities import Activity
9
+ from wbcrm.synchronization.activity.backends.outlook.backend import OutlookSyncBackend
10
+
11
+ from .fixtures import TestOutlookSyncFixture, msgraph_fixture
12
+
13
+
14
+ @pytest.mark.django_db
15
+ class TestOutlookSyncBackend(TestOutlookSyncFixture):
16
+ backend = OutlookSyncBackend()
17
+
18
+ def test_attribute(self):
19
+ assert OutlookSyncBackend.METADATA_KEY == "outlook"
20
+
21
+ def test_open(self):
22
+ preferences = global_preferences_registry.manager()
23
+ preferences["wbactivity_sync__outlook_sync_credentials"] = ""
24
+ assert hasattr(self.backend, "msgraph") is False
25
+ self.backend.open()
26
+ assert self.backend.msgraph
27
+
28
+ @pytest.mark.parametrize("type_request", [None, "validationToken", "admin_consent"])
29
+ def test_validation_response(self, type_request):
30
+ request1 = APIRequestFactory().get("")
31
+ if type_request:
32
+ request1.GET = request1.GET.copy()
33
+ request1.GET[type_request] = "fake_info"
34
+ assert self.backend._validation_response(request1).content.decode("UTF-8") == "fake_info"
35
+ else:
36
+ assert self.backend._validation_response(request1) is None
37
+
38
+ @pytest.mark.parametrize("client_state", [False, True])
39
+ def test_is_inbound_request_valid(
40
+ self, client_state, notification_created_fixture, notification_call_record_fixture
41
+ ):
42
+ api_factory = APIRequestFactory()
43
+ request1 = api_factory.post("", data={})
44
+ request2 = api_factory.post(
45
+ "", data=json.dumps({"value": [notification_call_record_fixture]}), content_type="application/json"
46
+ )
47
+ request3 = api_factory.post(
48
+ "", data=json.dumps({"value": [notification_created_fixture]}), content_type="application/json"
49
+ )
50
+ preferences = global_preferences_registry.manager()
51
+ preferences["wbactivity_sync__outlook_sync_client_state"] = ""
52
+ if client_state:
53
+ preferences["wbactivity_sync__outlook_sync_client_state"] = notification_created_fixture["client_state"]
54
+
55
+ assert self.backend._is_inbound_request_valid(request1) is False
56
+ assert self.backend._is_inbound_request_valid(request2) is False
57
+ assert self.backend._is_inbound_request_valid(request3) == client_state
58
+
59
+ @pytest.mark.parametrize("event_found, is_organizer", [(False, True), (True, True), (True, False)])
60
+ @patch("wbcrm.synchronization.activity.backends.outlook.backend.MicrosoftGraphAPI")
61
+ def test_get_events_from_request(
62
+ self,
63
+ mock_msgraph,
64
+ event_found,
65
+ is_organizer,
66
+ notification_created_fixture,
67
+ organizer_event_fixture_parsed,
68
+ invitation_event_fixture_parsed,
69
+ ):
70
+ api_factory = APIRequestFactory()
71
+ request1 = api_factory.post(
72
+ "", data=json.dumps({"value": [notification_created_fixture]}), content_type="application/json"
73
+ )
74
+ self.backend.open()
75
+ self.backend.msgraph = msgraph_fixture()
76
+ if event_found:
77
+ if is_organizer:
78
+ msgraph_fixture.event = organizer_event_fixture_parsed
79
+ msgraph_fixture.tenant_id = "fake_tenant_id"
80
+ else:
81
+ msgraph_fixture.event = invitation_event_fixture_parsed
82
+ msgraph_fixture.event_by_uid = organizer_event_fixture_parsed
83
+ events = self.backend._get_events_from_request(request1)
84
+ self.backend.close()
85
+ assert len(events) == 1
86
+ if event_found:
87
+ assert {"change_type", "resource", "subscription_id"}.issubset(set(events[0].keys()))
88
+ assert events[0].get("id")
89
+ else:
90
+ assert {"change_type", "resource", "subscription_id"} == set(events[0].keys())
91
+
92
+ def test_deserialize(self, organizer_event_fixture_parsed, organizer_master_event_fixture_parsed):
93
+ self.backend.open()
94
+ activity_dto0, is_deleted0, user_dto0 = self.backend._deserialize(
95
+ organizer_event_fixture_parsed, include_metadata=False
96
+ )
97
+ activity_dto, is_deleted, user_dto = self.backend._deserialize(organizer_event_fixture_parsed)
98
+ activity_dto2, is_deleted2, user_dto2 = self.backend._deserialize(organizer_master_event_fixture_parsed)
99
+ self.backend.close()
100
+
101
+ assert is_deleted0 == is_deleted == is_deleted2 is False
102
+ assert user_dto0 == user_dto == user_dto2
103
+ assert user_dto0.metadata == {"outlook": {"subscription": {"id": None}}}
104
+ assert activity_dto0.metadata == {}
105
+ assert activity_dto.metadata == {
106
+ self.backend.METADATA_KEY: {
107
+ "event_uid": organizer_event_fixture_parsed["uid"],
108
+ "event_id": organizer_event_fixture_parsed["id"],
109
+ }
110
+ }
111
+ assert activity_dto2.metadata == {
112
+ self.backend.METADATA_KEY: {
113
+ "event_uid": organizer_master_event_fixture_parsed["uid"],
114
+ "event_id": organizer_master_event_fixture_parsed["id"],
115
+ }
116
+ }
117
+ assert activity_dto.period
118
+ assert activity_dto.repeat_choice == "NEVER"
119
+ assert activity_dto2.repeat_choice != "NEVER"
120
+
121
+ def test_serialize(self, activity_factory):
122
+ activity_dto = activity_factory(preceded_by=None)._build_dto()
123
+ act_dto = activity_factory(
124
+ repeat_choice=Activity.ReoccuranceChoice.DAILY, recurrence_count=3, preceded_by=None
125
+ )._build_dto()
126
+ activity_dict = self.backend._serialize(activity_dto)
127
+ activity_dict1 = self.backend._serialize(activity_dto, created=True)
128
+ act_dict = self.backend._serialize(act_dto)
129
+ act_dict1 = self.backend._serialize(act_dto, created=True)
130
+ keys = {
131
+ "subject",
132
+ "start",
133
+ "end",
134
+ "body",
135
+ "attendees",
136
+ "sensitivity",
137
+ "isReminderOn",
138
+ "reminderMinutesBeforeStart",
139
+ "isAllDay",
140
+ "responseRequested",
141
+ "location",
142
+ "locations",
143
+ }
144
+ assert set(activity_dict.keys()) == set(act_dict.keys()) == set(activity_dict1.keys()) == keys
145
+ assert set(act_dict1.keys()) == keys.union({"recurrence"})
146
+
147
+ @pytest.mark.parametrize(
148
+ "metadata, master_event",
149
+ [
150
+ ({}, False),
151
+ ({}, True),
152
+ ({"organizer_resource": "fake_resource"}, False),
153
+ ({"organizer_resource": "fake_resource"}, True),
154
+ ({"event_id": "fake_event_id"}, False),
155
+ ({"event_id": "fake_event_id"}, True),
156
+ ({"event_uid": "fake_event_uid"}, False),
157
+ ({"event_uid": "fake_event_uid"}, True),
158
+ ({"occurrence_resource": "fake_resource"}, False),
159
+ ({"occurrence_resource": "fake_resource"}, True),
160
+ ({"occurrence_id": "fake_occurrence_id"}, False),
161
+ ({"occurrence_id": "fake_occurrence_id"}, True),
162
+ ],
163
+ )
164
+ def test_get_external_event(self, metadata, master_event, activity_factory):
165
+ activity_dto = activity_factory(preceded_by=None)._build_dto()
166
+ activity_dto2 = activity_factory(preceded_by=None, metadata={self.backend.METADATA_KEY: metadata})._build_dto()
167
+ self.backend.open()
168
+ self.backend.msgraph = msgraph_fixture()
169
+ msgraph_fixture.tenant_id = "fake_tenant_id"
170
+ msgraph_fixture.event_by_uid = msgraph_fixture.event = {"id": "event_id"}
171
+ assert (
172
+ self.backend.get_external_event(activity_dto, master_event)
173
+ == self.backend.get_external_event(activity_dto, master_event)
174
+ is None
175
+ )
176
+ if metadata:
177
+ event_result = self.backend.get_external_event(activity_dto2, master_event)
178
+ if master_event or not activity_dto2.is_recurrent:
179
+ if metadata.get("event_uid") or metadata.get("event_id") or metadata.get("organizer_resource"):
180
+ assert event_result == {"id": "event_id"}
181
+ else:
182
+ assert event_result is None
183
+ else:
184
+ if metadata.get("occurrence_resource") or metadata.get("occurrence_id"):
185
+ assert event_result == {"id": "event_id"}
186
+ else:
187
+ assert event_result is None
188
+ else:
189
+ assert self.backend.get_external_event(activity_dto2, master_event) is None
190
+
191
+ def test_get_external_participants(self, activity_factory, person_factory):
192
+ person = person_factory()
193
+ activity_dto = activity_factory(preceded_by=None, participants=(person,))._build_dto()
194
+ activity_dto2 = activity_factory(
195
+ preceded_by=None,
196
+ participants=(person,),
197
+ metadata={self.backend.METADATA_KEY: {"organizer_resource": "fake_resource"}},
198
+ )._build_dto()
199
+ self.backend.open()
200
+ self.backend.msgraph = msgraph_fixture()
201
+ msgraph_fixture.tenant_id = "fake_tenant_id"
202
+ msgraph_fixture.event_by_uid = msgraph_fixture.event = {
203
+ "id": "event_id",
204
+ "attendees": [{"emailAddress": {"address": person._build_dto().email}}],
205
+ }
206
+ assert len(self.backend.get_external_participants(activity_dto, [])) == 0
207
+ assert len(self.backend.get_external_participants(activity_dto2, [])) == 1
208
+ assert len(self.backend.get_external_participants(activity_dto2, activity_dto2.participants)) == 0
209
+
210
+ @patch("wbcrm.synchronization.activity.backends.outlook.backend.MicrosoftGraphAPI")
211
+ def test_is_participant_valid(self, mock_msgraph):
212
+ user = UserFactory(is_superuser=True)
213
+ user2 = UserFactory(is_superuser=True)
214
+ user2.metadata = {self.backend.METADATA_KEY: {"subscription": {"id": "fake_subscription_id"}}}
215
+ user2.save()
216
+ msgraph_fixture._subscription = {"id": "fake_subscription_id"}
217
+ mock_msgraph.return_value = msgraph_fixture()
218
+ assert self.backend._is_participant_valid(user2) is True
219
+ assert self.backend._is_participant_valid(user) is False
220
+
221
+ def test_generate_event_metadata(self):
222
+ event = {"id": "fake_id", "uid": "fake_uid"}
223
+ occurrence_event = {"id": "fake_id", "uid": "fake_uid"}
224
+ resource = f"Users/fake_tenant_id/Events/{event['id']}"
225
+ result = {
226
+ "resources": [resource],
227
+ "organizer_resource": resource,
228
+ "event_uid": event["uid"],
229
+ "event_id": event["id"],
230
+ }
231
+ metadata = self.backend._generate_event_metadata("fake_tenant_id", event)
232
+ metadata2 = self.backend._generate_event_metadata("fake_tenant_id", event, occurrence_event)
233
+ assert metadata == {self.backend.METADATA_KEY: result}
234
+ assert metadata2 == {
235
+ self.backend.METADATA_KEY: {**result, **{"occurrence_id": event["id"], "occurrence_resource": resource}}
236
+ }
237
+
238
+ def test_get_metadata_from_event(
239
+ self, activity_factory, organizer_event_fixture_parsed, organizer_master_event_fixture_parsed
240
+ ):
241
+ self.backend.open()
242
+ self.backend.msgraph = msgraph_fixture()
243
+ msgraph_fixture.tenant_id = "fake_tenant_id"
244
+ activity_dto = activity_factory(preceded_by=None)._build_dto()
245
+ metadata_list = self.backend._get_metadata_from_event(activity_dto, organizer_event_fixture_parsed)
246
+ # self.backend._get_metadata_from_event()
247
+ resource = f"Users/fake_tenant_id/Events/{organizer_event_fixture_parsed['id']}"
248
+ assert len(metadata_list) == 1
249
+ assert metadata_list[0][0] == activity_dto
250
+ assert metadata_list[0][1] == {
251
+ self.backend.METADATA_KEY: {
252
+ "resources": [resource],
253
+ "organizer_resource": resource,
254
+ "event_uid": organizer_event_fixture_parsed["uid"],
255
+ "event_id": organizer_event_fixture_parsed["id"],
256
+ }
257
+ }
258
+
259
+ @patch("wbcrm.synchronization.activity.backends.outlook.backend.MicrosoftGraphAPI")
260
+ def test_renew_web_hooks(self, mock_msgraph):
261
+ msgraph_fixture._subscription = {"id": "fake_id_new"}
262
+ mock_msgraph.return_value = msgraph_fixture()
263
+
264
+ user1 = UserFactory(is_superuser=True)
265
+ user2 = UserFactory(is_superuser=True)
266
+ user2.metadata = {self.backend.METADATA_KEY: {"subscription": {"id": "fake_id"}}}
267
+ user2.save()
268
+
269
+ self.backend.renew_web_hooks()
270
+ user1.refresh_from_db()
271
+ user2.refresh_from_db()
272
+ assert user1.metadata == {}
273
+ assert user2.metadata == {self.backend.METADATA_KEY: {"subscription": {"id": "fake_id_new"}}}
@@ -0,0 +1,248 @@
1
+ import itertools
2
+ import json
3
+ from unittest.mock import patch
4
+
5
+ import pytest
6
+ from django.contrib.auth import get_user_model
7
+ from django.contrib.auth.models import Permission
8
+ from django.contrib.contenttypes.models import ContentType
9
+ from dynamic_preferences.registries import global_preferences_registry
10
+ from rest_framework.test import APIRequestFactory
11
+ from wbcore.permissions.registry import user_registry
12
+ from wbcrm.factories import ActivityFactory
13
+ from wbcrm.models import Activity, ActivityParticipant
14
+ from wbcrm.synchronization.activity.shortcuts import get_backend
15
+ from wbcrm.typings import User as UserDTO
16
+
17
+ from .fixtures import TestOutlookSyncFixture, msgraph_fixture
18
+
19
+ User = get_user_model()
20
+
21
+
22
+ @pytest.mark.django_db
23
+ class TestInitController:
24
+ def test_init(self):
25
+ global_preferences_registry.manager()["wbactivity_sync__sync_backend_calendar"] = ""
26
+ controller = get_backend()
27
+ assert controller is None
28
+
29
+ global_preferences_registry.manager()[
30
+ "wbactivity_sync__sync_backend_calendar"
31
+ ] = "wbcrm.synchronization.activity.backends.outlook.backend.OutlookSyncBackend"
32
+ controller2 = get_backend()
33
+ assert controller2.backend
34
+
35
+
36
+ @pytest.mark.django_db
37
+ class TestController(TestOutlookSyncFixture):
38
+ def setup_method(self):
39
+ global_preferences_registry.manager()[
40
+ "wbactivity_sync__sync_backend_calendar"
41
+ ] = "wbcrm.synchronization.activity.backends.outlook.backend.OutlookSyncBackend"
42
+
43
+ @pytest.fixture()
44
+ @patch("wbcrm.synchronization.activity.backends.outlook.backend.MicrosoftGraphAPI")
45
+ def controller(self, mock_msgraph):
46
+ controller = get_backend()
47
+ controller.backend.open()
48
+ controller.backend.msgraph = msgraph_fixture()
49
+ return controller
50
+
51
+ @pytest.mark.parametrize("type_request", [None, "validationToken", "admin_consent"])
52
+ def test_handle_inbound_validation_response(self, controller, type_request):
53
+ request1 = APIRequestFactory().get("")
54
+ if type_request:
55
+ request1.GET = request1.GET.copy()
56
+ request1.GET[type_request] = "fake_info"
57
+ assert controller.handle_inbound_validation_response(request1).content.decode("UTF-8") == "fake_info"
58
+ else:
59
+ assert controller.handle_inbound_validation_response(request1) is None
60
+
61
+ @pytest.mark.parametrize("client_state", ["secret1", "secret2"])
62
+ @patch("wbcrm.synchronization.activity.backends.outlook.backend.MicrosoftGraphAPI")
63
+ def test_get_events_from_inbound_request(
64
+ self, mock_msgraph, controller, client_state, notification_fixture, teams_event_fixture
65
+ ):
66
+ notification_fixture["client_state"] = client_state
67
+ global_preferences_registry.manager()["wbactivity_sync__outlook_sync_client_state"] = "secret2"
68
+ api_factory = APIRequestFactory()
69
+ request1 = api_factory.post("", data={})
70
+ request2 = api_factory.post(
71
+ "", data=json.dumps({"value": [notification_fixture]}), content_type="application/json"
72
+ )
73
+
74
+ mock_msgraph.return_value = controller.backend.msgraph
75
+ controller.backend.msgraph.event = teams_event_fixture
76
+ controller.backend.msgraph.event_by_uid = teams_event_fixture
77
+ events1 = controller.get_events_from_inbound_request(request1)
78
+ events2 = controller.get_events_from_inbound_request(request2)
79
+ assert events1 == []
80
+ if client_state != global_preferences_registry.manager()["wbactivity_sync__outlook_sync_client_state"]:
81
+ assert events1 == events2 == []
82
+ else:
83
+ assert events1 == []
84
+ assert len(events2) == 1
85
+ expected_result = {
86
+ "change_type": notification_fixture["change_type"],
87
+ "resource": notification_fixture["resource"],
88
+ "subscription_id": notification_fixture["subscription_id"],
89
+ "organizer_resource": notification_fixture["resource"],
90
+ **teams_event_fixture,
91
+ }
92
+ assert set(events2[0].keys()) == set(expected_result.keys())
93
+
94
+ @patch("wbcrm.synchronization.activity.backends.outlook.backend.MicrosoftGraphAPI")
95
+ def test_user_for_handle_inbound(
96
+ self, mock_msgraph, controller, notification_fixture, teams_event_fixture, user_factory
97
+ ):
98
+ event = {
99
+ "change_type": notification_fixture["change_type"],
100
+ "resource": notification_fixture["resource"],
101
+ "subscription_id": notification_fixture["subscription_id"],
102
+ **teams_event_fixture,
103
+ }
104
+ metadata = {"outlook": {"subscription": {"id": notification_fixture["subscription_id"]}}}
105
+ _, _, user_dto = controller.backend._deserialize(event)
106
+ user_result = UserDTO(metadata=metadata, id=None)
107
+ assert user_dto.id is None
108
+ assert user_dto.metadata == user_result.metadata
109
+
110
+ assert controller.get_activity_participant(user_dto) is None
111
+ user = user_factory(is_active=True, is_superuser=True, metadata=metadata)
112
+ assert controller.get_activity_participant(user_dto) == user.profile
113
+
114
+ def _get_user_and_activities(self, notification_fixture, user_factory, teams_event_fixture):
115
+ user1 = user_factory(is_active=True)
116
+ user2 = user_factory(
117
+ is_active=True, metadata={"outlook": {"subscription": {"id": notification_fixture["subscription_id"]}}}
118
+ )
119
+
120
+ other1_activity = ActivityFactory(creator=user1.profile, participants=(user2.profile,))
121
+ other2_activity = ActivityFactory(
122
+ creator=user1.profile,
123
+ participants=(user2.profile,),
124
+ metadata={"outlook": {"event_id": 1, "event_uid": 2, "resources": ["Users/1/events/1"]}},
125
+ )
126
+ metadata = {
127
+ "outlook": {
128
+ "resources": [notification_fixture["resource"]],
129
+ "organizer_resource": notification_fixture["resource"],
130
+ "event_uid": teams_event_fixture["uid"],
131
+ "event_id": teams_event_fixture["id"],
132
+ }
133
+ }
134
+ activity = ActivityFactory(creator=user1.profile, participants=(user2.profile,))
135
+ Activity.objects.filter(id=activity.id).update(metadata=metadata)
136
+ return {"activities": (other1_activity, other2_activity, activity), "users": (user1, user2)}
137
+
138
+ @pytest.mark.parametrize(
139
+ "is_internal_creator, cancel_activity, delete_notification", list(itertools.product([True, False], repeat=3))
140
+ )
141
+ def test_delete_activity(
142
+ self,
143
+ cancel_activity,
144
+ is_internal_creator,
145
+ controller,
146
+ notification_fixture,
147
+ user_factory,
148
+ teams_event_fixture,
149
+ delete_notification,
150
+ ):
151
+ global_preferences_registry.manager()["wbactivity_sync__sync_cancelled_activity"] = cancel_activity
152
+
153
+ data = self._get_user_and_activities(notification_fixture, user_factory, teams_event_fixture)
154
+ other1_activity, other2_activity, activity = data["activities"]
155
+ user1, user2 = data["users"]
156
+ if is_internal_creator:
157
+ permission = Permission.objects.get_or_create(
158
+ content_type=ContentType.objects.get_for_model(User), codename="is_internal_user"
159
+ )[0]
160
+ user1.user_permissions.add(permission)
161
+ user_registry.reset_cache()
162
+ activity.refresh_from_db()
163
+ activity_dto = activity._build_dto()
164
+ activity_dto.delete_notification = delete_notification
165
+
166
+ user1_dto = UserDTO(metadata=user1.metadata, id=None)
167
+ user2_dto = UserDTO(metadata=user2.metadata, id=None)
168
+
169
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity, activity}
170
+
171
+ controller.delete_activity(activity_dto, user1_dto)
172
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity, activity}
173
+
174
+ controller.delete_activity(
175
+ activity_dto, user2_dto
176
+ ) # user 2 is not the creator of the activity. it's a participant
177
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity, activity}
178
+ if is_internal_creator:
179
+ user2_status = (
180
+ ActivityParticipant.ParticipationStatus.CANCELLED
181
+ if delete_notification
182
+ else ActivityParticipant.ParticipationStatus.PENDING_INVITATION
183
+ )
184
+ assert activity.activity_participants.get(participant=user2.profile).participation_status == user2_status
185
+
186
+ controller.delete_activity(
187
+ activity_dto, user1_dto
188
+ ) # user 1 is the creator of the activity but doesn't have an active subscription
189
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity, activity}
190
+ user1.metadata = user2.metadata
191
+ user1.save()
192
+ user2.metadata = {}
193
+ user2.metadata = {"outlook": {"subscription": {"id": "new_subscription1"}}}
194
+ user2.save()
195
+ user1.refresh_from_db()
196
+ user1_dto = UserDTO(metadata=user1.metadata, id=None)
197
+
198
+ controller.delete_activity(
199
+ activity_dto, user1_dto
200
+ ) # user 1 is the creator of the activity with an active subscription
201
+ if delete_notification and is_internal_creator and not cancel_activity:
202
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity}
203
+ else:
204
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity, activity}
205
+ assert set(Activity.all_objects.all()) == {other1_activity, other2_activity, activity}
206
+
207
+ @pytest.mark.parametrize(
208
+ "cancel_external_creator_activity, cancel_activity, delete_notification",
209
+ list(itertools.product([True, False], repeat=3)),
210
+ )
211
+ def test_cancel_no_participant_external_creator_activity(
212
+ self,
213
+ cancel_external_creator_activity,
214
+ cancel_activity,
215
+ controller,
216
+ notification_fixture,
217
+ user_factory,
218
+ teams_event_fixture,
219
+ delete_notification,
220
+ ):
221
+ global_preferences_registry.manager()["wbactivity_sync__sync_cancelled_activity"] = cancel_activity
222
+ global_preferences_registry.manager()[
223
+ "wbactivity_sync__sync_cancelled_external_activity"
224
+ ] = cancel_external_creator_activity
225
+
226
+ data = self._get_user_and_activities(notification_fixture, user_factory, teams_event_fixture)
227
+ other1_activity, other2_activity, activity = data["activities"]
228
+ user1, user2 = data["users"]
229
+ permission = Permission.objects.get_or_create(
230
+ content_type=ContentType.objects.get_for_model(User), codename="is_internal_user"
231
+ )[0]
232
+ user2.user_permissions.add(permission)
233
+ user_registry.reset_cache()
234
+ user2_dto = UserDTO(metadata=user2.metadata, id=None)
235
+ Activity.objects.filter(id=activity.id).update(creator=user1.profile)
236
+ activity.refresh_from_db()
237
+ activity_dto = activity._build_dto()
238
+ activity_dto.delete_notification = delete_notification
239
+
240
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity, activity}
241
+
242
+ # user 1 is an external user and it's the creator of the activity without an active subscription
243
+ # user 2 is a internal participant
244
+ controller.delete_activity(activity_dto, user2_dto)
245
+ if delete_notification and cancel_external_creator_activity and not cancel_activity:
246
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity}
247
+ else:
248
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity, activity}