bottleneckcrm/backend/common/lookups.py
Ashwin 66421bbad0 Add ticket approval panel tests and enhance ticket board functionality
- 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.
2026-09-26 23:33:20 +05:30

63 lines
2.6 KiB
Python

"""Tenant-scoped object lookup that answers 404 instead of 500.
The `get_object(self, pk)` helper on a detail view is written the same way in a
dozen places:
return self.model.objects.get(pk=pk, org=self.request.profile.org)
which fails two ways a caller can trigger without doing anything wrong.
* A record that has been deleted, or belongs to another tenant, raises
``DoesNotExist``. Nothing catches it, so it leaves the view as a 500.
* ``id`` is a UUID column, so an id that is not a well-formed UUID raises
``django.core.exceptions.ValidationError`` (or ``ValueError``) while the query
is being built, before any row is looked at. DRF's exception handler does not
translate that class either, so it is a 500 too. A stale bookmark or a
hand-edited URL takes the endpoint down.
Both are the same answer: the caller asked for something that is not there, and
that is a 404. Returning 404 rather than 403 for a record in another tenant is
also the right disclosure: a 403 would confirm the id exists somewhere.
This mirrors ``cases.access.get_case_or_404``, which solved it for one model. It
is generic so the next detail view can use it without a fourteenth copy.
"""
from django.core.exceptions import ValidationError as DjangoValidationError
from django.http import Http404
def get_scoped_or_404(model, pk, org, **extra):
"""Return the ``model`` row with this pk inside ``org``, or raise ``Http404``.
``extra`` adds further filters (``profile=``, ``is_active=True``) for views
whose scope is narrower than the org.
"""
try:
obj = model.objects.filter(pk=pk, org=org, **extra).first()
except (DjangoValidationError, ValueError, TypeError):
raise Http404(f"No such {model._meta.verbose_name}.") from None
if obj is None:
raise Http404(f"No such {model._meta.verbose_name}.")
return obj
def get_on_visible_record_or_404(model, pk, org, visible):
"""A comment or attachment by pk, only if it hangs off a record in ``visible``.
``model`` is `Comment` or `Attachments`, one generic table per kind shared
by every module. ``visible`` is the module's read-rule queryset (for
example `visible_leads_qs(profile)`), so a comment on a record the
caller cannot open, or on another module's record, raises the same 404 as
an id that does not exist. Checking the author after an org-wide fetch
answered 403 there, which confirmed the id was real.
"""
from django.contrib.contenttypes.models import ContentType
return get_scoped_or_404(
model,
pk,
org,
content_type=ContentType.objects.get_for_model(visible.model),
object_id__in=visible.values("id"),
)