django-cachex 0.4.1__tar.gz → 0.4.2__tar.gz

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.
Files changed (88) hide show
  1. {django_cachex-0.4.1 → django_cachex-0.4.2}/PKG-INFO +2 -2
  2. {django_cachex-0.4.1 → django_cachex-0.4.2}/crates/django-cachex-redis-rs/pyproject.toml +1 -1
  3. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/cas.py +36 -0
  4. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/helpers.py +36 -31
  5. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/templates/admin/django_cachex/cache/styles.css +48 -8
  6. django_cachex-0.4.2/django_cachex/admin/templates/admin/django_cachex/key/key_detail_hash.html +73 -0
  7. django_cachex-0.4.2/django_cachex/admin/templates/admin/django_cachex/key/key_detail_list.html +114 -0
  8. django_cachex-0.4.2/django_cachex/admin/templates/admin/django_cachex/key/key_detail_set.html +79 -0
  9. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/templates/admin/django_cachex/key/key_detail_stream.html +17 -11
  10. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/templates/admin/django_cachex/key/key_detail_string.html +3 -9
  11. django_cachex-0.4.2/django_cachex/admin/templates/admin/django_cachex/key/key_detail_value_input.html +18 -0
  12. django_cachex-0.4.2/django_cachex/admin/templates/admin/django_cachex/key/key_detail_zset.html +98 -0
  13. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/views/key_detail.py +103 -2
  14. {django_cachex-0.4.1 → django_cachex-0.4.2}/pyproject.toml +2 -2
  15. django_cachex-0.4.1/django_cachex/admin/templates/admin/django_cachex/key/key_detail_hash.html +0 -101
  16. django_cachex-0.4.1/django_cachex/admin/templates/admin/django_cachex/key/key_detail_list.html +0 -127
  17. django_cachex-0.4.1/django_cachex/admin/templates/admin/django_cachex/key/key_detail_set.html +0 -57
  18. django_cachex-0.4.1/django_cachex/admin/templates/admin/django_cachex/key/key_detail_zset.html +0 -85
  19. {django_cachex-0.4.1 → django_cachex-0.4.2}/.gitignore +0 -0
  20. {django_cachex-0.4.1 → django_cachex-0.4.2}/LICENSE +0 -0
  21. {django_cachex-0.4.1 → django_cachex-0.4.2}/README.md +0 -0
  22. {django_cachex-0.4.1 → django_cachex-0.4.2}/benchmarks/README.md +0 -0
  23. {django_cachex-0.4.1 → django_cachex-0.4.2}/crates/django-cachex-redis-rs/LICENSE +0 -0
  24. {django_cachex-0.4.1 → django_cachex-0.4.2}/crates/django-cachex-redis-rs/README.md +0 -0
  25. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/__init__.py +0 -0
  26. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/adapters/__init__.py +0 -0
  27. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/adapters/_async_helpers.py +0 -0
  28. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/adapters/_pipeline_parsers.py +0 -0
  29. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/adapters/pipeline.py +0 -0
  30. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/adapters/protocols.py +0 -0
  31. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/adapters/redis_py.py +0 -0
  32. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/adapters/redis_rs.py +0 -0
  33. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/adapters/valkey_glide.py +0 -0
  34. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/adapters/valkey_py.py +0 -0
  35. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/__init__.py +0 -0
  36. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/admin.py +0 -0
  37. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/apps.py +0 -0
  38. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/models.py +0 -0
  39. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/queryset.py +0 -0
  40. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/templates/admin/django_cachex/cache/change_form.html +0 -0
  41. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/templates/admin/django_cachex/cache/change_list.html +0 -0
  42. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/templates/admin/django_cachex/key/_key_detail_actions.html +0 -0
  43. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/templates/admin/django_cachex/key/add_form.html +0 -0
  44. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/templates/admin/django_cachex/key/change_form.html +0 -0
  45. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/templates/admin/django_cachex/key/change_list.html +0 -0
  46. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/templates/admin/django_cachex/key/key_detail_pagination.html +0 -0
  47. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/views/__init__.py +0 -0
  48. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/views/base.py +0 -0
  49. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/views/cache_detail.py +0 -0
  50. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/admin/views/key_add.py +0 -0
  51. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/cache/__init__.py +0 -0
  52. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/cache/_semaphore_lua.py +0 -0
  53. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/cache/base.py +0 -0
  54. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/cache/database.py +0 -0
  55. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/cache/locmem.py +0 -0
  56. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/cache/redis_py.py +0 -0
  57. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/cache/redis_rs.py +0 -0
  58. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/cache/resp.py +0 -0
  59. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/cache/stream.py +0 -0
  60. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/cache/tiered.py +0 -0
  61. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/cache/valkey_glide.py +0 -0
  62. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/cache/valkey_py.py +0 -0
  63. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/compressors/__init__.py +0 -0
  64. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/compressors/base.py +0 -0
  65. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/compressors/gzip.py +0 -0
  66. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/compressors/lz4.py +0 -0
  67. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/compressors/lzma.py +0 -0
  68. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/compressors/zlib.py +0 -0
  69. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/compressors/zstd.py +0 -0
  70. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/exceptions.py +0 -0
  71. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/lock.py +0 -0
  72. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/py.typed +0 -0
  73. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/script.py +0 -0
  74. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/semaphore.py +0 -0
  75. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/serializers/__init__.py +0 -0
  76. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/serializers/base.py +0 -0
  77. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/serializers/json.py +0 -0
  78. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/serializers/msgpack.py +0 -0
  79. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/serializers/orjson.py +0 -0
  80. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/serializers/ormsgpack.py +0 -0
  81. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/serializers/pickle.py +0 -0
  82. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/stampede.py +0 -0
  83. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/types.py +0 -0
  84. {django_cachex-0.4.1 → django_cachex-0.4.2}/django_cachex/utils.py +0 -0
  85. {django_cachex-0.4.1 → django_cachex-0.4.2}/examples/README.md +0 -0
  86. {django_cachex-0.4.1 → django_cachex-0.4.2}/examples/full/README.md +0 -0
  87. {django_cachex-0.4.1 → django_cachex-0.4.2}/examples/full/pyproject.toml +0 -0
  88. {django_cachex-0.4.1 → django_cachex-0.4.2}/examples/simple/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: django-cachex
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Full featured Valkey and Redis cache backend for Django
5
5
  Project-URL: Homepage, https://github.com/oliverhaas/django-cachex
6
6
  Project-URL: Documentation, https://oliverhaas.github.io/django-cachex/
@@ -44,7 +44,7 @@ Requires-Dist: ormsgpack>=1.10.0; extra == 'ormsgpack'
44
44
  Provides-Extra: redis-py
45
45
  Requires-Dist: redis<9,>=6; extra == 'redis-py'
46
46
  Provides-Extra: redis-rs
47
- Requires-Dist: django-cachex-redis-rs==0.4.1; extra == 'redis-rs'
47
+ Requires-Dist: django-cachex-redis-rs==0.4.2; extra == 'redis-rs'
48
48
  Provides-Extra: valkey-glide
49
49
  Requires-Dist: valkey-glide-sync<3,>=2.3; extra == 'valkey-glide'
50
50
  Requires-Dist: valkey-glide<3,>=2.3; extra == 'valkey-glide'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "django-cachex-redis-rs"
3
- version = "0.4.1"
3
+ version = "0.4.2"
4
4
  description = "Rust adapter (PyO3 + tokio + redis-rs) for django-cachex"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -8,6 +8,7 @@ Each CAS function returns:
8
8
  1: success (value matched, update applied)
9
9
  0: conflict (value changed since page load)
10
10
  -1: gone (key/field/index no longer exists)
11
+ -2: target name already taken (hash field rename only)
11
12
  """
12
13
 
13
14
  from typing import TYPE_CHECKING, Any
@@ -91,6 +92,16 @@ end
91
92
  return 0
92
93
  """
93
94
 
95
+ _CAS_HASH_RENAME = """\
96
+ local v = redis.call('HGET', KEYS[1], ARGV[1])
97
+ if v == false then return -1 end
98
+ if redis.sha1hex(v) ~= ARGV[3] then return 0 end
99
+ if redis.call('HEXISTS', KEYS[1], ARGV[2]) == 1 then return -2 end
100
+ redis.call('HSET', KEYS[1], ARGV[2], ARGV[4])
101
+ redis.call('HDEL', KEYS[1], ARGV[1])
102
+ return 1
103
+ """
104
+
94
105
  _CAS_ZSET_SCORE_UPDATE = """\
95
106
  local v = redis.call('ZSCORE', KEYS[1], ARGV[1])
96
107
  if v == false then return -1 end
@@ -243,6 +254,31 @@ def cas_update_hash_field(
243
254
  )
244
255
 
245
256
 
257
+ def cas_rename_hash_field(
258
+ cache: RespCache,
259
+ key: str,
260
+ field: str,
261
+ new_field: str,
262
+ expected_sha1: str,
263
+ new_value: Any,
264
+ ) -> int:
265
+ """Atomically move a hash field to a new name if its value hasn't changed.
266
+
267
+ Redis has no HRENAME, so a rename is HSET plus HDEL. Running the pair in a
268
+ script keeps it atomic and costs a single round trip.
269
+
270
+ Returns:
271
+ 1 = success, 0 = conflict, -1 = field gone, -2 = new name already taken.
272
+ """
273
+ encoded = cache.encode(new_value)
274
+ return cache.eval_script(
275
+ _CAS_HASH_RENAME,
276
+ keys=[key],
277
+ args=[field, new_field, expected_sha1, encoded],
278
+ pre_hook=keys_only_pre,
279
+ )
280
+
281
+
246
282
  def cas_update_zset_score(
247
283
  cache: RespCache,
248
284
  key: str,
@@ -1,6 +1,5 @@
1
1
  """Helper functions for cache admin views."""
2
2
 
3
- import json
4
3
  import logging
5
4
  from datetime import UTC, datetime
6
5
  from typing import TYPE_CHECKING, Any
@@ -204,22 +203,18 @@ def get_type_data(
204
203
  return result
205
204
 
206
205
 
207
- def _format_value(value: Any) -> str:
208
- """Format a cache value for display: JSON for round-trippable values, repr() otherwise.
206
+ def _format_entry(value: Any) -> tuple[str, bool]:
207
+ """Format one container entry as (display_string, editable).
209
208
 
210
- Uses the same approach as format_value_for_display() for string keys:
211
- JSON-serializes anything that round-trips cleanly, so types are distinguishable
212
- (e.g. 42 vs "42", null vs "null").
209
+ Shares ``format_value_for_display`` with the string editor so containers and
210
+ strings agree on what is round-trippable. Entries that fall back to repr()
211
+ are marked non-editable: submitting the repr back would store the repr text.
213
212
  """
214
- if value is None:
215
- return "null"
216
- try:
217
- serialized = json.dumps(value, indent=2, ensure_ascii=False)
218
- if json.loads(serialized) == value:
219
- return serialized
220
- except TypeError, ValueError, OverflowError:
221
- pass
222
- return repr(value)
213
+ # Imported lazily: views.base is reachable from views/__init__, which
214
+ # imports the modules that import this one.
215
+ from django_cachex.admin.views.base import format_value_for_display
216
+
217
+ return format_value_for_display(value)
223
218
 
224
219
 
225
220
  def _fetch_type_data(cache: Any, key: str, key_type: str, *, page: int = 1) -> dict[str, Any]: # noqa: PLR0911
@@ -231,19 +226,25 @@ def _fetch_type_data(cache: Any, key: str, key_type: str, *, page: int = 1) -> d
231
226
  pagination = _paginate(length, page)
232
227
  start = pagination["start_index"]
233
228
  stop = pagination["end_index"] - 1 # LRANGE stop is inclusive
234
- items = [_format_value(i) for i in cache.lrange(key, start, stop)]
235
- # Pre-build item_entries with offset-aware indices (CAS overwrites with SHA1s)
236
- item_entries = [(start + i, item, "") for i, item in enumerate(items)]
237
- return {"items": items, "length": length, "pagination": pagination, "item_entries": item_entries}
229
+ # Every entry carries an empty SHA1 slot that CAS fills in later, so
230
+ # the template sees the same tuple shape on backends without scripting.
231
+ item_entries = []
232
+ for i, raw in enumerate(cache.lrange(key, start, stop)):
233
+ item, editable = _format_entry(raw)
234
+ item_entries.append((start + i, item, "", editable))
235
+ return {"length": length, "pagination": pagination, "item_entries": item_entries}
238
236
  case KeyType.HASH:
239
- fields = {str(k): _format_value(v) for k, v in cache.hgetall(key).items()}
237
+ fields = {str(k): v for k, v in cache.hgetall(key).items()}
240
238
  length = len(fields)
241
239
  pagination = _paginate(length, page)
242
240
  s, e = pagination["start_index"], pagination["end_index"]
243
- sliced = dict(list(fields.items())[s:e])
244
- return {"fields": sliced, "length": length, "pagination": pagination}
241
+ field_entries = []
242
+ for field, raw in list(fields.items())[s:e]:
243
+ value, editable = _format_entry(raw)
244
+ field_entries.append((field, value, "", editable))
245
+ return {"length": length, "pagination": pagination, "field_entries": field_entries}
245
246
  case KeyType.SET:
246
- members = sorted(_format_value(m) for m in cache.smembers(key))
247
+ members = sorted(_format_entry(m) for m in cache.smembers(key))
247
248
  length = len(members)
248
249
  pagination = _paginate(length, page)
249
250
  s, e = pagination["start_index"], pagination["end_index"]
@@ -253,7 +254,10 @@ def _fetch_type_data(cache: Any, key: str, key_type: str, *, page: int = 1) -> d
253
254
  pagination = _paginate(length, page)
254
255
  start = pagination["start_index"]
255
256
  stop = pagination["end_index"] - 1 # ZRANGE stop is inclusive
256
- zset_members = [(_format_value(m), s) for m, s in cache.zrange(key, start, stop, withscores=True)]
257
+ zset_members = []
258
+ for raw, score in cache.zrange(key, start, stop, withscores=True):
259
+ member, editable = _format_entry(raw)
260
+ zset_members.append((member, score, editable))
257
261
  return {"members": zset_members, "length": length, "pagination": pagination}
258
262
  case KeyType.STREAM if hasattr(cache, "xrange"):
259
263
  length = cache.xlen(key)
@@ -288,22 +292,23 @@ def _add_cas_fingerprints(cache: Any, key: str, key_type: str | None, result: di
288
292
  from django_cachex.admin.cas import get_list_sha1s
289
293
 
290
294
  list_sha1s = get_list_sha1s(cache, key)
291
- items = result.get("items", [])
292
- offset = pagination["start_index"] if pagination else 0
293
295
  result["item_entries"] = [
294
- (offset + i, item, list_sha1s[i] if i < len(list_sha1s) else "") for i, item in enumerate(items)
296
+ (index, item, list_sha1s[i] if i < len(list_sha1s) else "", editable)
297
+ for i, (index, item, _, editable) in enumerate(result.get("item_entries", []))
295
298
  ]
296
299
  case KeyType.HASH:
297
- fields = result.get("fields", {})
298
- if pagination and fields:
300
+ entries = result.get("field_entries", [])
301
+ if pagination and entries:
299
302
  from django_cachex.admin.cas import get_hash_field_sha1s_for
300
303
 
301
- hash_sha1s = get_hash_field_sha1s_for(cache, key, list(fields.keys()))
304
+ hash_sha1s = get_hash_field_sha1s_for(cache, key, [field for field, *_ in entries])
302
305
  else:
303
306
  from django_cachex.admin.cas import get_hash_field_sha1s
304
307
 
305
308
  hash_sha1s = get_hash_field_sha1s(cache, key)
306
- result["field_entries"] = [(field, value, hash_sha1s.get(field, "")) for field, value in fields.items()]
309
+ result["field_entries"] = [
310
+ (field, value, hash_sha1s.get(field, ""), editable) for field, value, _, editable in entries
311
+ ]
307
312
  except NotSupportedError:
308
313
  # ``BaseCachex`` declares ``eval_script`` and raises, so LocMem and
309
314
  # Database land here: render without CAS fingerprints.
@@ -172,18 +172,58 @@
172
172
  color: var(--error-fg);
173
173
  }
174
174
 
175
- /* Monospace text for code-like textareas */
175
+ /* ===== VALUE INPUTS ===== */
176
+ /* Shared textarea for every value, item and member editor. Height comes from
177
+ the rows attribute, which differs per context; width fills the container. */
176
178
  /* border-box: admin textareas carry a 1px border and 8px of side padding, which
177
179
  would otherwise sit outside the 100% width and clip the right edge */
178
- #id_value,
179
- #id_list_values,
180
- #id_set_members,
181
- #id_hash_value,
182
- #id_zset_value {
183
- font-family: var(--font-family-monospace);
180
+ .cachex-value-input {
184
181
  width: 100%;
185
182
  box-sizing: border-box;
186
- min-height: 200px;
183
+ resize: vertical;
184
+ }
185
+
186
+ /* The admin's fieldset.monospace rule covers textareas only, but the hash field
187
+ name and the zset score sit next to one and should read the same. */
188
+ fieldset.monospace input.cachex-value-input {
189
+ font-family: var(--font-family-monospace);
190
+ }
191
+
192
+ /* Submit row under a value textarea */
193
+ .cachex-value-actions {
194
+ margin-top: 10px;
195
+ }
196
+
197
+ /* Field-name input above a value textarea. A <p> wrapper would pick up the
198
+ admin's .aligned padding-left and sit indented against the textarea. */
199
+ .cachex-name-row {
200
+ margin-bottom: 6px;
201
+ }
202
+
203
+ .cachex-name-row input {
204
+ width: 20em;
205
+ max-width: 100%;
206
+ }
207
+
208
+ /* In an entry table the stacked buttons are taller than a two-row field, so let
209
+ the field grow to the row height rather than leaving a gap under it. The 1px
210
+ cell height is the usual table trick: a percentage height on the child only
211
+ resolves once the cell itself has a specified one, and content still wins. */
212
+ .cachex-entry-table tbody td {
213
+ height: 1px;
214
+ }
215
+
216
+ .cachex-entry-table td textarea.cachex-value-input {
217
+ height: 100%;
218
+ }
219
+
220
+ /* Row actions sit beside the value in a table row, stacked so that two buttons
221
+ line up with the value field instead of widening the row. */
222
+ .cachex-row-actions {
223
+ display: flex;
224
+ flex-direction: column;
225
+ gap: 4px;
226
+ align-items: stretch;
187
227
  }
188
228
 
189
229
  /* ===== NOTES ===== */
@@ -0,0 +1,73 @@
1
+ {% load i18n %}
2
+ {% with input_tpl="admin/django_cachex/key/key_detail_value_input.html#value-input" %}
3
+ <!-- Hash Operations -->
4
+ <fieldset class="module aligned monospace">
5
+ <h2>{% trans 'Operations' %}</h2>
6
+ <div class="form-row">
7
+ <div>
8
+ <label for="id_hset_value">{% trans 'Set Field:' %}</label>
9
+ <form method="post">
10
+ {% csrf_token %}
11
+ <input type="hidden" name="action" value="hset">
12
+ <div class="cachex-name-row"><input type="text" name="field" placeholder="{% trans 'field' %}" class="vTextField"></div>
13
+ {% include input_tpl with name="field_value" rows=4 field_id="id_hset_value" placeholder=_("value") editable=True only %}
14
+ <div class="cachex-value-actions">
15
+ <button type="submit" class="button btn-small">{% trans 'Set' %}</button>
16
+ </div>
17
+ </form>
18
+ </div>
19
+ </div>
20
+ </fieldset>
21
+ <!-- Hash Fields -->
22
+ <fieldset class="module aligned monospace">
23
+ <h2>{% trans 'Hash Fields' %}{% if type_data.length %} ({{ type_data.length }}){% endif %}</h2>
24
+ {% if type_data.field_entries %}
25
+ <div class="results">
26
+ <table id="result_list" class="cachex-entry-table" style="width: 100%;">
27
+ <thead>
28
+ <tr>
29
+ <th scope="col" style="width: 25%;">{% trans 'Field' %}</th>
30
+ <th scope="col">{% trans 'Value' %}</th>
31
+ <th scope="col" style="width: 110px;">{% trans 'Actions' %}</th>
32
+ </tr>
33
+ </thead>
34
+ <tbody>
35
+ {% for field, value, sha1, editable in type_data.field_entries %}
36
+ <tr class="{% cycle 'row1' 'row2' %}">
37
+ <td><input type="text" name="new_field" value="{{ field }}" class="cachex-value-input" form="hash-update-{{ forloop.counter0 }}"{% if not editable %} readonly title="{% trans 'Not JSON-serializable, so this row cannot be edited safely.' %}"{% endif %}></td>
38
+ <td>{% include input_tpl with name="field_value" value=value rows=2 form_id="hash-update-" row_index=forloop.counter0 editable=editable only %}</td>
39
+ <td>
40
+ <div class="cachex-row-actions">
41
+ {% if editable %}
42
+ <button type="submit" form="hash-update-{{ forloop.counter0 }}" class="button btn-small">{% trans 'Update' %}</button>
43
+ {% else %}
44
+ <button type="button" class="button btn-small" disabled title="{% trans 'Not JSON-serializable, so this value cannot be edited safely.' %}">{% trans 'Update' %}</button>
45
+ {% endif %}
46
+ <button type="submit" form="hash-delete-{{ forloop.counter0 }}" class="button delete-btn btn-small" onclick="return confirm('{% trans 'Delete this field?' %}');">{% trans 'Delete' %}</button>
47
+ </div>
48
+ </td>
49
+ </tr>
50
+ {% endfor %}
51
+ </tbody>
52
+ </table>
53
+ </div>
54
+ <!-- Forms for hash operations (outside table for valid HTML) -->
55
+ {% for field, value, sha1, editable in type_data.field_entries %}
56
+ <form id="hash-update-{{ forloop.counter0 }}" method="post" style="display: none;">
57
+ {% csrf_token %}
58
+ <input type="hidden" name="action" value="hupdate">
59
+ <input type="hidden" name="field" value="{{ field }}">
60
+ {% if sha1 %}<input type="hidden" name="original_sha1" value="{{ sha1 }}">{% endif %}
61
+ </form>
62
+ <form id="hash-delete-{{ forloop.counter0 }}" method="post" style="display: none;">
63
+ {% csrf_token %}
64
+ <input type="hidden" name="action" value="hdel">
65
+ <input type="hidden" name="field" value="{{ field }}">
66
+ </form>
67
+ {% endfor %}
68
+ {% else %}
69
+ <p class="quiet" style="padding: 10px;">{% trans 'Hash is empty.' %}</p>
70
+ {% endif %}
71
+ {% include "admin/django_cachex/key/key_detail_pagination.html" %}
72
+ </fieldset>
73
+ {% endwith %}
@@ -0,0 +1,114 @@
1
+ {% load i18n %}
2
+ {% with input_tpl="admin/django_cachex/key/key_detail_value_input.html#value-input" %}
3
+ <!-- List Operations -->
4
+ <fieldset class="module aligned monospace">
5
+ <h2>{% trans 'Operations' %}</h2>
6
+ <div class="form-row">
7
+ <div>
8
+ <label for="id_lpush_value">{% trans 'Push Left:' %}</label>
9
+ <form method="post">
10
+ {% csrf_token %}
11
+ <input type="hidden" name="action" value="lpush">
12
+ {% include input_tpl with name="value" rows=4 field_id="id_lpush_value" placeholder=_("value") editable=True only %}
13
+ <div class="cachex-value-actions">
14
+ <button type="submit" class="button btn-small">{% trans 'Push' %}</button>
15
+ </div>
16
+ </form>
17
+ </div>
18
+ </div>
19
+ <div class="form-row">
20
+ <div>
21
+ <label for="id_rpush_value">{% trans 'Push Right:' %}</label>
22
+ <form method="post">
23
+ {% csrf_token %}
24
+ <input type="hidden" name="action" value="rpush">
25
+ {% include input_tpl with name="value" rows=4 field_id="id_rpush_value" placeholder=_("value") editable=True only %}
26
+ <div class="cachex-value-actions">
27
+ <button type="submit" class="button btn-small">{% trans 'Push' %}</button>
28
+ </div>
29
+ </form>
30
+ </div>
31
+ </div>
32
+ <div class="form-row">
33
+ <label>{% trans 'Pop Left:' %}</label>
34
+ <form method="post" style="display: inline;">
35
+ {% csrf_token %}
36
+ <input type="hidden" name="action" value="lpop">
37
+ <input type="number" name="pop_count" value="1" min="1" class="vIntegerField" style="width: 50px;" title="{% trans 'Count' %}">
38
+ <button type="submit" class="button btn-small">{% trans 'Pop' %}</button>
39
+ </form>
40
+ </div>
41
+ <div class="form-row">
42
+ <label>{% trans 'Pop Right:' %}</label>
43
+ <form method="post" style="display: inline;">
44
+ {% csrf_token %}
45
+ <input type="hidden" name="action" value="rpop">
46
+ <input type="number" name="pop_count" value="1" min="1" class="vIntegerField" style="width: 50px;" title="{% trans 'Count' %}">
47
+ <button type="submit" class="button btn-small">{% trans 'Pop' %}</button>
48
+ </form>
49
+ </div>
50
+ <div class="form-row">
51
+ <label>{% trans 'Trim:' %}</label>
52
+ <form method="post" style="display: inline;">
53
+ {% csrf_token %}
54
+ <input type="hidden" name="action" value="ltrim">
55
+ <input type="number" name="trim_start" value="0" class="vIntegerField" style="width: 60px;" title="{% trans 'Start index' %}" placeholder="{% trans 'start' %}">
56
+ <input type="number" name="trim_stop" value="-1" class="vIntegerField" style="width: 60px;" title="{% trans 'Stop index (-1 for last)' %}" placeholder="{% trans 'stop' %}">
57
+ <button type="submit" class="button btn-small" onclick="return confirm('{% trans 'Trim list to this range? Elements outside the range will be removed.' %}');">{% trans 'Trim' %}</button>
58
+ </form>
59
+ </div>
60
+ </fieldset>
61
+ <!-- List Items -->
62
+ <fieldset class="module aligned monospace">
63
+ <h2>{% trans 'List Items' %}{% if type_data.length %} ({{ type_data.length }}){% endif %}</h2>
64
+ {% if type_data.item_entries %}
65
+ <div class="results">
66
+ <table id="result_list" class="cachex-entry-table" style="width: 100%;">
67
+ <thead>
68
+ <tr>
69
+ <th scope="col" style="width: 60px;">{% trans 'Index' %}</th>
70
+ <th scope="col">{% trans 'Value' %}</th>
71
+ <th scope="col" style="width: 110px;">{% trans 'Actions' %}</th>
72
+ </tr>
73
+ </thead>
74
+ <tbody>
75
+ {% for index, item, sha1, editable in type_data.item_entries %}
76
+ <tr class="{% cycle 'row1' 'row2' %}">
77
+ <td class="quiet">{{ index }}</td>
78
+ <td>{% include input_tpl with name="value" value=item rows=2 form_id="list-update-" row_index=forloop.counter0 editable=editable only %}</td>
79
+ <td>
80
+ <div class="cachex-row-actions">
81
+ {% if editable %}
82
+ <button type="submit" form="list-update-{{ forloop.counter0 }}" class="button btn-small">{% trans 'Update' %}</button>
83
+ <button type="submit" form="list-remove-{{ forloop.counter0 }}" class="button delete-btn btn-small" onclick="return confirm('{% trans 'Remove this item?' %}');">{% trans 'Remove' %}</button>
84
+ {% else %}
85
+ <button type="button" class="button btn-small" disabled title="{% trans 'Not JSON-serializable, so this item cannot be edited or matched for removal.' %}">{% trans 'Update' %}</button>
86
+ <button type="button" class="button delete-btn btn-small" disabled title="{% trans 'Not JSON-serializable, so this item cannot be edited or matched for removal.' %}">{% trans 'Remove' %}</button>
87
+ {% endif %}
88
+ </div>
89
+ </td>
90
+ </tr>
91
+ {% endfor %}
92
+ </tbody>
93
+ </table>
94
+ </div>
95
+ <!-- Forms for list operations (outside table for valid HTML) -->
96
+ {% for index, item, sha1, editable in type_data.item_entries %}
97
+ <form id="list-update-{{ forloop.counter0 }}" method="post" style="display: none;">
98
+ {% csrf_token %}
99
+ <input type="hidden" name="action" value="lset">
100
+ <input type="hidden" name="index" value="{{ index }}">
101
+ {% if sha1 %}<input type="hidden" name="original_sha1" value="{{ sha1 }}">{% endif %}
102
+ </form>
103
+ <form id="list-remove-{{ forloop.counter0 }}" method="post" style="display: none;">
104
+ {% csrf_token %}
105
+ <input type="hidden" name="action" value="lrem">
106
+ <input type="hidden" name="value" value="{{ item }}">
107
+ </form>
108
+ {% endfor %}
109
+ {% else %}
110
+ <p class="quiet" style="padding: 10px;">{% trans 'List is empty.' %}</p>
111
+ {% endif %}
112
+ {% include "admin/django_cachex/key/key_detail_pagination.html" %}
113
+ </fieldset>
114
+ {% endwith %}
@@ -0,0 +1,79 @@
1
+ {% load i18n %}
2
+ {% with input_tpl="admin/django_cachex/key/key_detail_value_input.html#value-input" %}
3
+ <!-- Set Operations -->
4
+ <fieldset class="module aligned monospace">
5
+ <h2>{% trans 'Operations' %}</h2>
6
+ <div class="form-row">
7
+ <div>
8
+ <label for="id_sadd_member">{% trans 'Add:' %}</label>
9
+ <form method="post">
10
+ {% csrf_token %}
11
+ <input type="hidden" name="action" value="sadd">
12
+ {% include input_tpl with name="member" rows=4 field_id="id_sadd_member" placeholder=_("member") editable=True only %}
13
+ <div class="cachex-value-actions">
14
+ <button type="submit" class="button btn-small">{% trans 'Add' %}</button>
15
+ </div>
16
+ </form>
17
+ </div>
18
+ </div>
19
+ <div class="form-row">
20
+ <label>{% trans 'Pop:' %}</label>
21
+ <form method="post" style="display: inline;">
22
+ {% csrf_token %}
23
+ <input type="hidden" name="action" value="spop">
24
+ <input type="number" name="pop_count" value="1" min="1" class="vIntegerField" style="width: 50px;" title="{% trans 'Count' %}">
25
+ <button type="submit" class="button btn-small" onclick="return confirm('{% trans 'Pop random members? They will be removed from the set.' %}');">{% trans 'Random' %}</button>
26
+ </form>
27
+ </div>
28
+ </fieldset>
29
+ <!-- Set Members -->
30
+ <fieldset class="module aligned monospace">
31
+ <h2>{% trans 'Set Members' %}{% if type_data.length %} ({{ type_data.length }}){% endif %}</h2>
32
+ {% if type_data.members %}
33
+ <div class="results">
34
+ <table id="result_list" class="cachex-entry-table" style="width: 100%;">
35
+ <thead>
36
+ <tr>
37
+ <th scope="col">{% trans 'Member' %}</th>
38
+ <th scope="col" style="width: 110px;">{% trans 'Actions' %}</th>
39
+ </tr>
40
+ </thead>
41
+ <tbody>
42
+ {% for member, editable in type_data.members %}
43
+ <tr class="{% cycle 'row1' 'row2' %}">
44
+ <td>{% include input_tpl with name="new_member" value=member rows=2 form_id="set-update-" row_index=forloop.counter0 editable=editable only %}</td>
45
+ <td>
46
+ <div class="cachex-row-actions">
47
+ {% if editable %}
48
+ <button type="submit" form="set-update-{{ forloop.counter0 }}" class="button btn-small">{% trans 'Update' %}</button>
49
+ <button type="submit" form="set-remove-{{ forloop.counter0 }}" class="button delete-btn btn-small" onclick="return confirm('{% trans 'Remove this member?' %}');">{% trans 'Remove' %}</button>
50
+ {% else %}
51
+ <button type="button" class="button btn-small" disabled title="{% trans 'Not JSON-serializable, so this member cannot be edited or matched for removal.' %}">{% trans 'Update' %}</button>
52
+ <button type="button" class="button delete-btn btn-small" disabled title="{% trans 'Not JSON-serializable, so this member cannot be edited or matched for removal.' %}">{% trans 'Remove' %}</button>
53
+ {% endif %}
54
+ </div>
55
+ </td>
56
+ </tr>
57
+ {% endfor %}
58
+ </tbody>
59
+ </table>
60
+ </div>
61
+ <!-- Forms for set operations (outside table for valid HTML) -->
62
+ {% for member, editable in type_data.members %}
63
+ <form id="set-update-{{ forloop.counter0 }}" method="post" style="display: none;">
64
+ {% csrf_token %}
65
+ <input type="hidden" name="action" value="supdate">
66
+ <input type="hidden" name="member" value="{{ member }}">
67
+ </form>
68
+ <form id="set-remove-{{ forloop.counter0 }}" method="post" style="display: none;">
69
+ {% csrf_token %}
70
+ <input type="hidden" name="action" value="srem">
71
+ <input type="hidden" name="member" value="{{ member }}">
72
+ </form>
73
+ {% endfor %}
74
+ {% else %}
75
+ <p class="quiet" style="padding: 10px;">{% trans 'Set is empty.' %}</p>
76
+ {% endif %}
77
+ {% include "admin/django_cachex/key/key_detail_pagination.html" %}
78
+ </fieldset>
79
+ {% endwith %}
@@ -1,16 +1,21 @@
1
1
  {% load i18n %}
2
+ {% with input_tpl="admin/django_cachex/key/key_detail_value_input.html#value-input" %}
2
3
  <!-- Stream Operations -->
3
- <fieldset class="module aligned">
4
+ <fieldset class="module aligned monospace">
4
5
  <h2>{% trans 'Operations' %}</h2>
5
6
  <div class="form-row">
6
- <label>{% trans 'Add Entry:' %}</label>
7
- <form method="post" style="display: inline;">
8
- {% csrf_token %}
9
- <input type="hidden" name="action" value="xadd">
10
- <input type="text" name="field" placeholder="{% trans 'field' %}" style="width: 100px;">
11
- <input type="text" name="field_value" placeholder="{% trans 'value' %}" style="width: 150px;">
12
- <button type="submit" class="button btn-small">{% trans 'Add' %}</button>
13
- </form>
7
+ <div>
8
+ <label for="id_xadd_value">{% trans 'Add Entry:' %}</label>
9
+ <form method="post">
10
+ {% csrf_token %}
11
+ <input type="hidden" name="action" value="xadd">
12
+ <div class="cachex-name-row"><input type="text" name="field" placeholder="{% trans 'field' %}" class="vTextField"></div>
13
+ {% include input_tpl with name="field_value" rows=4 field_id="id_xadd_value" placeholder=_("value") editable=True only %}
14
+ <div class="cachex-value-actions">
15
+ <button type="submit" class="button btn-small">{% trans 'Add' %}</button>
16
+ </div>
17
+ </form>
18
+ </div>
14
19
  </div>
15
20
  <div class="form-row">
16
21
  <label>{% trans 'Trim:' %}</label>
@@ -23,11 +28,11 @@
23
28
  </div>
24
29
  </fieldset>
25
30
  <!-- Stream Entries -->
26
- <fieldset class="module aligned">
31
+ <fieldset class="module aligned monospace">
27
32
  <h2>{% trans 'Stream Entries' %}{% if type_data.length %} ({{ type_data.length }}){% endif %}</h2>
28
33
  {% if type_data.entries %}
29
34
  <div class="results">
30
- <table id="result_list" style="width: 100%;">
35
+ <table id="result_list" class="cachex-entry-table" style="width: 100%;">
31
36
  <thead>
32
37
  <tr>
33
38
  <th scope="col" style="width: 180px;">{% trans 'Entry ID' %}</th>
@@ -58,3 +63,4 @@
58
63
  {% endif %}
59
64
  {% include "admin/django_cachex/key/key_detail_pagination.html" %}
60
65
  </fieldset>
66
+ {% endwith %}
@@ -1,6 +1,6 @@
1
1
  {% load i18n %}
2
2
  <!-- String Value Editor (default for string type or when type data not available) -->
3
- <fieldset class="module aligned">
3
+ <fieldset class="module aligned monospace">
4
4
  <h2>{% trans 'Value' %}{% if value_is_editable %} <span class="quiet" style="font-weight: normal; font-size: 0.85em;">(JSON)</span>{% else %} <span class="quiet" style="font-weight: normal; font-size: 0.85em;">({% trans 'read-only' %})</span>{% endif %}</h2>
5
5
  {% if not value_is_editable %}
6
6
  <div class="warningnote">
@@ -15,15 +15,9 @@
15
15
  <div class="form-row">
16
16
  <div>
17
17
  <label for="id_value" class="{% if not value_is_editable %}readonly{% endif %}">{% trans 'Value:' %}</label>
18
- <textarea
19
- id="id_value"
20
- name="value"
21
- rows="12"
22
- class="vLargeTextField"
23
- {% if not value_is_editable %}readonly{% endif %}
24
- >{{ value_display }}</textarea>
18
+ {% include "admin/django_cachex/key/key_detail_value_input.html#value-input" with name="value" value=value_display rows=12 field_id="id_value" editable=value_is_editable only %}
25
19
  {% if value_is_editable %}
26
- <div style="margin-top: 10px;">
20
+ <div class="cachex-value-actions">
27
21
  <button type="submit" class="button btn-small">{% trans 'Update' %}</button>
28
22
  </div>
29
23
  {% endif %}
@@ -0,0 +1,18 @@
1
+ {% load i18n %}
2
+ {% comment %}
3
+ Shared value textarea for every editor on the key detail page.
4
+
5
+ Parameters:
6
+ name POST field name (required)
7
+ value current text
8
+ rows visible rows: 12 for the string editor, 4 for the operations
9
+ forms, 2 for the compact rows in a member/item table
10
+ form_id id of the out-of-table <form> the field belongs to; in a table
11
+ it is a prefix that row_index completes
12
+ row_index row number appended to form_id, for per-row forms
13
+ field_id id attribute, for a <label for="...">
14
+ placeholder placeholder text
15
+ editable always pass explicitly: False renders the field read-only,
16
+ which is what a value that is not JSON-serializable needs
17
+ {% endcomment %}
18
+ {% partialdef value-input %}<textarea{% if field_id %} id="{{ field_id }}"{% endif %} name="{{ name }}" rows="{{ rows|default:2 }}" class="cachex-value-input"{% if form_id %} form="{{ form_id }}{{ row_index }}"{% endif %}{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}{% if not editable %} readonly title="{% trans 'Not JSON-serializable, so this is a Python repr() and cannot be edited safely.' %}"{% endif %}>{{ value }}</textarea>{% endpartialdef %}