Hide in-progress deletions of Project & Issue from the UI

I've done a full grep on Issue.objects, Project.objects and get_object_or_404
equivelents, and applying some common sense. The goal: avoid having
confusing/half-broken pages in the UI.

On index-usage: I've decided not to update the indexes. The assumption is:
`is_deleted` items will be a tiny minority of items in general, making the
cost/benefit analysis not turn out favorably (just scanning them out as a final
step is more efficient).  Also: sqlite is able to use the correct index without
adding a special one, proof:

```
EXPLAIN QUERY PLAN SELECT [..] WHERE ("issues_issue"."project_id" = 1 AND "issues_issue"."is_muted" = (0) AND "issues_issue"."is_resolved" = (0)) ORDER BY "issues_issue"."last_seen" DESC LIMIT 250;
QUERY PLAN
`--SEARCH issues_issue USING INDEX issue_list_open (project_id=? AND is_resolved=? AND is_muted=?)

EXPLAIN QUERY PLAN SELECT [..] WHERE ("issues_issue"."project_id" = 1 AND "issues_issue"."is_muted" = (0) AND "issues_issue"."is_resolved" = (0) AND "issues_issue"."is_deleted" = 0) ORDER BY "issues_issue"."last_seen" DESC LIMIT 250;
QUERY PLAN
`--SEARCH issues_issue USING INDEX issue_list_open (project_id=? AND is_resolved=? AND is_muted=?)
```

See #139 for the 0/1 notation in the above.

(Project-indexes: not an issue, the scale is "below relevance for indexes")
This commit is contained in:
Klaas van Schelven
2025-07-07 09:29:22 +02:00
parent 308034aadd
commit 7b340fd8ff
4 changed files with 24 additions and 18 deletions

View File

@@ -35,21 +35,24 @@ def project_list(request, ownership_filter=None):
my_memberships = ProjectMembership.objects.filter(user=request.user)
my_team_memberships = TeamMembership.objects.filter(user=request.user)
my_projects = Project.objects.filter(projectmembership__in=my_memberships).order_by('name').distinct()
my_projects = Project.objects.filter(
projectmembership__in=my_memberships, is_deleted=False).order_by('name').distinct()
my_teams_projects = \
Project.objects \
.filter(team__teammembership__in=my_team_memberships) \
.filter(team__teammembership__in=my_team_memberships, is_deleted=False) \
.exclude(projectmembership__in=my_memberships) \
.order_by('name').distinct()
if request.user.is_superuser:
# superusers can see all project, even hidden ones
other_projects = Project.objects \
.filter(is_deleted=False) \
.exclude(projectmembership__in=my_memberships) \
.exclude(team__teammembership__in=my_team_memberships) \
.order_by('name').distinct()
else:
other_projects = Project.objects \
.filter(is_deleted=False) \
.exclude(projectmembership__in=my_memberships) \
.exclude(team__teammembership__in=my_team_memberships) \
.exclude(visibility=ProjectVisibility.TEAM_MEMBERS) \
@@ -158,7 +161,7 @@ def _check_project_admin(project, user):
@atomic_for_request_method
def project_edit(request, project_pk):
project = Project.objects.get(id=project_pk)
project = Project.objects.get(id=project_pk, is_deleted=False)
_check_project_admin(project, request.user)
@@ -195,7 +198,7 @@ def project_edit(request, project_pk):
@atomic_for_request_method
def project_members(request, project_pk):
project = Project.objects.get(id=project_pk)
project = Project.objects.get(id=project_pk, is_deleted=False)
_check_project_admin(project, request.user)
if request.method == 'POST':
@@ -230,7 +233,7 @@ def project_members_invite(request, project_pk):
# NOTE: project-member invite is just that: a direct invite to a project. If you want to also/instead invite someone
# to a team, you need to just do that instead.
project = Project.objects.get(id=project_pk)
project = Project.objects.get(id=project_pk, is_deleted=False)
_check_project_admin(project, request.user)
@@ -292,7 +295,7 @@ def project_member_settings(request, project_pk, user_pk):
this_is_you = str(user_pk) == str(request.user.id)
if not this_is_you:
_check_project_admin(Project.objects.get(id=project_pk), request.user)
_check_project_admin(Project.objects.get(id=project_pk, is_deleted=False), request.user)
membership = ProjectMembership.objects.get(project=project_pk, user=user_pk)
create_form = lambda data: ProjectMembershipForm(data, instance=membership) # noqa
@@ -317,7 +320,7 @@ def project_member_settings(request, project_pk, user_pk):
return render(request, 'projects/project_member_settings.html', {
'this_is_you': this_is_you,
'user': User.objects.get(id=user_pk),
'project': Project.objects.get(id=project_pk),
'project': Project.objects.get(id=project_pk, is_deleted=False),
'form': form,
})
@@ -377,7 +380,7 @@ def project_members_accept(request, project_pk):
# invited as user B. Security-wise this is fine, but UX-wise it could be confusing. However, I'm in the assumption
# here that normal people (i.e. not me) don't have multiple accounts, so I'm not going to bother with this.
project = Project.objects.get(id=project_pk)
project = Project.objects.get(id=project_pk, is_deleted=False)
membership = ProjectMembership.objects.get(project=project, user=request.user)
if membership.accepted:
@@ -402,7 +405,7 @@ def project_members_accept(request, project_pk):
@atomic_for_request_method
def project_sdk_setup(request, project_pk, platform=""):
project = Project.objects.get(id=project_pk)
project = Project.objects.get(id=project_pk, is_deleted=False)
if not request.user.is_superuser and not ProjectMembership.objects.filter(project=project, user=request.user,
accepted=True).exists():
@@ -423,7 +426,7 @@ def project_sdk_setup(request, project_pk, platform=""):
@atomic_for_request_method
def project_alerts_setup(request, project_pk):
project = Project.objects.get(id=project_pk)
project = Project.objects.get(id=project_pk, is_deleted=False)
_check_project_admin(project, request.user)
if request.method == 'POST':
@@ -446,7 +449,7 @@ def project_alerts_setup(request, project_pk):
@atomic_for_request_method
def project_messaging_service_add(request, project_pk):
project = Project.objects.get(id=project_pk)
project = Project.objects.get(id=project_pk, is_deleted=False)
_check_project_admin(project, request.user)
if request.method == 'POST':
@@ -474,7 +477,7 @@ def project_messaging_service_add(request, project_pk):
@atomic_for_request_method
def project_messaging_service_edit(request, project_pk, service_pk):
project = Project.objects.get(id=project_pk)
project = Project.objects.get(id=project_pk, is_deleted=False)
_check_project_admin(project, request.user)
instance = project.service_configs.get(id=service_pk)