aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--python/fatcat_web/entity_helpers.py143
-rw-r--r--python/fatcat_web/routes.py30
-rw-r--r--python/fatcat_web/templates/editgroup_diff.html54
-rw-r--r--python/fatcat_web/templates/editgroup_view.html71
-rw-r--r--python/fatcat_web/templates/entity_macros.html4
-rw-r--r--python/tests/web_editgroup.py8
6 files changed, 256 insertions, 54 deletions
diff --git a/python/fatcat_web/entity_helpers.py b/python/fatcat_web/entity_helpers.py
index 86543ee3..2e3b83c5 100644
--- a/python/fatcat_web/entity_helpers.py
+++ b/python/fatcat_web/entity_helpers.py
@@ -1,4 +1,5 @@
-from typing import Any, Tuple
+import difflib
+from typing import Any, Dict, List, Tuple
from fatcat_openapi_client import (
ContainerEntity,
@@ -17,6 +18,7 @@ from flask import abort
from fatcat_tools.transforms import (
container_to_elasticsearch,
+ entity_to_toml,
file_to_elasticsearch,
release_to_elasticsearch,
)
@@ -139,24 +141,38 @@ def enrich_work_entity(entity: WorkEntity) -> WorkEntity:
return entity
-def generic_get_entity(entity_type: str, ident: str) -> Any:
+def generic_get_entity(entity_type: str, ident: str, enrich: bool = True) -> Any:
try:
- if entity_type == "container":
+ if entity_type == "container" and enrich:
return enrich_container_entity(api.get_container(ident))
- elif entity_type == "creator":
+ elif entity_type == "container":
+ return api.get_container(ident)
+ elif entity_type == "creator" and enrich:
return enrich_creator_entity(api.get_creator(ident))
- elif entity_type == "file":
+ elif entity_type == "creator":
+ return api.get_creator(ident)
+ elif entity_type == "file" and enrich:
return enrich_file_entity(api.get_file(ident, expand="releases"))
- elif entity_type == "fileset":
+ elif entity_type == "file":
+ return api.get_file(ident, expand="releases")
+ elif entity_type == "fileset" and enrich:
return enrich_fileset_entity(api.get_fileset(ident, expand="releases"))
- elif entity_type == "webcapture":
+ elif entity_type == "fileset":
+ return api.get_fileset(ident)
+ elif entity_type == "webcapture" and enrich:
return enrich_webcapture_entity(api.get_webcapture(ident, expand="releases"))
- elif entity_type == "release":
+ elif entity_type == "webcapture":
+ return api.get_webcapture(ident)
+ elif entity_type == "release" and enrich:
return enrich_release_entity(
api.get_release(ident, expand="container,creators,files,filesets,webcaptures")
)
- elif entity_type == "work":
+ elif entity_type == "release":
+ return api.get_release(ident)
+ elif entity_type == "work" and enrich:
return enrich_work_entity(api.get_work(ident))
+ elif entity_type == "work":
+ return api.get_work(ident)
else:
raise NotImplementedError
except ApiException as ae:
@@ -165,30 +181,44 @@ def generic_get_entity(entity_type: str, ident: str) -> Any:
abort(400)
-def generic_get_entity_revision(entity_type: str, revision_id: str) -> Any:
+def generic_get_entity_revision(entity_type: str, revision_id: str, enrich: bool = True) -> Any:
try:
- if entity_type == "container":
+ if entity_type == "container" and enrich:
return enrich_container_entity(api.get_container_revision(revision_id))
- elif entity_type == "creator":
+ elif entity_type == "container":
+ return api.get_container_revision(revision_id)
+ elif entity_type == "creator" and enrich:
return enrich_creator_entity(api.get_creator_revision(revision_id))
- elif entity_type == "file":
+ elif entity_type == "creator":
+ return api.get_creator_revision(revision_id)
+ elif entity_type == "file" and enrich:
return enrich_file_entity(api.get_file_revision(revision_id, expand="releases"))
- elif entity_type == "fileset":
+ elif entity_type == "file":
+ return api.get_file_revision(revision_id)
+ elif entity_type == "fileset" and enrich:
return enrich_fileset_entity(
api.get_fileset_revision(revision_id, expand="releases")
)
- elif entity_type == "webcapture":
+ elif entity_type == "fileset":
+ return api.get_fileset_revision(revision_id)
+ elif entity_type == "webcapture" and enrich:
return enrich_webcapture_entity(
api.get_webcapture_revision(revision_id, expand="releases")
)
- elif entity_type == "release":
+ elif entity_type == "webcapture":
+ return api.get_webcapture_revision(revision_id)
+ elif entity_type == "release" and enrich:
return enrich_release_entity(
api.get_release_revision(revision_id, expand="container")
)
- elif entity_type == "work":
+ elif entity_type == "release":
+ return api.get_release_revision(revision_id)
+ elif entity_type == "work" and enrich:
return enrich_work_entity(api.get_work_revision(revision_id))
+ elif entity_type == "work":
+ return api.get_work_revision(revision_id)
else:
- raise NotImplementedError
+ raise NotImplementedError(f"entity_type: {entity_type}")
except ApiException as ae:
abort(ae.status)
except ApiValueError:
@@ -217,7 +247,10 @@ def generic_deleted_entity(entity_type: str, ident: str) -> Any:
def generic_get_editgroup_entity(
- editgroup: Editgroup, entity_type: str, ident: str
+ editgroup: Editgroup,
+ entity_type: str,
+ ident: str,
+ enrich: bool = True,
) -> Tuple[Any, EntityEdit]:
if entity_type == "container":
edits = editgroup.edits.containers
@@ -250,7 +283,7 @@ def generic_get_editgroup_entity(
return generic_deleted_entity(entity_type, ident), edit
try:
- entity = generic_get_entity_revision(entity_type, revision_id)
+ entity = generic_get_entity_revision(entity_type, revision_id, enrich=enrich)
except ApiException as ae:
abort(ae.status)
except ApiValueError:
@@ -258,3 +291,73 @@ def generic_get_editgroup_entity(
entity.ident = ident
return entity, edit
+
+
+def _entity_edit_diff(entity_type: str, entity_edit: EntityEdit) -> List[str]:
+ """
+ Helper to generate diff lines for a single entity edit.
+
+ Schema of entity_edit (as a reminder):
+
+ entity_edit
+ ident
+ revision
+ prev_revision
+ redirect_ident
+ """
+ pop_fields = ["ident", "revision", "state"]
+ new_rev = generic_get_entity_revision(entity_type, entity_edit.revision, enrich=False)
+ new_toml = entity_to_toml(new_rev, pop_fields=pop_fields).strip().split("\n")
+ if len(new_toml) == 1 and not new_toml[0].strip():
+ new_toml = []
+ if entity_edit.prev_revision:
+ old_rev = generic_get_entity_revision(
+ entity_type, entity_edit.prev_revision, enrich=False
+ )
+ old_toml = entity_to_toml(old_rev, pop_fields=pop_fields).strip().split("\n")
+ fromdesc = f"/{entity_type}/rev/{entity_edit.prev_revision}.toml"
+ else:
+ old_toml = []
+ fromdesc = "(created)"
+
+ diff_lines = list(
+ difflib.unified_diff(
+ old_toml,
+ new_toml,
+ fromfile=fromdesc,
+ tofile=f"/{entity_type}/rev/{entity_edit.revision}.toml",
+ )
+ )
+ return diff_lines
+
+
+def editgroup_get_diffs(editgroup: Editgroup) -> Dict[str, Any]:
+ """
+ Fetches before/after entity revisions, and computes "diffs" of TOML representations.
+
+ Returns a dict with entity type (pluralized, like "files"), then within
+ that a dict with entity ident (without prefix) containing a list of
+ strings, one per line of the "unified diff" format. If there is no diff for
+ an edited entity (eg, it was or redirected), instead `None` is returned for
+ that entity.
+ """
+ diffs: Dict[str, Any] = {}
+
+ for entity_type in [
+ "container",
+ "creator",
+ "release",
+ "work",
+ "file",
+ "fileset",
+ "webcapture",
+ ]:
+ edits = getattr(editgroup.edits, entity_type + "s") or []
+ diffs[entity_type] = {}
+ for ed in edits:
+ # only for creation and update
+ if ed.revision and not ed.redirect_ident:
+ diffs[entity_type][ed.ident] = _entity_edit_diff(entity_type, ed)
+ else:
+ diffs[entity_type][ed.ident] = None
+ return diffs
diff --git a/python/fatcat_web/routes.py b/python/fatcat_web/routes.py
index 3d2c68cd..f180e339 100644
--- a/python/fatcat_web/routes.py
+++ b/python/fatcat_web/routes.py
@@ -41,6 +41,7 @@ from fatcat_web.auth import (
)
from fatcat_web.cors import crossdomain
from fatcat_web.entity_helpers import (
+ editgroup_get_diffs,
generic_get_editgroup_entity,
generic_get_entity,
generic_get_entity_revision,
@@ -689,6 +690,35 @@ def editgroup_view(ident: str) -> AnyResponse:
return render_template("editgroup_view.html", editgroup=eg, auth_to=auth_to)
+@app.route("/editgroup/<string(length=26):ident>/diff", methods=["GET"])
+def editgroup_diff_view(ident: str) -> AnyResponse:
+ try:
+ eg = api.get_editgroup(str(ident))
+ eg.editor = api.get_editor(eg.editor_id)
+ eg.annotations = api.get_editgroup_annotations(eg.editgroup_id, expand="editors")
+ except ApiException as ae:
+ abort(ae.status)
+ # TODO: idomatic check for login?
+ auth_to = dict(
+ submit=False,
+ accept=False,
+ edit=False,
+ annotate=False,
+ )
+ if session.get("editor"):
+ user = load_user(session["editor"]["editor_id"])
+ auth_to["annotate"] = True
+ if user.is_admin or user.editor_id == eg.editor_id:
+ auth_to["submit"] = True
+ auth_to["edit"] = True
+ if user.is_admin:
+ auth_to["accept"] = True
+ diffs = editgroup_get_diffs(eg)
+ return render_template(
+ "editgroup_diff.html", editgroup=eg, auth_to=auth_to, editgroup_diffs=diffs
+ )
+
+
@app.route("/editgroup/<string(length=26):ident>/annotation", methods=["POST"])
@login_required
def editgroup_create_annotation(ident: str) -> AnyResponse:
diff --git a/python/fatcat_web/templates/editgroup_diff.html b/python/fatcat_web/templates/editgroup_diff.html
new file mode 100644
index 00000000..ec5e4a82
--- /dev/null
+++ b/python/fatcat_web/templates/editgroup_diff.html
@@ -0,0 +1,54 @@
+{% extends "editgroup_view.html" %}
+
+{% macro edit_diff_list(auth_to, editgroup, edits, diffs, entity_type, entity_name) -%}
+{% if edits %}
+ <h3>{{ entity_name }} Edit Diffs ({{ edits|count }})</h3>
+ <hr>
+ <div class="ui divided list">
+ {% for edit in edits %}
+ <div class="item" id="{{ entity_type }}_{{ edit.ident }}">
+ <div class="content" style="padding-bottom: 0.5em;">
+ {{ entity_edit_header(auth_to, editgroup, edit, entity_type, entity_name) }}
+ {% if edit.extra %}
+ {{ entity_macros.extra_metadata(edit.extra) }}
+ {% endif %}
+ {% if edit.revision and not edit.redirect_ident and edit.ident in diffs and diffs[edit.ident] != None %}
+ <br clear="all">
+ <div style="border: 1px solid black; font-size: smaller; font-color: #222; word-break: break-all; margin-top: 0.5em; margin-bottom: 0.5em;">
+ {% for line in diffs[edit.ident] %}
+ {% set line_space = false %}
+ {% if line.startswith('@@') or line.startswith('---') or line.startswith('+++') %}
+ {% set line_color = "lightblue" %}{# a light blue #}
+ {% elif line.startswith('+') %}
+ {% set line_color = "#a4efa4" %}{# a light green #}
+ {% elif line.startswith('-') %}
+ {% set line_color = "#ffa3a3" %}{# a light red #}
+ {% else %}
+ {% set line_color = "#eee" %}{# almost white #}
+ {% set line_space = true %}
+ {% endif %}
+ <pre style="background-color: {{ line_color }}; white-space: pre-wrap; margin: 0px;">{% if line_space %}&nbsp;{% endif %}{{ line.strip() }}</pre>
+ {% endfor %}
+ </div>
+ {% endif %}
+ </div>
+ </div>
+ {% endfor %}
+ </div>
+{% endif %}
+{%- endmacro %}
+
+{% block title %}Editgroup Diff{% endblock %}
+
+{% block pagetitle %}Editgroup Diff{% endblock %}
+
+{% block editgroupedits %}
+{{ edit_diff_list(auth_to, editgroup, editgroup.edits.releases, editgroup_diffs.release, "release", "Release") }}
+{{ edit_diff_list(auth_to, editgroup, editgroup.edits.works, editgroup_diffs.work, "work", "Work") }}
+{{ edit_diff_list(auth_to, editgroup, editgroup.edits.containers, editgroup_diffs.container, "container", "Container") }}
+{{ edit_diff_list(auth_to, editgroup, editgroup.edits.creators, editgroup_diffs.creator, "creator", "Creator") }}
+{{ edit_diff_list(auth_to, editgroup, editgroup.edits.files, editgroup_diffs.file, "file", "File") }}
+{{ edit_diff_list(auth_to, editgroup, editgroup.edits.filesets, editgroup_diffs.fileset, "fileset", "File Set") }}
+{{ edit_diff_list(auth_to, editgroup, editgroup.edits.webcaptures, editgroup_diffs.webcapture, "webcapture", "Web Capture") }}
+{% endblock %}
+
diff --git a/python/fatcat_web/templates/editgroup_view.html b/python/fatcat_web/templates/editgroup_view.html
index e1af719d..0142a46b 100644
--- a/python/fatcat_web/templates/editgroup_view.html
+++ b/python/fatcat_web/templates/editgroup_view.html
@@ -1,9 +1,35 @@
{% extends "base.html" %}
{% import "entity_macros.html" as entity_macros %}
-{% block title %}Editgroup{% endblock %}
-
-{% block body %}
+{% macro entity_edit_header(auth_to, editgroup, edit, entity_type, entity_name) -%}
+ <div style="float: right; font-weight: bold;">
+ <a href="/editgroup/{{ editgroup.editgroup_id }}/{{ entity_type }}/{{ edit.ident }}">[view]</a>
+ <a href="/editgroup/{{ editgroup.editgroup_id }}/diff#{{ entity_type }}_{{ edit.ident }}">[diff]</a>
+ {% if auth_to.edit and not editgroup.changelog_index and not editgroup.submitted %}
+ <br><a href="/editgroup/{{ editgroup.editgroup_id }}/{{ entity_type }}/{{ edit.ident }}/edit" style="color: green;">[re-edit]</a>
+ <br>
+ <form id="submit_edit_delete" method="POST" action="/editgroup/{{ editgroup.editgroup_id }}/{{ entity_type }}/edit/{{ edit.edit_id }}/delete" style="display:inline;">
+ <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
+ <input type="submit" value="[delete]" style="background:none; color: red; border: none; font-weight:bold; cursor:pointer; padding: 0;"></input>
+ </form>
+ {% endif %}
+ </div>
+ <div class="header">
+ <a href="/{{ entity_type }}/{{ edit.ident }}">{{ entity_type }}_{{ edit.ident }}</a>
+ {% if edit.redirect_ident %}
+ =&gt; redirect to <a href="/{{ entity_type }}/{{ edit.redirect_ident }}">{{ entity_type }}/{{ edit.redirect_ident }}</a>
+ {% elif not edit.revision %}
+ deleted
+ {% elif not edit.prev_revision %}
+ created
+ {% else %}
+ updated
+ {% endif %}
+ </div>
+ {% if edit.revision %}
+ Revision: <small><code><a href="/{{ entity_type }}/rev/{{ edit.revision }}">{{ edit.revision }}</a></code></small>
+ {% endif %}
+{%- endmacro %}
{% macro edit_list(auth_to, editgroup, edits, entity_type, entity_name) -%}
<div class="{% if edits %}active{% endif %} title">
@@ -11,34 +37,9 @@
</div><div class="{% if edits %}active{% endif %} content" style="padding-bottom: 0.5em;">
<div class="ui divided list">
{% for edit in edits %}
- <div class="item">
+ <div class="item" id="{{ entity_type }}_{{ edit.ident }}">
<div class="content" style="padding-bottom: 0.5em;">
- <div style="float: right; font-weight: bold;">
- <a href="/editgroup/{{ editgroup.editgroup_id }}/{{ entity_type }}/{{ edit.ident }}">[view]</a>
- {% if auth_to.edit and not editgroup.changelog_index and not editgroup.submitted %}
- <br><a href="/editgroup/{{ editgroup.editgroup_id }}/{{ entity_type }}/{{ edit.ident }}/edit" style="color: green;">[re-edit]</a>
- <br>
- <form id="submit_edit_delete" method="POST" action="/editgroup/{{ editgroup.editgroup_id }}/{{ entity_type }}/edit/{{ edit.edit_id }}/delete" style="display:inline;">
- <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
- <input type="submit" value="[delete]" style="background:none; color: red; border: none; font-weight:bold; cursor:pointer; padding: 0;"></input>
- </form>
- {% endif %}
- </div>
- <div class="header">
- <a href="/{{ entity_type }}/{{ edit.ident }}">{{ entity_type }}_{{ edit.ident }}</a>
- {% if edit.redirect_ident %}
- =&gt; redirect to <a href="/{{ entity_type }}/{{ edit.redirect_ident }}">{{ entity_type }}/{{ edit.redirect_ident }}</a>
- {% elif not edit.revision %}
- deleted
- {% elif not edit.prev_revision %}
- created
- {% else %}
- updated
- {% endif %}
- </div>
- {% if edit.revision %}
- Revision: <small><code><a href="/{{ entity_type }}/rev/{{ edit.revision }}">{{ edit.revision }}</a></code></small>
- {% endif %}
+ {{ entity_edit_header(auth_to, editgroup, edit, entity_type, entity_name) }}
{% if edit.extra %}
{{ entity_macros.extra_metadata(edit.extra) }}
{% endif %}
@@ -49,14 +50,16 @@
</div>
{%- endmacro %}
+{% block title %}Editgroup{% endblock %}
+
+{% block body %}
{# extended by changelog_entry #}
{% block editgroupheader %}
-<h1 class="ui header">Editgroup
+<h1 class="ui header">{% block pagetitle %}Editgroup{% endblock %}
<span class="sub header"><code>editgroup_{{ editgroup.editgroup_id }}</code></span></h1>
{% if not auth_to.submit %}
-<br clear="all">
<div class="ui info small message">
<div class="header">
What is an editgroup?
@@ -170,6 +173,7 @@
{{ entity_macros.extra_metadata(editgroup.extra) }}
{% endif %}
+{% block editgroupedits %}
<h3 class="ui header">All Entity Changes</h3>
<div class="ui styled fluid accordion">
{{ edit_list(auth_to, editgroup, editgroup.edits.releases, "release", "Release") }}
@@ -183,8 +187,10 @@
<div style="float: right; font-size: smaller;">
<a href="{{ config.FATCAT_PUBLIC_API_HOST }}/editgroup/{{ editgroup.editgroup_id }}">As JSON via API</a>
</div>
+{% endblock %}
<br>
+{% block editgroupannotations %}
<h3 class="ui header">Comments and Annotations</h3>
{% for annotation in editgroup.annotations|reverse %}
<div class="ui segments">
@@ -241,6 +247,7 @@
</form><br>
</div>
{% endif %}
+{% endblock %}
{% endblock %}
diff --git a/python/fatcat_web/templates/entity_macros.html b/python/fatcat_web/templates/entity_macros.html
index 9ebff060..39eb8a80 100644
--- a/python/fatcat_web/templates/entity_macros.html
+++ b/python/fatcat_web/templates/entity_macros.html
@@ -37,8 +37,8 @@
{% endif %}
<br>API URL: <a href="
{{ config.FATCAT_PUBLIC_API_HOST -}}
- {%- if editgroup and entity.ident -%}
- /editgroup/{{ editgroup.editgroup_id }}{# /{{ entity_type }}/{{ entity.ident }} #}
+ {%- if editgroup and entity.ident and entity.revision -%}
+ /{{ entity_type }}/rev/{{ entity.revision }}
{%- elif entity.ident -%}
/{{ entity_type }}/{{ entity.ident }}
{%- elif entity.revision -%}
diff --git a/python/tests/web_editgroup.py b/python/tests/web_editgroup.py
index 62a5df2e..906c18e6 100644
--- a/python/tests/web_editgroup.py
+++ b/python/tests/web_editgroup.py
@@ -5,8 +5,16 @@ def test_editgroup_basics(app):
rv = app.get("/editgroup/aaaaaaaaaaaabo53aaaaaaaaae")
assert rv.status_code == 200
+ rv = app.get("/editgroup/aaaaaaaaaaaabo53aaaaaaaaae/diff")
+ assert rv.status_code == 200
+ rv = app.get("/editgroup/aaaaaaaaaaaabo53aaaaaaaaa4/diff")
+ assert rv.status_code == 200
+ rv = app.get("/editgroup/aaaaaaaaaaaabo53aaaaaaaaaq/diff")
+ assert rv.status_code == 200
rv = app.get("/editgroup/ccccccccccccccccccccccccca")
assert rv.status_code == 404
+ rv = app.get("/editgroup/ccccccccccccccccccccccccca/diff")
+ assert rv.status_code == 404
rv = app.get("/editor/aaaaaaaaaaaabkvkaaaaaaaaae")
assert rv.status_code == 200