Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
4019815
feat: general backend pagination for delete blockers
melton-jason Jun 29, 2026
f7c94d9
Merge remote-tracking branch 'origin/v7_12_0_7_base' into issue-7515-2
melton-jason Jul 28, 2026
f821721
chore: remove whitespace in .env for consisitency
melton-jason Jul 28, 2026
383e28c
fix: incorrect lookup for role policy bulk create
melton-jason Aug 3, 2026
95ea421
Merge tag 'tags/v7.12.0.7' into issue-7515-2
melton-jason Aug 5, 2026
905537b
chore: add propper Jest links
melton-jason Sep 9, 2026
3c6eea4
feat: generalize QueryBuilder result hook
melton-jason Sep 9, 2026
6357f66
chore: fix offset in hook comment
melton-jason Sep 9, 2026
0c0afa6
Merge tag 'v7.12.1.2' into issue-7515-2
melton-jason Sep 24, 2026
3eba926
feat: keyset pagination and fast tree blockers
melton-jason Oct 3, 2026
b96ad9a
refactor: unify redis usage throughout backend
melton-jason Oct 5, 2026
fdf2b47
feat: cache nodenumber validation result
melton-jason Oct 5, 2026
5efbf79
feat: exclude parent relationship from optimized tree blockers
melton-jason Oct 6, 2026
3bcebf5
feat: include anchor and backwards options in response
melton-jason Oct 7, 2026
c453933
feat: implement LRU cache on frontend
melton-jason Oct 7, 2026
7387f3f
feat: implement a named promise queue on frontend
melton-jason Oct 7, 2026
157a2cd
feat: implement delete blocker cache on frontend
melton-jason Oct 7, 2026
44f56b3
feat: add updated delete blocker fetch functions
melton-jason Oct 7, 2026
e5d000c
feat: implement DeleteBlockerStore to manage blocker state
melton-jason Oct 7, 2026
4f431d0
fix: handle foreign key relationships from POST requests
melton-jason Oct 7, 2026
c959e02
fix: make sure to clean up pending promise queue
melton-jason Oct 7, 2026
d0f58d4
fix: improve indexing of blocker relationships
melton-jason Oct 8, 2026
35c8e80
feat: nicer external usage for delete blockers
melton-jason Oct 8, 2026
71f166d
feat: delete blocker aside
melton-jason Oct 8, 2026
90cee0d
feat: smarter blocker collection for TreeDef CASCADE relationship
melton-jason Oct 8, 2026
13303a8
chore: change placement of comment
melton-jason Oct 8, 2026
609e9c2
feat: implement pagination hook for DeleteBlockers
melton-jason Oct 8, 2026
4292cf9
fix: correct table for RecordSelector and better page navigation
melton-jason Oct 9, 2026
0a95774
fix: ensure NoneType is used when checking anchor type
melton-jason Oct 9, 2026
bf114a8
feat: define hook for slider buttons
melton-jason Oct 9, 2026
76d05e3
chore: extract DeleteBlocker hooks to own files
melton-jason Oct 9, 2026
89cca6a
feat: show delete blocker counts on aside
melton-jason Oct 9, 2026
50a331c
feat: improve styling of DeleteBlockerAside
melton-jason Oct 9, 2026
ad3570c
feat: define hook for conditional rendering on screen size
melton-jason Oct 9, 2026
238adce
feat: open paginator in dialog on small screens
melton-jason Oct 9, 2026
deb71d5
fix: ensure unload protect works in DeleteBlocker Paginator
melton-jason Oct 9, 2026
a46e196
chore: re-organize DeleteBlocker components
melton-jason Oct 9, 2026
40db22d
fix: misc. navigation bugs in Page Record Selector
melton-jason Oct 9, 2026
d27796f
feat: handle record deletion
melton-jason Oct 9, 2026
9c96b7c
feat: integrate new delete blockers with rest of frontend
melton-jason Oct 9, 2026
1ba7b32
feat: simplify DeleteBlocker output when no cascades present
melton-jason Oct 9, 2026
2f7c81f
chore: remove unused blocker count hook
melton-jason Oct 9, 2026
a982fe7
fix: resolve failing type errors
melton-jason Oct 9, 2026
50d376c
fix: race condition when showing delete confirmation
melton-jason Oct 9, 2026
a3f5b7e
Merge remote-tracking branch 'origin/main' into issue-7515-2
melton-jason Oct 9, 2026
24228a5
fix: only show paginator on large screens, explictly hide nonblocking…
melton-jason Oct 9, 2026
110d2be
fix: ensure LinkRecords and DeleteButton use same store in resource view
melton-jason Oct 9, 2026
09c62c7
feat: group record merging usages to single delete blocker store
melton-jason Oct 9, 2026
4574e3b
fix: handle case when adding no values to set
melton-jason Oct 12, 2026
7c9d769
feat: re-add delete blocked description
melton-jason Oct 12, 2026
b78c7ab
feat: define generic hook to track element size
melton-jason Oct 12, 2026
9b398e9
feat: handle delete blocker dialog resize
melton-jason Oct 12, 2026
6e1fb23
chore: add comments for DeleteBlockerAside
melton-jason Oct 12, 2026
d0d221c
fix: correcting index in page after record deletion
melton-jason Oct 12, 2026
b319d8a
fix: handle case when all records from referencing table are deleted
melton-jason Oct 12, 2026
a24e4a6
fix: import and type fixes
melton-jason Oct 13, 2026
0fb91c7
fix: ensure table count is reset to 0 when relationship count is 0
melton-jason Oct 13, 2026
98a0da9
fix: show horizontal scrollbar when paginator exceeds width
melton-jason Oct 13, 2026
cca01fd
feat: Add a View Parent button on dependent records
melton-jason Oct 13, 2026
3a00209
fix: handle records being deleted while requests are in-flight
melton-jason Oct 13, 2026
6710517
feat: make parent detection somewhat smarter
melton-jason Oct 13, 2026
cd2c62c
chore: remove unused imports
melton-jason Oct 13, 2026
970b64f
fix: cleanly handle parent record deletion
melton-jason Oct 14, 2026
42a3e6d
fix: handle deletion where parents have multiple blockers across diff…
melton-jason Oct 14, 2026
c412631
chore: add typecast in gatherDependents
melton-jason Oct 14, 2026
a4648c1
fix: incorrect complete when limit is 0
melton-jason Oct 14, 2026
ba1575d
Merge remote-tracking branch 'origin/main' into issue-7515-2
melton-jason Oct 14, 2026
365a264
feat: optimize fetching blockers for schema items when checking disci…
melton-jason Oct 15, 2026
d2d01ce
chore: update refactor comment
melton-jason Oct 15, 2026
cd7ef12
fix: correct argument name in paginator
melton-jason Oct 15, 2026
a234133
fix: implement adapater for Django fields
melton-jason Oct 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ APP_USER_PASSWORD=specify_user
# https://discourse.specifysoftware.org/t/allow-support-login-documentation/2838
ALLOW_SUPPORT_LOGIN=false
# The amount of time in seconds each token is valid for
SUPPORT_LOGIN_TTL = 180
SUPPORT_LOGIN_TTL=180

# Make sure to set the `SECRET_KEY` to a unique value
SECRET_KEY=change_this_to_some_unique_random_string
Expand Down Expand Up @@ -60,7 +60,7 @@ CELERY_RESULT_BACKEND=redis://redis/1
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = America/Chicago
TIME_ZONE=America/Chicago

# This variable controls the Specify 7 logging level. Possible values
# are:
Expand Down
12 changes: 1 addition & 11 deletions specifyweb/backend/cache/redis/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1 @@
from .store import (
set_bytes,
set_string,
get_bytes,
get_string,
add_to_set,
delete_key,
remove_from_set,
set_members,
redis_type
)
from .connect import RedisConnection, RedisSet, RedisList, RedisString
106 changes: 84 additions & 22 deletions specifyweb/backend/cache/redis/connect.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
from typing import Literal

from redis import Redis
from django.conf import settings

# https://redis.io/docs/latest/commands/type/
Redis_Type = Literal["none", "string", "list", "set", "hash", "stream", "vectorset"]

# REFACTOR: It might make more sense to move this to a setting on
# RedisConnection, rather than having it as a flag on each adapter
def _format_key(key: str):
db_name = getattr(settings, "DATABASE_NAME")
key_parts = ["specify", db_name, key]
return ":".join(key_parts)

class RedisConnection:
def __init__(self,
host=getattr(settings, "REDIS_HOST", None),
Expand All @@ -21,9 +33,16 @@ def __init__(self,
decode_responses=self.decode_responses
)

def delete(self, key: str):
def delete(self, key: str, format_key=True):
if format_key:
key = _format_key(key)
return self.connection.delete(key)

def key_type(self, key: str, format_key=True) -> Redis_Type:
if format_key:
key = _format_key(key)
return self.connection.type(key)


class RedisDataType:
def __init__(self, established: RedisConnection) -> None:
Expand All @@ -33,36 +52,55 @@ def __init__(self, established: RedisConnection) -> None:
def connection(self):
return self._established.connection

def delete(self, key: str):
return self._established.delete(key)
def delete(self, key: str, format_key=True):
return self._established.delete(key, format_key=format_key)

def key_type(self, key: str, format_key=True) -> Redis_Type:
return self._established.key_type(key, format_key=format_key)

class RedisList(RedisDataType):
"""
See https://redis.io/docs/latest/develop/data-types/lists/
"""

def left_push(self, key: str, value) -> int:
def left_push(self, key: str, value, format_key=True) -> int:
if format_key:
key = _format_key(key)
return self.connection.lpush(key, value)

def right_push(self, key: str, value) -> int:
def right_push(self, key: str, value, format_key=True) -> int:
if format_key:
key = _format_key(key)
return self.connection.rpush(key, value)

def right_pop(self, key: str) -> str | bytes | None:
def right_pop(self, key: str, format_key=True) -> str | bytes | None:
if format_key:
key = _format_key(key)
return self.connection.rpop(key)

def left_pop(self, key: str) -> str | bytes | None:
def left_pop(self, key: str, format_key=True) -> str | bytes | None:
if format_key:
key = _format_key(key)
return self.connection.lpop(key)

def length(self, key: str) -> int:
def length(self, key: str, format_key=True) -> int:
if format_key:
key = _format_key(key)
return self.connection.llen(key)

def range(self, key: str, start_index: int, end_index: int) -> list[str] | list[bytes]:
def range(self, key: str, start_index: int, end_index: int, format_key=True) -> list[str] | list[bytes]:
if format_key:
key = _format_key(key)
return self.connection.lrange(key, start_index, end_index)

def trim(self, key: str, start_index: int, end_index: int) -> list[str] | list[bytes]:
def trim(self, key: str, start_index: int, end_index: int, format_key=True) -> list[str] | list[bytes]:
if format_key:
key = _format_key(key)
return self.connection.ltrim(key, start_index, end_index)

def blocking_left_pop(self, key: str, timeout: int) -> str | bytes | None:
def blocking_left_pop(self, key: str, timeout: int, format_key=True) -> str | bytes | None:
if format_key:
key = _format_key(key)
# This will block the thread until either an item is pushed into the
# List or timeout seconds have passed
# https://redis.io/docs/latest/commands/blpop/
Expand All @@ -76,44 +114,68 @@ class RedisSet(RedisDataType):
"""
See https://redis.io/docs/latest/develop/data-types/sets/
"""
def add(self, key: str, *values: str) -> int:
def add(self, key: str, *values: str, format_key=True) -> int:
if format_key:
key = _format_key(key)
if len(values) == 0:
return 0
return self.connection.sadd(key, *values)

def is_member(self, key: str, value: str) -> bool:
def is_member(self, key: str, value: str, format_key=True) -> bool:
if format_key:
key = _format_key(key)
is_member = int(self.connection.sismember(key, value))
return is_member == 1

def remove(self, key: str, value: str):
return self.connection.srem(key, value)
def remove(self, key: str, *values: str, format_key=True):
if format_key:
key = _format_key(key)
return self.connection.srem(key, *values)

def size(self, key: str) -> int:
def size(self, key: str, format_key=True) -> int:
if format_key:
key = _format_key(key)
return self.connection.scard(key)

def members(self, key: str) -> set[str]:
def members(self, key: str, format_key=True) -> set[str]:
if format_key:
key = _format_key(key)
return self.connection.smembers(key)

def union(self, *keys: str) -> set[str]:
def union(self, *keys: str, format_keys=True) -> set[str]:
if format_keys:
keys = tuple((_format_key(key) for key in keys))
return self.connection.sunion(*keys)

def intersection(self, *keys: str) -> set[str]:
def intersection(self, *keys: str, format_keys=True) -> set[str]:
if format_keys:
keys = tuple((_format_key(key) for key in keys))
return self.connection.sinter(*keys)

def difference(self, *keys: str) -> set[str]:
def difference(self, *keys: str, format_keys=True) -> set[str]:
if format_keys:
keys = tuple((_format_key(key) for key in keys))
return self.connection.sdiff(*keys)

# REFACTOR: Make a dedicated RedisBytes class to make working with bytes more
# intuitive
class RedisString(RedisDataType):
"""
See https://redis.io/docs/latest/develop/data-types/strings/
"""

def set(self, key, value, time_to_live=None, override_existing=True):
def set(self, key, value, time_to_live=None, override_existing=True, format_key=True):
if format_key:
key = _format_key(key)
flags = {
"ex": time_to_live,
"nx": not override_existing
}
self.connection.set(key, value, **flags)

def get(self, key, delete_key=False) -> str | bytes | None:
def get(self, key, delete_key=False, format_key=True) -> str | bytes | None:
if format_key:
key = _format_key(key)
if delete_key:
return self.connection.getdel(key)
return self.connection.get(key)
61 changes: 0 additions & 61 deletions specifyweb/backend/cache/redis/rqueue.py

This file was deleted.

38 changes: 0 additions & 38 deletions specifyweb/backend/cache/redis/store.py

This file was deleted.

79 changes: 0 additions & 79 deletions specifyweb/backend/cache/redis/utils.py

This file was deleted.

Loading
Loading