wbcrm 2.2.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.
- wbcrm/__init__.py +1 -0
- wbcrm/admin/__init__.py +4 -0
- wbcrm/admin/accounts.py +59 -0
- wbcrm/admin/activities.py +101 -0
- wbcrm/admin/groups.py +7 -0
- wbcrm/admin/products.py +8 -0
- wbcrm/apps.py +5 -0
- wbcrm/configurations/__init__.py +1 -0
- wbcrm/configurations/base.py +16 -0
- wbcrm/dynamic_preferences_registry.py +38 -0
- wbcrm/factories/__init__.py +14 -0
- wbcrm/factories/accounts.py +56 -0
- wbcrm/factories/activities.py +125 -0
- wbcrm/factories/groups.py +23 -0
- wbcrm/factories/products.py +10 -0
- wbcrm/filters/__init__.py +10 -0
- wbcrm/filters/accounts.py +67 -0
- wbcrm/filters/activities.py +181 -0
- wbcrm/filters/groups.py +20 -0
- wbcrm/filters/products.py +37 -0
- wbcrm/filters/signals.py +94 -0
- wbcrm/migrations/0001_initial_squashed_squashed_0032_productcompanyrelationship_alter_product_prospects_and_more.py +3948 -0
- wbcrm/migrations/0002_alter_activity_repeat_choice.py +32 -0
- wbcrm/migrations/0003_remove_activity_external_id_and_more.py +63 -0
- wbcrm/migrations/0004_alter_activity_status.py +28 -0
- wbcrm/migrations/0005_account_accountrole_accountroletype_and_more.py +182 -0
- wbcrm/migrations/0006_alter_activity_location.py +17 -0
- wbcrm/migrations/0007_alter_account_status.py +23 -0
- wbcrm/migrations/0008_alter_activity_options.py +16 -0
- wbcrm/migrations/0009_alter_account_is_public.py +19 -0
- wbcrm/migrations/0010_alter_account_reference_id.py +17 -0
- wbcrm/migrations/0011_activity_summary.py +22 -0
- wbcrm/migrations/0012_alter_activity_summary.py +17 -0
- wbcrm/migrations/0013_account_action_plan_account_relationship_status_and_more.py +34 -0
- wbcrm/migrations/0014_alter_account_relationship_status.py +24 -0
- wbcrm/migrations/0015_alter_activity_type.py +23 -0
- wbcrm/migrations/0016_auto_20241205_1015.py +106 -0
- wbcrm/migrations/__init__.py +0 -0
- wbcrm/models/__init__.py +4 -0
- wbcrm/models/accounts.py +637 -0
- wbcrm/models/activities.py +1335 -0
- wbcrm/models/groups.py +118 -0
- wbcrm/models/products.py +83 -0
- wbcrm/models/recurrence.py +279 -0
- wbcrm/preferences.py +14 -0
- wbcrm/serializers/__init__.py +23 -0
- wbcrm/serializers/accounts.py +126 -0
- wbcrm/serializers/activities.py +526 -0
- wbcrm/serializers/groups.py +30 -0
- wbcrm/serializers/products.py +57 -0
- wbcrm/serializers/recurrence.py +90 -0
- wbcrm/serializers/signals.py +70 -0
- wbcrm/synchronization/__init__.py +0 -0
- wbcrm/synchronization/activity/__init__.py +0 -0
- wbcrm/synchronization/activity/admin.py +72 -0
- wbcrm/synchronization/activity/backend.py +207 -0
- wbcrm/synchronization/activity/backends/__init__.py +0 -0
- wbcrm/synchronization/activity/backends/google/__init__.py +2 -0
- wbcrm/synchronization/activity/backends/google/google_calendar_backend.py +399 -0
- wbcrm/synchronization/activity/backends/google/request_utils/__init__.py +16 -0
- wbcrm/synchronization/activity/backends/google/tasks.py +21 -0
- wbcrm/synchronization/activity/backends/google/tests/__init__.py +0 -0
- wbcrm/synchronization/activity/backends/google/tests/conftest.py +1 -0
- wbcrm/synchronization/activity/backends/google/tests/test_data.py +81 -0
- wbcrm/synchronization/activity/backends/google/tests/test_google_backend.py +319 -0
- wbcrm/synchronization/activity/backends/google/tests/test_utils.py +274 -0
- wbcrm/synchronization/activity/backends/google/typing_informations.py +139 -0
- wbcrm/synchronization/activity/backends/google/utils.py +216 -0
- wbcrm/synchronization/activity/backends/outlook/__init__.py +0 -0
- wbcrm/synchronization/activity/backends/outlook/backend.py +576 -0
- wbcrm/synchronization/activity/backends/outlook/msgraph.py +438 -0
- wbcrm/synchronization/activity/backends/outlook/parser.py +423 -0
- wbcrm/synchronization/activity/backends/outlook/tests/__init__.py +0 -0
- wbcrm/synchronization/activity/backends/outlook/tests/conftest.py +1 -0
- wbcrm/synchronization/activity/backends/outlook/tests/fixtures.py +606 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_admin.py +117 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_backend.py +269 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_controller.py +237 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_parser.py +173 -0
- wbcrm/synchronization/activity/controller.py +545 -0
- wbcrm/synchronization/activity/dynamic_preferences_registry.py +107 -0
- wbcrm/synchronization/activity/preferences.py +21 -0
- wbcrm/synchronization/activity/shortcuts.py +9 -0
- wbcrm/synchronization/activity/signals.py +28 -0
- wbcrm/synchronization/activity/tasks.py +21 -0
- wbcrm/synchronization/activity/urls.py +6 -0
- wbcrm/synchronization/activity/utils.py +46 -0
- wbcrm/synchronization/activity/views.py +37 -0
- wbcrm/synchronization/admin.py +1 -0
- wbcrm/synchronization/apps.py +15 -0
- wbcrm/synchronization/dynamic_preferences_registry.py +1 -0
- wbcrm/synchronization/management.py +36 -0
- wbcrm/synchronization/tasks.py +1 -0
- wbcrm/synchronization/urls.py +5 -0
- wbcrm/tasks.py +312 -0
- wbcrm/tests/__init__.py +0 -0
- wbcrm/tests/accounts/__init__.py +0 -0
- wbcrm/tests/accounts/test_models.py +380 -0
- wbcrm/tests/accounts/test_viewsets.py +87 -0
- wbcrm/tests/conftest.py +76 -0
- wbcrm/tests/disable_signals.py +52 -0
- wbcrm/tests/e2e/__init__.py +1 -0
- wbcrm/tests/e2e/e2e_wbcrm_utility.py +82 -0
- wbcrm/tests/e2e/test_e2e.py +369 -0
- wbcrm/tests/test_assignee_methods.py +39 -0
- wbcrm/tests/test_chartviewsets.py +111 -0
- wbcrm/tests/test_dto.py +63 -0
- wbcrm/tests/test_filters.py +51 -0
- wbcrm/tests/test_models.py +216 -0
- wbcrm/tests/test_recurrence.py +291 -0
- wbcrm/tests/test_report.py +20 -0
- wbcrm/tests/test_serializers.py +170 -0
- wbcrm/tests/test_tasks.py +94 -0
- wbcrm/tests/test_viewsets.py +967 -0
- wbcrm/tests/tests.py +120 -0
- wbcrm/typings.py +107 -0
- wbcrm/urls.py +67 -0
- wbcrm/viewsets/__init__.py +22 -0
- wbcrm/viewsets/accounts.py +121 -0
- wbcrm/viewsets/activities.py +315 -0
- wbcrm/viewsets/buttons/__init__.py +7 -0
- wbcrm/viewsets/buttons/accounts.py +27 -0
- wbcrm/viewsets/buttons/activities.py +68 -0
- wbcrm/viewsets/buttons/signals.py +17 -0
- wbcrm/viewsets/display/__init__.py +12 -0
- wbcrm/viewsets/display/accounts.py +110 -0
- wbcrm/viewsets/display/activities.py +443 -0
- wbcrm/viewsets/display/groups.py +22 -0
- wbcrm/viewsets/display/products.py +105 -0
- wbcrm/viewsets/endpoints/__init__.py +8 -0
- wbcrm/viewsets/endpoints/accounts.py +32 -0
- wbcrm/viewsets/endpoints/activities.py +30 -0
- wbcrm/viewsets/endpoints/groups.py +7 -0
- wbcrm/viewsets/endpoints/products.py +9 -0
- wbcrm/viewsets/groups.py +37 -0
- wbcrm/viewsets/menu/__init__.py +8 -0
- wbcrm/viewsets/menu/accounts.py +18 -0
- wbcrm/viewsets/menu/activities.py +61 -0
- wbcrm/viewsets/menu/groups.py +16 -0
- wbcrm/viewsets/menu/products.py +20 -0
- wbcrm/viewsets/mixins.py +34 -0
- wbcrm/viewsets/previews/__init__.py +1 -0
- wbcrm/viewsets/previews/activities.py +10 -0
- wbcrm/viewsets/products.py +56 -0
- wbcrm/viewsets/recurrence.py +26 -0
- wbcrm/viewsets/titles/__init__.py +13 -0
- wbcrm/viewsets/titles/accounts.py +22 -0
- wbcrm/viewsets/titles/activities.py +61 -0
- wbcrm/viewsets/titles/products.py +13 -0
- wbcrm/viewsets/titles/utils.py +46 -0
- wbcrm/workflows/__init__.py +1 -0
- wbcrm/workflows/assignee_methods.py +25 -0
- wbcrm-2.2.1.dist-info/METADATA +11 -0
- wbcrm-2.2.1.dist-info/RECORD +155 -0
- wbcrm-2.2.1.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
from datetime import date, timedelta
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from django.contrib.auth import get_user_model
|
|
5
|
+
from django.contrib.auth.models import Permission
|
|
6
|
+
from faker import Faker
|
|
7
|
+
from psycopg.types.range import DateRange
|
|
8
|
+
from wbcore.contrib.directory.models import Entry
|
|
9
|
+
from wbcore.contrib.directory.signals import deactivate_profile
|
|
10
|
+
from wbcrm.models import Account, AccountRole, AccountRoleValidity
|
|
11
|
+
|
|
12
|
+
fake = Faker()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.django_db
|
|
16
|
+
class TestAccountModel:
|
|
17
|
+
def test_init(self, account):
|
|
18
|
+
assert account.id is not None
|
|
19
|
+
|
|
20
|
+
def test_account_with_owner_create_account_role(self, account_factory, entry_factory):
|
|
21
|
+
customer = entry_factory.create()
|
|
22
|
+
account = account_factory.create(owner=customer)
|
|
23
|
+
assert account.roles.filter(entry=customer, role_type__key="customer").exists()
|
|
24
|
+
|
|
25
|
+
def test_leaf_account_automatically_set_to_terminal(self, account_factory):
|
|
26
|
+
parent_account = account_factory.create()
|
|
27
|
+
|
|
28
|
+
assert parent_account.is_terminal_account is True
|
|
29
|
+
child_account1 = account_factory.create(parent=parent_account, is_terminal_account=False)
|
|
30
|
+
child_account2 = account_factory.create(parent=parent_account, is_terminal_account=False)
|
|
31
|
+
|
|
32
|
+
parent_account.refresh_from_db()
|
|
33
|
+
child_account1.refresh_from_db()
|
|
34
|
+
child_account2.refresh_from_db()
|
|
35
|
+
|
|
36
|
+
assert parent_account.is_terminal_account is False
|
|
37
|
+
assert child_account1.is_terminal_account is True
|
|
38
|
+
assert child_account2.is_terminal_account is True
|
|
39
|
+
|
|
40
|
+
child_account1.delete(no_deletion=False)
|
|
41
|
+
parent_account.refresh_from_db()
|
|
42
|
+
assert parent_account.is_terminal_account is False # because there is still another child
|
|
43
|
+
|
|
44
|
+
child_account2.delete(no_deletion=False)
|
|
45
|
+
parent_account.refresh_from_db()
|
|
46
|
+
assert parent_account.is_terminal_account is True # There is no children left so we expect it to become leaf
|
|
47
|
+
|
|
48
|
+
def test_disabled_parent_account_disabled_children(self, account_factory):
|
|
49
|
+
parent_account = account_factory.create()
|
|
50
|
+
child_account = account_factory.create(parent=parent_account)
|
|
51
|
+
assert child_account.is_active
|
|
52
|
+
assert parent_account.is_active
|
|
53
|
+
|
|
54
|
+
parent_account.is_active = False
|
|
55
|
+
parent_account.save()
|
|
56
|
+
|
|
57
|
+
child_account.refresh_from_db()
|
|
58
|
+
assert not child_account.is_active
|
|
59
|
+
|
|
60
|
+
def test_private_root_account_set_children_as_private(self, account_factory):
|
|
61
|
+
parent_account = account_factory.create(is_public=True)
|
|
62
|
+
child_account = account_factory.create(parent=parent_account, is_public=True)
|
|
63
|
+
parent_account.is_public = False
|
|
64
|
+
parent_account.save()
|
|
65
|
+
child_account.refresh_from_db()
|
|
66
|
+
assert not child_account.is_public
|
|
67
|
+
|
|
68
|
+
def test_can_manage(self, account, user):
|
|
69
|
+
assert not account.can_administrate(user)
|
|
70
|
+
|
|
71
|
+
perm = Permission.objects.get(content_type__app_label="wbcrm", codename="administrate_account")
|
|
72
|
+
user.user_permissions.add(perm)
|
|
73
|
+
|
|
74
|
+
user = get_user_model().objects.get(
|
|
75
|
+
id=user.id
|
|
76
|
+
) # we refetch user to clear perm cache, doesn't happen with refresh_from_db
|
|
77
|
+
assert account.can_administrate(user)
|
|
78
|
+
|
|
79
|
+
def test_get_inherited_roles_for_account(self, account_factory, account_role_factory):
|
|
80
|
+
parent_account = account_factory.create()
|
|
81
|
+
parent_role = account_role_factory.create(account=parent_account)
|
|
82
|
+
child_account = account_factory.create(parent=parent_account)
|
|
83
|
+
child_role = account_role_factory.create(account=child_account)
|
|
84
|
+
assert set(child_account.get_inherited_roles_for_account()) == {parent_role}
|
|
85
|
+
assert set(child_account.get_inherited_roles_for_account(include_self=True)) == {child_role, parent_role}
|
|
86
|
+
|
|
87
|
+
@pytest.mark.parametrize("validity_date,account__is_public", [(fake.date_object(), False)])
|
|
88
|
+
def test_can_see_account(self, user, account, account_role_factory, validity_date):
|
|
89
|
+
# Check user doesn't see permission on the account by default
|
|
90
|
+
assert not account.can_see_account(user, validity_date)
|
|
91
|
+
role = account_role_factory.create(account=account, entry=Entry.objects.get(id=user.profile.id))
|
|
92
|
+
# with a valid role, they can see it
|
|
93
|
+
assert account.can_see_account(user, validity_date=validity_date)
|
|
94
|
+
|
|
95
|
+
# with an unvalid role, they cannot see it
|
|
96
|
+
role.validity_set.first().timespan = DateRange(date.min, validity_date - timedelta(days=1))
|
|
97
|
+
role.validity_set.first().save()
|
|
98
|
+
assert account.can_see_account(user, validity_date=validity_date)
|
|
99
|
+
|
|
100
|
+
@pytest.mark.parametrize("account__is_public", [True, False])
|
|
101
|
+
def test_can_see_account_with_internal_employee(self, internal_user_factory, account):
|
|
102
|
+
# Check user doesn't see permission on the account by default
|
|
103
|
+
|
|
104
|
+
# with being an internal user, they can see it
|
|
105
|
+
assert account.can_see_account(internal_user_factory.create()) == account.is_public
|
|
106
|
+
|
|
107
|
+
@pytest.mark.parametrize("account__is_public", [True])
|
|
108
|
+
def test_normaluser_cannot_see_account_public_account(self, user, account):
|
|
109
|
+
assert not account.can_see_account(user)
|
|
110
|
+
|
|
111
|
+
def test_get_accounts_for_customer(self, account_factory, company_factory, employer_employee_relationship_factory):
|
|
112
|
+
owner1 = company_factory.create()
|
|
113
|
+
employee1 = employer_employee_relationship_factory.create(employer=owner1).employee
|
|
114
|
+
owner2 = company_factory.create()
|
|
115
|
+
employee2 = employer_employee_relationship_factory.create(employer=owner2).employee
|
|
116
|
+
|
|
117
|
+
parent_account = account_factory.create(owner=owner1)
|
|
118
|
+
other_parent_account = account_factory.create(owner=owner1)
|
|
119
|
+
child_account = account_factory.create(parent=parent_account, owner=owner2)
|
|
120
|
+
# test if all child account also shows in the queryset as well as the direct related account for that owner
|
|
121
|
+
assert set(Account.get_accounts_for_customer(owner1.entry_ptr)) == {
|
|
122
|
+
parent_account,
|
|
123
|
+
other_parent_account,
|
|
124
|
+
child_account,
|
|
125
|
+
}
|
|
126
|
+
assert set(Account.get_accounts_for_customer(employee1.entry_ptr)) == {
|
|
127
|
+
parent_account,
|
|
128
|
+
other_parent_account,
|
|
129
|
+
child_account,
|
|
130
|
+
}
|
|
131
|
+
assert set(Account.get_accounts_for_customer(owner2.entry_ptr)) == {child_account}
|
|
132
|
+
assert set(Account.get_accounts_for_customer(employee2.entry_ptr)) == {child_account}
|
|
133
|
+
|
|
134
|
+
def test_get_managed_accounts_for_entry(self, entry, account_factory, account_role_factory):
|
|
135
|
+
main_role = account_role_factory.create(entry=entry)
|
|
136
|
+
child_account = account_factory.create(parent=main_role.account)
|
|
137
|
+
other_account = account_factory.create() # noqa
|
|
138
|
+
other_role = account_role_factory.create() # noqa
|
|
139
|
+
assert set(Account.get_managed_accounts_for_entry(entry)) == {main_role.account, child_account}
|
|
140
|
+
|
|
141
|
+
def test_handle_user_deactivation(self, account_role_factory, person_factory):
|
|
142
|
+
profile = person_factory.create()
|
|
143
|
+
substitute_profile = person_factory.create()
|
|
144
|
+
|
|
145
|
+
valid_role = account_role_factory.create(entry=Entry.objects.get(id=profile.id))
|
|
146
|
+
unvalid_role = account_role_factory.create(
|
|
147
|
+
entry=Entry.objects.get(id=profile.id), visibility_daterange=DateRange(date.min, date.today())
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
deactivate_profile.send(AccountRole, instance=profile, substitute_profile=substitute_profile)
|
|
151
|
+
valid_role.refresh_from_db()
|
|
152
|
+
unvalid_role.refresh_from_db()
|
|
153
|
+
assert valid_role.validity_set.first().timespan == DateRange(date.min, date.today() - timedelta(days=1))
|
|
154
|
+
assert valid_role.entry.id == profile.id
|
|
155
|
+
assert unvalid_role.entry.id == profile.id
|
|
156
|
+
|
|
157
|
+
new_role = AccountRole.objects.get(entry_id=substitute_profile.id, account=valid_role.account)
|
|
158
|
+
assert new_role.validity_set.first().timespan == DateRange(date.today() - timedelta(days=1), date.max)
|
|
159
|
+
|
|
160
|
+
@pytest.mark.parametrize(
|
|
161
|
+
"validity_date,is_internal_user, is_superuser",
|
|
162
|
+
[
|
|
163
|
+
(fake.date_object(), True, True),
|
|
164
|
+
(fake.date_object(), False, True),
|
|
165
|
+
(fake.date_object(), True, False),
|
|
166
|
+
(fake.date_object(), False, False),
|
|
167
|
+
],
|
|
168
|
+
)
|
|
169
|
+
def test_filter_for_user(
|
|
170
|
+
self,
|
|
171
|
+
user_factory,
|
|
172
|
+
internal_user_factory,
|
|
173
|
+
account_factory,
|
|
174
|
+
account_role_factory,
|
|
175
|
+
validity_date,
|
|
176
|
+
is_internal_user,
|
|
177
|
+
is_superuser,
|
|
178
|
+
):
|
|
179
|
+
# We assume role logic are tested in the AccountRole manager unit test
|
|
180
|
+
# so we test here only:
|
|
181
|
+
# - chain filtering
|
|
182
|
+
# - Return account where user has a valid role
|
|
183
|
+
# - and Return public account if user is superuser or internal
|
|
184
|
+
|
|
185
|
+
if is_internal_user:
|
|
186
|
+
user = internal_user_factory.create()
|
|
187
|
+
else:
|
|
188
|
+
user = user_factory.create()
|
|
189
|
+
if is_superuser:
|
|
190
|
+
user.user_permissions.add(
|
|
191
|
+
Permission.objects.get(content_type__app_label="wbcrm", codename="administrate_account")
|
|
192
|
+
)
|
|
193
|
+
profile_entry = Entry.objects.get(id=user.profile.id)
|
|
194
|
+
public_account = account_factory.create(is_public=True)
|
|
195
|
+
private_account = account_factory.create(is_public=False) # noqa
|
|
196
|
+
valid_role = account_role_factory.create(account__is_public=fake.pybool(), entry=profile_entry)
|
|
197
|
+
children_valid_role_account = account_factory.create(
|
|
198
|
+
parent=valid_role.account, is_public=True
|
|
199
|
+
) # check that entry with role on the parent account can access the public children account
|
|
200
|
+
account_factory.create(
|
|
201
|
+
parent=valid_role.account, is_public=False
|
|
202
|
+
) # check that entry with role on the parent account cannot access private children account
|
|
203
|
+
|
|
204
|
+
unvalid_role = account_role_factory.create( # noqa
|
|
205
|
+
account__is_public=False,
|
|
206
|
+
entry=profile_entry,
|
|
207
|
+
visibility_daterange=DateRange(date.min, validity_date - timedelta(days=1)),
|
|
208
|
+
)
|
|
209
|
+
user = get_user_model().objects.get(id=user.id)
|
|
210
|
+
if is_superuser:
|
|
211
|
+
assert set(Account.objects.filter_for_user(user, validity_date)) == set(Account.objects.all())
|
|
212
|
+
elif is_internal_user:
|
|
213
|
+
assert set(Account.objects.filter_for_user(user, validity_date)) == {
|
|
214
|
+
public_account,
|
|
215
|
+
valid_role.account,
|
|
216
|
+
children_valid_role_account,
|
|
217
|
+
}
|
|
218
|
+
assert set(Account.objects.filter(id=public_account.id).filter_for_user(user, validity_date)) == {
|
|
219
|
+
public_account
|
|
220
|
+
} # basic test filter chainage
|
|
221
|
+
else:
|
|
222
|
+
assert set(Account.objects.filter_for_user(user, validity_date)) == {
|
|
223
|
+
valid_role.account,
|
|
224
|
+
children_valid_role_account,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
def test_cannot_merge_account_with_children(self, account_factory):
|
|
228
|
+
"""
|
|
229
|
+
Check that we cannot merge an account that still has children
|
|
230
|
+
"""
|
|
231
|
+
base_account = account_factory.create()
|
|
232
|
+
merged_account = account_factory.create()
|
|
233
|
+
children_merged_account = account_factory.create(parent=merged_account)
|
|
234
|
+
base_account.merge(merged_account)
|
|
235
|
+
merged_account.refresh_from_db()
|
|
236
|
+
assert merged_account
|
|
237
|
+
|
|
238
|
+
# but can merge the children into the parent account
|
|
239
|
+
merged_account.merge(children_merged_account)
|
|
240
|
+
with pytest.raises(Account.DoesNotExist):
|
|
241
|
+
children_merged_account.refresh_from_db()
|
|
242
|
+
|
|
243
|
+
def test_cannot_merge_account_on_different_trees(self, account_factory):
|
|
244
|
+
"""
|
|
245
|
+
Check that we cannot merge an account that still has children
|
|
246
|
+
"""
|
|
247
|
+
base_account = account_factory.create(parent=account_factory.create())
|
|
248
|
+
merged_account = account_factory.create(parent=account_factory.create())
|
|
249
|
+
account_factory.create(parent=merged_account)
|
|
250
|
+
base_account.merge(merged_account)
|
|
251
|
+
merged_account.refresh_from_db()
|
|
252
|
+
assert merged_account
|
|
253
|
+
|
|
254
|
+
def test_account_merging(self, account_factory, account_role_factory, user):
|
|
255
|
+
# TODO implemetns for commission
|
|
256
|
+
pivot_date = date(2023, 1, 1)
|
|
257
|
+
base_account = account_factory.create()
|
|
258
|
+
base_account_role = account_role_factory.create(
|
|
259
|
+
account=base_account, visibility_daterange=DateRange(date.min, pivot_date)
|
|
260
|
+
)
|
|
261
|
+
base_account_role_validity = base_account_role.validity_set.first()
|
|
262
|
+
|
|
263
|
+
merged_account = account_factory.create()
|
|
264
|
+
merged_account_role_overlapping = account_role_factory.create(
|
|
265
|
+
account=merged_account, entry=base_account_role.entry, visibility_daterange=DateRange(pivot_date, date.max)
|
|
266
|
+
)
|
|
267
|
+
merged_account_role_overlapping.authorized_hidden_users.add(user)
|
|
268
|
+
merged_account_role_validity = merged_account_role_overlapping.validity_set.first()
|
|
269
|
+
|
|
270
|
+
merged_account_role_different = account_role_factory.create(account=merged_account)
|
|
271
|
+
merged_account_role_different_validity = merged_account_role_different.validity_set.first()
|
|
272
|
+
|
|
273
|
+
base_account.merge(merged_account)
|
|
274
|
+
|
|
275
|
+
# test that the none overlapping role were forwarded to the base account
|
|
276
|
+
assert set(base_account.roles.all()) == {base_account_role, merged_account_role_different}
|
|
277
|
+
|
|
278
|
+
# test that the validity set for the base role got the new validity from the base merged account role
|
|
279
|
+
assert set(base_account_role.validity_set.all()) == {base_account_role_validity, merged_account_role_validity}
|
|
280
|
+
|
|
281
|
+
# test that the authorized user set on the merged account role were forwarded to the base account
|
|
282
|
+
assert set(base_account_role.authorized_hidden_users.all()) == {user}
|
|
283
|
+
|
|
284
|
+
# test that the merged account was deleted
|
|
285
|
+
with pytest.raises(Account.DoesNotExist):
|
|
286
|
+
merged_account.refresh_from_db()
|
|
287
|
+
|
|
288
|
+
# test that the non overlapping role correctly follows the role
|
|
289
|
+
role = AccountRole.objects.get(account=base_account, entry=merged_account_role_different.entry)
|
|
290
|
+
assert set(role.validity_set.all()) == {merged_account_role_different_validity}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@pytest.mark.django_db
|
|
294
|
+
class TestAccountRoleModel:
|
|
295
|
+
def test_init(self, account_role):
|
|
296
|
+
assert account_role.id is not None
|
|
297
|
+
validity = account_role.validity_set.first()
|
|
298
|
+
assert validity
|
|
299
|
+
assert validity.timespan == DateRange(date.min, date.max)
|
|
300
|
+
|
|
301
|
+
def test_hidden_role_set_account_to_private(self, account_factory, account_role_factory):
|
|
302
|
+
public_account = account_factory.create(is_public=True)
|
|
303
|
+
role = account_role_factory.create(account=public_account, is_hidden=True)
|
|
304
|
+
assert not role.account.is_public
|
|
305
|
+
|
|
306
|
+
# def test_filter_for_user(self, ):
|
|
307
|
+
# # here we want to check that the chained filtering works and
|
|
308
|
+
# # - Internal or super user can see their roles + public account roles if there are unhidden + hidden role if they have the right to see them
|
|
309
|
+
# # - Otherwise, can only see direct roles
|
|
310
|
+
# # - Check if the annotation is_currently_valid is there
|
|
311
|
+
@pytest.mark.parametrize(
|
|
312
|
+
"is_superuser,is_internal_user",
|
|
313
|
+
[
|
|
314
|
+
(True, True),
|
|
315
|
+
(True, False),
|
|
316
|
+
(False, True),
|
|
317
|
+
(False, False),
|
|
318
|
+
],
|
|
319
|
+
)
|
|
320
|
+
def test_filter_related_account_roles_for_internal_user(
|
|
321
|
+
self, user_factory, internal_user_factory, account_role_factory, is_superuser, is_internal_user
|
|
322
|
+
):
|
|
323
|
+
if is_internal_user:
|
|
324
|
+
user = internal_user_factory.create()
|
|
325
|
+
else:
|
|
326
|
+
user = user_factory.create()
|
|
327
|
+
if is_superuser:
|
|
328
|
+
user.user_permissions.add(
|
|
329
|
+
Permission.objects.get(content_type__app_label="wbcrm", codename="administrate_account")
|
|
330
|
+
)
|
|
331
|
+
role_unhidden_public_account = account_role_factory.create(is_hidden=False, account__is_public=True)
|
|
332
|
+
role_unhidden_private_account = account_role_factory.create(is_hidden=False, account__is_public=False) # noqa
|
|
333
|
+
role_hidden_public_account_without_privilege = account_role_factory.create( # noqa
|
|
334
|
+
is_hidden=True, account__is_public=True
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
role_hidden_private_account_with_privilege = account_role_factory.create(
|
|
338
|
+
is_hidden=True, account__is_public=False, authorized_hidden_users=[user]
|
|
339
|
+
)
|
|
340
|
+
role_direct = account_role_factory.create(
|
|
341
|
+
entry=Entry.objects.get(id=user.profile.id),
|
|
342
|
+
account__is_public=fake.pybool(),
|
|
343
|
+
)
|
|
344
|
+
role_indirect_private_account = account_role_factory.create(account__is_public=False) # noqa
|
|
345
|
+
|
|
346
|
+
if is_superuser:
|
|
347
|
+
assert set(AccountRole.objects.filter_for_user(user)) == set(AccountRole.objects.all())
|
|
348
|
+
elif is_internal_user:
|
|
349
|
+
assert set(AccountRole.objects.filter_for_user(user)) == {
|
|
350
|
+
role_unhidden_public_account,
|
|
351
|
+
role_hidden_private_account_with_privilege,
|
|
352
|
+
role_direct,
|
|
353
|
+
}
|
|
354
|
+
assert set(AccountRole.objects.filter(is_hidden=True).filter_for_user(user)) == {
|
|
355
|
+
role_hidden_private_account_with_privilege
|
|
356
|
+
} # Test that chain filtering works
|
|
357
|
+
else:
|
|
358
|
+
assert set(AccountRole.objects.filter_for_user(user)) == {role_direct}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@pytest.mark.django_db
|
|
362
|
+
class TestAccountRoleValidityModel:
|
|
363
|
+
@pytest.mark.parametrize("validity_date", [fake.date_object()])
|
|
364
|
+
def test_get_role_validity_subquery(self, account_role, validity_date):
|
|
365
|
+
assert AccountRole.objects.count() == 1
|
|
366
|
+
assert (
|
|
367
|
+
AccountRole.objects.annotate(is_valid=AccountRoleValidity.get_role_validity_subquery(validity_date))
|
|
368
|
+
.filter(is_valid=True)
|
|
369
|
+
.count()
|
|
370
|
+
== 1
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
validity = account_role.validity_set.first()
|
|
374
|
+
validity.timespan = DateRange(date.min, validity_date - timedelta(days=1))
|
|
375
|
+
validity.save()
|
|
376
|
+
assert (
|
|
377
|
+
not AccountRole.objects.annotate(is_valid=AccountRoleValidity.get_role_validity_subquery(validity_date))
|
|
378
|
+
.filter(is_valid=True)
|
|
379
|
+
.exists()
|
|
380
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from faker import Faker
|
|
3
|
+
from rest_framework.exceptions import AuthenticationFailed
|
|
4
|
+
from rest_framework.test import APIRequestFactory
|
|
5
|
+
from wbcore.contrib.authentication.factories.users import UserFactory
|
|
6
|
+
from wbcore.contrib.directory.models import Entry
|
|
7
|
+
from wbcrm.factories.accounts import AccountFactory, AccountRoleFactory
|
|
8
|
+
from wbcrm.models import Account
|
|
9
|
+
from wbcrm.viewsets.accounts import (
|
|
10
|
+
AccountModelViewSet,
|
|
11
|
+
AccountRepresentationViewSet,
|
|
12
|
+
AccountRoleAccountModelViewSet,
|
|
13
|
+
ChildAccountAccountModelViewSet,
|
|
14
|
+
InheritedAccountRoleAccountModelViewSet,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
fake = Faker()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.django_db
|
|
21
|
+
class TestAccountViewset:
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def account_user(self):
|
|
24
|
+
# True, we create a superuser
|
|
25
|
+
|
|
26
|
+
if fake.pybool():
|
|
27
|
+
user = UserFactory.create(is_superuser=True)
|
|
28
|
+
else:
|
|
29
|
+
user = UserFactory.create(is_superuser=True)
|
|
30
|
+
entry = Entry.objects.get(id=user.profile.id)
|
|
31
|
+
|
|
32
|
+
# Create a bunch of account and roles
|
|
33
|
+
public_account = AccountFactory.create(is_public=True)
|
|
34
|
+
AccountRoleFactory.create(account=public_account, entry=entry)
|
|
35
|
+
AccountRoleFactory.create(account=public_account)
|
|
36
|
+
private_account = AccountFactory.create(is_public=False)
|
|
37
|
+
AccountRoleFactory.create(account=private_account, entry=entry)
|
|
38
|
+
return user
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize(
|
|
41
|
+
"viewset_class",
|
|
42
|
+
[
|
|
43
|
+
AccountRepresentationViewSet,
|
|
44
|
+
AccountModelViewSet,
|
|
45
|
+
],
|
|
46
|
+
)
|
|
47
|
+
def test_ensure_permission_on_account(self, account_user, viewset_class):
|
|
48
|
+
request = APIRequestFactory().get("")
|
|
49
|
+
request.user = account_user
|
|
50
|
+
viewset = viewset_class(request=request)
|
|
51
|
+
assert set(viewset.get_queryset()) == set(Account.objects.filter_for_user(account_user))
|
|
52
|
+
|
|
53
|
+
def test_ensure_permission_for_nested_view(self, account_user, account_factory):
|
|
54
|
+
request = APIRequestFactory().get("")
|
|
55
|
+
request.user = account_user
|
|
56
|
+
parent_account = Account.objects.first()
|
|
57
|
+
account_factory.create(parent=parent_account, is_public=True)
|
|
58
|
+
viewset = ChildAccountAccountModelViewSet(request=request, kwargs={"account_id": parent_account.id})
|
|
59
|
+
|
|
60
|
+
assert set(viewset.get_queryset()) == set(
|
|
61
|
+
Account.objects.filter(parent=parent_account).filter_for_user(account_user)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def test_ensure_permission_on_account_role_account(self, user, account_factory, account_role_factory):
|
|
65
|
+
request = APIRequestFactory().get("")
|
|
66
|
+
request.user = user
|
|
67
|
+
parent_account = account_factory.create()
|
|
68
|
+
child_account = account_factory.create(parent=parent_account)
|
|
69
|
+
viewset_account_role = AccountRoleAccountModelViewSet(
|
|
70
|
+
request=request, kwargs={"account_id": parent_account.id}
|
|
71
|
+
)
|
|
72
|
+
viewset_inherited_account_role = InheritedAccountRoleAccountModelViewSet(
|
|
73
|
+
request=request, kwargs={"account_id": child_account.id}
|
|
74
|
+
)
|
|
75
|
+
with pytest.raises((AuthenticationFailed,)):
|
|
76
|
+
viewset_account_role.get_queryset()
|
|
77
|
+
with pytest.raises((AuthenticationFailed,)):
|
|
78
|
+
viewset_inherited_account_role.get_queryset()
|
|
79
|
+
|
|
80
|
+
role = account_role_factory.create(account=parent_account, entry=Entry.objects.get(id=user.profile.id))
|
|
81
|
+
account_role_factory.create(account=parent_account) # noise account role
|
|
82
|
+
assert set(viewset_account_role.get_queryset()) == {role}
|
|
83
|
+
|
|
84
|
+
account_role_factory.create(
|
|
85
|
+
account=child_account, entry=Entry.objects.get(id=user.profile.id)
|
|
86
|
+
) # noise account role
|
|
87
|
+
assert set(viewset_inherited_account_role.get_queryset()) == {role}
|
wbcrm/tests/conftest.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from wbcore.tests.conftest import * # isort:skip
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from django.db.models.signals import pre_migrate
|
|
5
|
+
from pytest_factoryboy import register
|
|
6
|
+
from wbcore.contrib.agenda.factories import CalendarItemFactory, ConferenceRoomFactory
|
|
7
|
+
from wbcore.contrib.authentication.factories import (
|
|
8
|
+
InternalUserFactory,
|
|
9
|
+
SuperUserFactory,
|
|
10
|
+
UserActivityFactory,
|
|
11
|
+
UserFactory,
|
|
12
|
+
)
|
|
13
|
+
from wbcore.contrib.directory.factories import (
|
|
14
|
+
CompanyFactory,
|
|
15
|
+
EmailContactFactory,
|
|
16
|
+
EmployerEmployeeRelationshipFactory,
|
|
17
|
+
EntryFactory,
|
|
18
|
+
PersonFactory,
|
|
19
|
+
)
|
|
20
|
+
from wbcrm.factories import (
|
|
21
|
+
AccountFactory,
|
|
22
|
+
AccountRoleFactory,
|
|
23
|
+
AccountRoleTypeFactory,
|
|
24
|
+
ActivityCompanyFactory,
|
|
25
|
+
ActivityFactory,
|
|
26
|
+
ActivityParticipantFactory,
|
|
27
|
+
ActivityPersonFactory,
|
|
28
|
+
ActivityTypeFactory,
|
|
29
|
+
GroupFactory,
|
|
30
|
+
ProductFactory,
|
|
31
|
+
RecurringActivityFactory,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
register(EntryFactory)
|
|
35
|
+
register(InternalUserFactory)
|
|
36
|
+
register(EmailContactFactory)
|
|
37
|
+
register(CompanyFactory)
|
|
38
|
+
register(PersonFactory)
|
|
39
|
+
register(EmployerEmployeeRelationshipFactory)
|
|
40
|
+
register(ActivityFactory)
|
|
41
|
+
register(ActivityTypeFactory)
|
|
42
|
+
register(RecurringActivityFactory)
|
|
43
|
+
register(ActivityCompanyFactory)
|
|
44
|
+
register(ActivityPersonFactory)
|
|
45
|
+
register(ActivityParticipantFactory)
|
|
46
|
+
register(GroupFactory)
|
|
47
|
+
register(ProductFactory)
|
|
48
|
+
register(ConferenceRoomFactory)
|
|
49
|
+
register(AccountFactory)
|
|
50
|
+
register(AccountRoleFactory)
|
|
51
|
+
register(AccountRoleTypeFactory)
|
|
52
|
+
register(CalendarItemFactory)
|
|
53
|
+
|
|
54
|
+
# Authentication
|
|
55
|
+
register(UserFactory)
|
|
56
|
+
register(SuperUserFactory, "superuser")
|
|
57
|
+
register(UserActivityFactory)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.fixture(autouse=True, scope="session")
|
|
61
|
+
def django_test_environment(django_test_environment):
|
|
62
|
+
from django.apps import apps
|
|
63
|
+
|
|
64
|
+
get_models = apps.get_models
|
|
65
|
+
|
|
66
|
+
for m in [m for m in get_models() if not m._meta.managed]:
|
|
67
|
+
m._meta.managed = True
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.fixture
|
|
71
|
+
def chrome_options(chrome_options):
|
|
72
|
+
# chrome_options.add_argument("--headless=new")
|
|
73
|
+
return chrome_options
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
pre_migrate.connect(app_pre_migration, sender=apps.get_app_config("geography"))
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import re
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
|
|
5
|
+
from django.db.models.signals import *
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DisableSignals(object):
|
|
9
|
+
def __init__(self, disabled_signals=None):
|
|
10
|
+
self.stashed_signals = defaultdict(list)
|
|
11
|
+
self.disabled_signals = disabled_signals or [
|
|
12
|
+
pre_init,
|
|
13
|
+
post_init,
|
|
14
|
+
pre_save,
|
|
15
|
+
post_save,
|
|
16
|
+
pre_delete,
|
|
17
|
+
post_delete,
|
|
18
|
+
pre_migrate,
|
|
19
|
+
post_migrate,
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
def __enter__(self):
|
|
23
|
+
for signal in self.disabled_signals:
|
|
24
|
+
self.disconnect(signal)
|
|
25
|
+
|
|
26
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
27
|
+
for signal in list(self.stashed_signals):
|
|
28
|
+
self.reconnect(signal)
|
|
29
|
+
|
|
30
|
+
def disconnect(self, signal):
|
|
31
|
+
self.stashed_signals[signal] = signal.receivers
|
|
32
|
+
signal.receivers = []
|
|
33
|
+
|
|
34
|
+
def reconnect(self, signal):
|
|
35
|
+
signal.receivers = self.stashed_signals.get(signal, [])
|
|
36
|
+
del self.stashed_signals[signal]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# https://stackoverflow.com/questions/55578230/django-how-to-visualize-signals-and-save-overrides
|
|
40
|
+
RECEIVER_MODELS = re.compile(r"sender=(\w+)\W")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DisableSignalsNotification(DisableSignals):
|
|
44
|
+
def __enter__(self):
|
|
45
|
+
for signal in self.disabled_signals:
|
|
46
|
+
if not isinstance(signal, ModelSignal):
|
|
47
|
+
continue
|
|
48
|
+
for _, receiver in signal.receivers:
|
|
49
|
+
rcode = inspect.getsource(receiver())
|
|
50
|
+
rmodel = RECEIVER_MODELS.findall(rcode)
|
|
51
|
+
if "Notification" in rmodel:
|
|
52
|
+
self.disconnect(signal)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .e2e_wbcrm_utility import create_new_activity_instance, set_up_activities
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from selenium.webdriver.remote.webdriver import WebDriver
|
|
2
|
+
from wbcore.contrib.directory.factories import PersonFactory
|
|
3
|
+
from wbcore.contrib.directory.models import Person
|
|
4
|
+
from wbcore.test import (
|
|
5
|
+
click_element_by_path,
|
|
6
|
+
fill_out_form_fields,
|
|
7
|
+
open_create_instance,
|
|
8
|
+
)
|
|
9
|
+
from wbcrm.factories import ActivityFactory, ActivityTypeFactory
|
|
10
|
+
from wbcrm.models import Activity, ActivityType
|
|
11
|
+
from wbcrm.serializers import ActivityModelSerializer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def set_up_activities(creator: Person = None):
|
|
15
|
+
"""Sets up three activities for testing purposes with fixed participants, types and titles.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
creator (Person | None, optional): The person that is used as the activities creator. Defaults to None.
|
|
19
|
+
"""
|
|
20
|
+
type_a = ActivityTypeFactory(title="Meeting")
|
|
21
|
+
type_b = ActivityTypeFactory(title="Call")
|
|
22
|
+
participant_a = PersonFactory(last_name="Turing")
|
|
23
|
+
participant_b = PersonFactory(last_name="Zuse")
|
|
24
|
+
participant_c = PersonFactory(last_name="Lovelace")
|
|
25
|
+
activity_creator = creator if creator else PersonFactory()
|
|
26
|
+
ActivityFactory(
|
|
27
|
+
title="Activity A",
|
|
28
|
+
type=type_a,
|
|
29
|
+
creator=activity_creator,
|
|
30
|
+
preceded_by=None,
|
|
31
|
+
participants=[participant_a, participant_c],
|
|
32
|
+
status=Activity.Status.PLANNED,
|
|
33
|
+
)
|
|
34
|
+
ActivityFactory(
|
|
35
|
+
title="Activity B",
|
|
36
|
+
type=type_a,
|
|
37
|
+
creator=activity_creator,
|
|
38
|
+
preceded_by=None,
|
|
39
|
+
participants=[participant_a, participant_b],
|
|
40
|
+
status=Activity.Status.FINISHED,
|
|
41
|
+
)
|
|
42
|
+
ActivityFactory(
|
|
43
|
+
title="Activity C",
|
|
44
|
+
type=type_b,
|
|
45
|
+
creator=activity_creator,
|
|
46
|
+
preceded_by=None,
|
|
47
|
+
participants=[participant_b, participant_c],
|
|
48
|
+
status=Activity.Status.PLANNED,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def create_new_activity_instance(
|
|
53
|
+
driver: WebDriver,
|
|
54
|
+
field_list: list[str],
|
|
55
|
+
type_title="",
|
|
56
|
+
activity_title="",
|
|
57
|
+
is_create_instance_open=True,
|
|
58
|
+
) -> Activity:
|
|
59
|
+
"""A function that automatically creates a new activity for selenium e2e-tests.
|
|
60
|
+
After creating the instance this function will close the create-widget.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
driver (WebDriver): The Selenium webdriver.
|
|
64
|
+
field_list (list[str]): List of fields to be filled in the creation mask. The field names must match the names in the ActivitySerializer.
|
|
65
|
+
type_title (str, optional): The title of the activity type. Defaults to "".
|
|
66
|
+
activity_title (str, optional): The title of the activity. Defaults to "".
|
|
67
|
+
is_create_instance_open (bool, optional): Should be true if the create-widget is already open. Defaults to True.
|
|
68
|
+
"""
|
|
69
|
+
if not is_create_instance_open:
|
|
70
|
+
open_create_instance(driver, "CRM", "Create Activity")
|
|
71
|
+
|
|
72
|
+
type_title = type_title if type_title else "Test Activity Type"
|
|
73
|
+
activity_title = activity_title if activity_title else "Test Activity"
|
|
74
|
+
if ActivityType.objects.filter(title=activity_title).exists():
|
|
75
|
+
activity_type = ActivityType.objects.get(title=activity_title)
|
|
76
|
+
else:
|
|
77
|
+
activity_type = ActivityTypeFactory(title=type_title)
|
|
78
|
+
activity = ActivityFactory.build(title=activity_title, type=activity_type)
|
|
79
|
+
serializer = ActivityModelSerializer(activity)
|
|
80
|
+
fill_out_form_fields(driver, serializer, field_list, activity)
|
|
81
|
+
click_element_by_path(driver, "//button[@label='Save and close']")
|
|
82
|
+
return activity
|