- Implement tests for ticket approval panel to verify approval request logic and visibility based on user roles. - Add functionality to handle ticket movement based on server response for both tickets and leads. - Introduce new tests for duplicate checking API to ensure correct handling of duplicates and merging logic. - Remove outdated permissions tests that are no longer relevant. - Update ticket board and lead board providers to include 'can_move' field for better control over ticket actions. - Refactor existing tests to improve coverage and ensure accurate representation of API responses.
98 lines
4.3 KiB
Python
98 lines
4.3 KiB
Python
"""Who may open a contact, and so who may link one to a record.
|
|
|
|
One definition, asked by the detail view (through ``visible_contacts_qs``, in
|
|
the lookup itself, so a hidden contact is the same 404 as a missing one) and by
|
|
the attachment download and the link checks (``has_contact_access``).
|
|
"""
|
|
|
|
from django.db.models import Q
|
|
|
|
from common.permissions import is_org_admin
|
|
from contacts.models import Contact
|
|
|
|
|
|
def contact_account_ids(contact):
|
|
"""Every account this contact is joined to, by either route."""
|
|
accounts = set(contact.account_contacts.values_list("id", flat=True))
|
|
if contact.account_id:
|
|
accounts.add(contact.account_id)
|
|
return accounts
|
|
|
|
|
|
def has_contact_access(profile, contact):
|
|
"""Who may work on this person's record.
|
|
|
|
One predicate for every verb, because the divergence was the bug. The list
|
|
filter and ``delete`` compared ``created_by`` (a User) against
|
|
``request.profile.user``, correctly. ``get``, ``put``, ``patch`` and the
|
|
comment endpoint compared it against ``request.profile`` -- a Profile is
|
|
never equal to a User, so those four branches could only ever be False.
|
|
The result a non-admin actually saw: their own contacts listed on the
|
|
index, 403 on opening any of them, and a successful delete on the same
|
|
record they had just been refused a look at.
|
|
|
|
Assignment to the account the person belongs to counts as access. Whoever
|
|
owns the company owns the conversation with the people at it, and there is
|
|
no reading under which that is true for viewing but false for editing.
|
|
|
|
A Django superuser sees every contact in their own org, as they do every
|
|
account, deal and invoice. The flag is read from ``profile.user``, the
|
|
identity the request authenticated as, so a caller without a request (the
|
|
case CSV import, through `visible_contacts_qs`) gets the same answer. Like
|
|
the admin branch, it does not check the org itself: every caller fetches
|
|
``contact`` with ``org=profile.org`` first.
|
|
"""
|
|
if is_org_admin(profile):
|
|
return True
|
|
if profile.user_id == contact.created_by_id:
|
|
return True
|
|
if profile.id in {assignee.id for assignee in contact.assigned_to.all()}:
|
|
return True
|
|
my_accounts = set(profile.account_assigned_users.values_list("id", flat=True))
|
|
return bool(my_accounts & contact_account_ids(contact))
|
|
|
|
|
|
def may_delete_contact(profile, contact):
|
|
"""Narrower than reading or editing: admins (superusers included) and the
|
|
person who entered the contact. An assignee may work on a contact but not
|
|
destroy it, which includes merging it away into another contact."""
|
|
return is_org_admin(profile) or profile.user_id == contact.created_by_id
|
|
|
|
|
|
def visible_contacts_qs(profile):
|
|
"""Contacts ``profile`` may open, the queryset form of `has_contact_access`."""
|
|
qs = Contact.objects.filter(org=profile.org)
|
|
if is_org_admin(profile):
|
|
return qs
|
|
my_accounts = profile.account_assigned_users.values_list("id", flat=True)
|
|
return qs.filter(
|
|
Q(created_by=profile.user)
|
|
| Q(assigned_to=profile)
|
|
| Q(account_contacts__id__in=my_accounts)
|
|
| Q(account_id__in=my_accounts)
|
|
).distinct()
|
|
|
|
|
|
def replace_visible_contacts(related, contact_ids, profile):
|
|
"""Set the contacts in ``related`` to ``contact_ids``, as far as
|
|
``profile`` can see them.
|
|
|
|
``related`` is a record's contacts manager (``account.contacts``,
|
|
``deal.contacts``, ``case.contacts``, ``lead.contacts``). Every write path
|
|
that links contacts by id comes through here, on create and on replace.
|
|
|
|
An id the caller may not open is dropped, not linked, and the write still
|
|
succeeds, as it does for an id from another org. Linking it would hand
|
|
the caller a copy: through account assignment it becomes theirs to open,
|
|
and the other records nest their contacts in full.
|
|
|
|
Contacts already linked that the caller may not open stay linked. The
|
|
caller's form only ever held the ones they can see, so its list says
|
|
nothing about the rest. For an admin every contact in the org is visible,
|
|
so this is a plain replace; on a new record nothing is linked yet, so it
|
|
links the visible ids.
|
|
"""
|
|
visible = visible_contacts_qs(profile)
|
|
kept = related.exclude(id__in=visible.values("id"))
|
|
wanted = visible.filter(id__in=contact_ids) if contact_ids else []
|
|
related.set([*kept, *wanted])
|