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