diff options
Diffstat (limited to 'python')
| -rw-r--r-- | python/fatcat_web/entity_helpers.py | 143 | ||||
| -rw-r--r-- | python/fatcat_web/routes.py | 30 | ||||
| -rw-r--r-- | python/fatcat_web/templates/editgroup_diff.html | 54 | ||||
| -rw-r--r-- | python/fatcat_web/templates/editgroup_view.html | 71 | ||||
| -rw-r--r-- | python/fatcat_web/templates/entity_macros.html | 4 | ||||
| -rw-r--r-- | python/tests/web_editgroup.py | 8 | 
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 %} {% 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 %} +      => 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 %} -            => 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 | 
