admin.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. __package__ = 'archivebox.base_models'
  2. from typing import Any
  3. from django.contrib import admin, messages
  4. from django.core.exceptions import ValidationError
  5. from django.utils.html import format_html
  6. from django.utils.safestring import mark_safe
  7. from django.shortcuts import redirect
  8. from django_object_actions import DjangoObjectActions, action
  9. from archivebox.misc.util import parse_date
  10. from .abid import ABID
  11. def highlight_diff(display_val: Any, compare_val: Any, invert: bool=False, color_same: str | None=None, color_diff: str | None=None):
  12. """highlight each character in red that differs with the char at the same index in compare_val"""
  13. display_val = str(display_val)
  14. compare_val = str(compare_val)
  15. if len(compare_val) < len(display_val):
  16. compare_val += ' ' * (len(display_val) - len(compare_val))
  17. similar_color, highlighted_color = color_same or 'inherit', color_diff or 'red'
  18. if invert:
  19. similar_color, highlighted_color = color_same or 'green', color_diff or 'inherit'
  20. return mark_safe(''.join(
  21. format_html('<span style="color: {};">{}</span>', highlighted_color, display_val[i])
  22. if display_val[i] != compare_val[i] else
  23. format_html('<span style="color: {};">{}</span>', similar_color, display_val[i])
  24. for i in range(len(display_val))
  25. ))
  26. def get_abid_info(self, obj, request=None):
  27. from archivebox.api.auth import get_or_create_api_token
  28. try:
  29. #abid_diff = f' != obj.ABID: {highlight_diff(obj.ABID, obj.abid)} ❌' if str(obj.ABID) != str(obj.abid) else ' == .ABID ✅'
  30. fresh_values = obj.ABID_FRESH_VALUES
  31. fresh_hashes = obj.ABID_FRESH_HASHES
  32. fresh_diffs = obj.ABID_FRESH_DIFFS
  33. fresh_abid = ABID(**fresh_hashes)
  34. fresh_abid_diff = f'❌ != &nbsp; .fresh_abid: {highlight_diff(fresh_abid, obj.ABID)}' if str(fresh_abid) != str(obj.ABID) else '✅'
  35. fresh_uuid_diff = f'❌ != &nbsp; .fresh_uuid: {highlight_diff(fresh_abid.uuid, obj.ABID.uuid)}' if str(fresh_abid.uuid) != str(obj.ABID.uuid) else '✅'
  36. id_pk_diff = f'❌ != .pk: {highlight_diff(obj.pk, obj.id)}' if str(obj.pk) != str(obj.id) else '✅'
  37. fresh_ts = parse_date(fresh_values['ts']) or None
  38. ts_diff = f'❌ != {highlight_diff( fresh_hashes["ts"], obj.ABID.ts)}' if fresh_hashes["ts"] != obj.ABID.ts else '✅'
  39. derived_uri = fresh_hashes['uri']
  40. uri_diff = f'❌ != {highlight_diff(derived_uri, obj.ABID.uri)}' if derived_uri != obj.ABID.uri else '✅'
  41. derived_subtype = fresh_hashes['subtype']
  42. subtype_diff = f'❌ != {highlight_diff(derived_subtype, obj.ABID.subtype)}' if derived_subtype != obj.ABID.subtype else '✅'
  43. derived_rand = fresh_hashes['rand']
  44. rand_diff = f'❌ != {highlight_diff(derived_rand, obj.ABID.rand)}' if derived_rand != obj.ABID.rand else '✅'
  45. return format_html(
  46. # URL Hash: <code style="font-size: 10px; user-select: all">{}</code><br/>
  47. '''
  48. <a href="{}" style="font-size: 16px; font-family: monospace; user-select: all; border-radius: 8px; background-color: #ddf; padding: 3px 5px; border: 1px solid #aaa; margin-bottom: 8px; display: inline-block; vertical-align: top;">{}</a> &nbsp; &nbsp; <a href="{}" style="color: limegreen; font-size: 0.9em; vertical-align: 1px; font-family: monospace;">📖 API DOCS</a>
  49. <br/><hr/>
  50. <div style="opacity: 0.8">
  51. &nbsp; &nbsp; <small style="opacity: 0.8">.id: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<code style="font-size: 10px; user-select: all">{}</code> &nbsp; &nbsp; {}</small><br/>
  52. &nbsp; &nbsp; <small style="opacity: 0.8">.abid.uuid: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px; user-select: all">{}</code> &nbsp; &nbsp; {}</small><br/>
  53. &nbsp; &nbsp; <small style="opacity: 0.8">.abid: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px; user-select: all">{}</code> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {}</small><br/>
  54. <hr/>
  55. &nbsp; &nbsp; TS: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<code style="font-size: 10px;"><b style="user-select: all">{}</b> &nbsp; {}</code> &nbsp; &nbsp; &nbsp;&nbsp; <code style="font-size: 10px;"><b>{}</b></code> {}: <code style="user-select: all">{}</code><br/>
  56. &nbsp; &nbsp; URI: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px;"><b style="user-select: all">{}</b> &nbsp; &nbsp; {}</code> &nbsp;&nbsp; &nbsp; &nbsp; &nbsp;&nbsp; <code style="font-size: 10px;"><b>{}</b></code> <span style="display:inline-block; vertical-align: -4px; width: 330px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{}: <code style="user-select: all">{}</code></span><br/>
  57. &nbsp; &nbsp; SUBTYPE: &nbsp; &nbsp; &nbsp; <code style="font-size: 10px;"><b style="user-select: all">{}</b> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {}</code> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px;"><b>{}</b></code> {}: <code style="user-select: all">{}</code><br/>
  58. &nbsp; &nbsp; RAND: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px;"><b style="user-select: all">{}</b> &nbsp; &nbsp; &nbsp; {}</code> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px;"><b>{}</b></code> {}: <code style="user-select: all">{}</code></code>
  59. <br/><hr/>
  60. <span style="color: #f375a0">{}</span> <code style="color: red"><b>{}</b></code> {}
  61. </div>
  62. ''',
  63. obj.api_url + (f'?api_key={get_or_create_api_token(request.user)}' if request and request.user else ''), obj.api_url, obj.api_docs_url,
  64. highlight_diff(obj.id, obj.ABID.uuid, invert=True), mark_safe(id_pk_diff),
  65. highlight_diff(obj.ABID.uuid, obj.id, invert=True), mark_safe(fresh_uuid_diff),
  66. highlight_diff(obj.abid, fresh_abid), mark_safe(fresh_abid_diff),
  67. # str(fresh_abid.uuid), mark_safe(fresh_uuid_diff),
  68. # str(fresh_abid), mark_safe(fresh_abid_diff),
  69. highlight_diff(obj.ABID.ts, fresh_hashes['ts']), highlight_diff(str(obj.ABID.uuid)[0:14], str(fresh_abid.uuid)[0:14]), mark_safe(ts_diff), obj.abid_ts_src, fresh_ts and fresh_ts.isoformat(),
  70. highlight_diff(obj.ABID.uri, derived_uri), highlight_diff(str(obj.ABID.uuid)[14:26], str(fresh_abid.uuid)[14:26]), mark_safe(uri_diff), obj.abid_uri_src, str(fresh_values['uri']),
  71. highlight_diff(obj.ABID.subtype, derived_subtype), highlight_diff(str(obj.ABID.uuid)[26:28], str(fresh_abid.uuid)[26:28]), mark_safe(subtype_diff), obj.abid_subtype_src, str(fresh_values['subtype']),
  72. highlight_diff(obj.ABID.rand, derived_rand), highlight_diff(str(obj.ABID.uuid)[28:36], str(fresh_abid.uuid)[28:36]), mark_safe(rand_diff), obj.abid_rand_src, str(fresh_values['rand'])[-7:],
  73. 'Some values the ABID depends on have changed since the ABID was issued:' if fresh_diffs else '',
  74. ", ".join(diff['abid_src'] for diff in fresh_diffs.values()),
  75. '(clicking "Regenerate ABID" in the upper right will assign a new ABID, breaking any external references to the old ABID)' if fresh_diffs else '',
  76. )
  77. except Exception as e:
  78. # import ipdb; ipdb.set_trace()
  79. return str(e)
  80. class ABIDModelAdmin(DjangoObjectActions, admin.ModelAdmin):
  81. list_display = ('created_at', 'created_by', 'abid')
  82. sort_fields = ('created_at', 'created_by', 'abid')
  83. readonly_fields = ('created_at', 'modified_at', 'abid_info')
  84. # fields = [*readonly_fields]
  85. change_actions = ("regenerate_abid",)
  86. # changelist_actions = ("regenerate_abid",)
  87. def _get_obj_does_not_exist_redirect(self, request, opts, object_id):
  88. try:
  89. object_pk = self.model.id_from_abid(object_id)
  90. return redirect(self.request.path.replace(object_id, object_pk), permanent=False)
  91. except (self.model.DoesNotExist, ValidationError):
  92. pass
  93. return super()._get_obj_does_not_exist_redirect(request, opts, object_id) # type: ignore
  94. def queryset(self, request):
  95. self.request = request
  96. return super().queryset(request) # type: ignore
  97. def change_view(self, request, object_id, form_url="", extra_context=None):
  98. self.request = request
  99. return super().change_view(request, object_id, form_url, extra_context)
  100. def get_form(self, request, obj=None, **kwargs):
  101. self.request = request
  102. form = super().get_form(request, obj, **kwargs)
  103. if 'created_by' in form.base_fields:
  104. form.base_fields['created_by'].initial = request.user
  105. if obj:
  106. if obj.ABID_FRESH_DIFFS:
  107. messages.warning(request, "The ABID is not in sync with the object! See the API Identifiers section below for more info...")
  108. return form
  109. def get_formset(self, request, formset=None, obj=None, **kwargs):
  110. formset = super().get_formset(request, formset, obj, **kwargs) # type: ignore
  111. formset.form.base_fields['created_at'].disabled = True
  112. return formset
  113. def save_model(self, request, obj, form, change):
  114. self.request = request
  115. old_abid = getattr(obj, '_previous_abid', None) or obj.abid
  116. super().save_model(request, obj, form, change)
  117. obj.refresh_from_db()
  118. new_abid = obj.abid
  119. if new_abid != old_abid:
  120. messages.warning(request, f"The object's ABID has been updated! {old_abid} -> {new_abid} (any external references to the old ABID will need to be updated manually)")
  121. # import ipdb; ipdb.set_trace()
  122. @admin.display(description='API Identifiers')
  123. def abid_info(self, obj):
  124. return get_abid_info(self, obj, request=self.request)
  125. @action(label="Regenerate ABID", description="Re-Generate the ABID based on fresh values")
  126. def regenerate_abid(self, request, obj):
  127. old_abid = str(obj.abid)
  128. obj.abid = obj.issue_new_abid(overwrite=True)
  129. obj.save()
  130. obj.refresh_from_db()
  131. new_abid = str(obj.abid)
  132. if new_abid != old_abid:
  133. messages.warning(request, f"The object's ABID has been updated! {old_abid} -> {new_abid} (any external references to the old ABID will need to be updated manually)")
  134. else:
  135. messages.success(request, "The ABID was not regenerated, it is already up-to-date with the object.")