From c95e50aba5a76b3b18c42fc3931b6b17d73f2df4 Mon Sep 17 00:00:00 2001 From: Andrew Dickinson Date: Sun, 12 Apr 2020 21:05:23 -0400 Subject: [PATCH] Add history page and endpoints --- ihatemoney/static/css/main.css | 36 ++++- ihatemoney/static/images/add.png | Bin 0 -> 264 bytes ihatemoney/templates/history.html | 235 ++++++++++++++++++++++++++++++ ihatemoney/templates/layout.html | 2 + ihatemoney/web.py | 47 +++++- 5 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 ihatemoney/static/images/add.png create mode 100644 ihatemoney/templates/history.html diff --git a/ihatemoney/static/css/main.css b/ihatemoney/static/css/main.css index a646b175..aa266e10 100644 --- a/ihatemoney/static/css/main.css +++ b/ihatemoney/static/css/main.css @@ -320,7 +320,7 @@ footer .footer-left { background: url("../images/see.png") no-repeat right; } -#bill_table, #monthly_stats { +#bill_table, #monthly_stats, #history_table { margin-top: 30px; margin-bottom: 30px; } @@ -356,6 +356,36 @@ footer .footer-left { background: url("../images/see.png") no-repeat right; } +.history_icon > .delete, +.history_icon > .add, +.history_icon > .edit { + font-size: 0px; + display: block; + width: 16px; + height: 16px; + margin: 2px; + margin-right: 10px; + margin-top: 3px; + float: left; +} + +.history_icon > .delete { + background: url("../images/delete.png") no-repeat right; +} + +.history_icon > .edit { + background: url("../images/edit.png") no-repeat right; +} + +.history_icon > .add { + background: url("../images/add.png") no-repeat right; +} + +.history_text { + display: table-cell; +} + + .balance .balance-value { text-align: right; } @@ -516,3 +546,7 @@ footer .icon svg { text-align: right; width: 200px; } + +#history_warnings { + margin-top: 30px; +} diff --git a/ihatemoney/static/images/add.png b/ihatemoney/static/images/add.png new file mode 100644 index 0000000000000000000000000000000000000000..262891bf14eb8c2967317ad8694398745903f51c GIT binary patch literal 264 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b zKpodXn9)gNb_Gz7y~NYkmHi0|w~&#r=$j2LK%uFgE{-73qGpx{W<)cpFi!#q6HF{{ event.object_desc }}{% endmacro %} + +{% macro simple_property_change(event, localized_property_name, from=True) %} + {{ describe_object(event) }}: + {{ localized_property_name }} {{ _("changed") }} + {% if from %}{{ _("from") }} {{ event.val_before }} {% endif %} + {{ _("to") }} {{ event.val_after }} +{% endmacro %} + + +{% macro owers_changed(event, add) %} + {{ describe_object(event) }}: {% if add %}{{ _("Added") }}{% else %}{{ _("Removed") }}{% endif %} + {% if event.val_after|length > 1 %} + {% for name in event.val_after %} + {{ name }}{% if event.val_after|length > 2 and loop.index != event.val_after|length %},{% endif %} + {% if loop.index == event.val_after|length - 1 %} {{ _("and") }} {% endif %} + {% endfor %} + {% else %} + {{ event.val_after[0] }} + {% endif %} + {% if add %}{{ _("to") }}{% else %}{{ _("from") }}{% endif %} + {{ _("owers list") }} +{% endmacro %} + +{% block sidebar %} +
+ + + + + + + + {% set balance = g.project.balance %} + {% for member in g.project.members | sort(attribute='name') if member.activated or balance[member.id]|round(2) != 0 %} + + + + + {% endfor %} +
{{ _("Who?") }}{{ _("Balance") }}
{{ member.name }} + {% if balance[member.id]|round(2) > 0 %}+{% endif %}{{ "%.2f" | format(balance[member.id]) }} +
+
+{% endblock %} + + + +{% block content %} + {% if current_log_pref == LoggingMode.DISABLED or (current_log_pref != LoggingMode.RECORD_IP and any_ip_addresses) %} +
+ {% if current_log_pref == LoggingMode.DISABLED %} +

+ {{ _("This project has history disabled. New actions won't appear below. You can enable history on the") }} + {{ _("settings page") }} +

+ {% if history %} +

{{ _("The table below reflects actions recorded prior to disabling project history. You can ") }} + {{ _("clear project history") }} {{ _("to remove them.") }}

+ + + {% endif %} + {% endif %} + {% if current_log_pref != LoggingMode.RECORD_IP and any_ip_addresses %} +

{{ _("Some entries below contain IP addresses, even though this project has IP recording disabled. ") }} + {{ _(" Delete stored IP addresses ") }}

+ + + {% endif %} +
+ {% endif %} + {% if history %} + + + + + + + + {% for event in history %} + + + + + + {% endfor %} + +
{{ _("Time") }}{{ _("Event") }} + + {{ _("From IP") }}
{{ momentjs(event.time).calendar() }} +
+ +
+
+ {% if event.operation_type == OperationType.INSERT %} + {{ event.object_type }} {{ event.object_desc }} {{ _("added") }} + {% elif event.operation_type == OperationType.UPDATE %} + {% if event.object_type == _("Project") %} + {% if event.prop_changed == "password" %} + {{ _("Project private code changed") }} + {% elif event.prop_changed == "logging_preference" %} + {{ change_to_logging_preference(event) }} + {% elif event.prop_changed == "name" %} + {{ _("Project renamed to ") }} {{ event.object_desc }} + {% elif event.prop_changed == "contact_email" %} + {{ _("Project contact email changed to ") }} {{ event.val_after }} + {% else %} + {{ _("Project settings modified") }} + {% endif %} + {% elif event.prop_changed == "activated" %} + {{ event.object_type }} {{ event.object_desc }} + {% if event.val_after == False %}{{ _("deactivated") }}{% else %}{{ _("reactivated") }}{% endif %} + {% elif event.prop_changed == "name" or event.prop_changed == "what" %} + {{ describe_object(event) }} {{ _("renamed") }} {{ _("to") }} {{ event.val_after }} + {% elif event.prop_changed == "weight" %} + {{ simple_property_change(event, _("Weight")) }} + {% elif event.prop_changed == "external_link" %} + {{ describe_object(event) }}: {{ _("External link changed to") }} + {{ event.val_after }} + {% elif event.prop_changed == "owers_added" %} + {{ owers_changed(event, True)}} + {% elif event.prop_changed == "owers_removed" %} + {{ owers_changed(event, False)}} + {% elif event.prop_changed == "payer" %} + {{ simple_property_change(event, _("Payer"))}} + {% elif event.prop_changed == "amount" %} + {{ simple_property_change(event, _("Amount")) }} + {% elif event.prop_changed == "date" %} + {{ simple_property_change(event, _("Date")) }} + {% else %} + {{ describe_object(event) }} {{ _("modified") }} + {% endif %} + {% elif event.operation_type == OperationType.DELETE %} + {{ event.object_type }} {{ event.object_desc }} {{ _("removed") }} + {% else %} + {# Should be unreachable #} + {{ describe_object(event) }} {{ _("changed in a unknown way") }} + {% endif %} +
+
{% if event.ip %}{{ event.ip }}{% else %} -- {% endif %}
+ {% else %} +
+
+
+ {{ static_include("images/hand-holding-heart.svg") | safe }} +

{{ _('Nothing to list')}}

+

+ {{ _("Someone probably") }}
+ {{ _("cleared the project history.") }} +

+
+
+ {% endif %} + +{% endblock %} diff --git a/ihatemoney/templates/layout.html b/ihatemoney/templates/layout.html index dc3d32f6..449ab350 100644 --- a/ihatemoney/templates/layout.html +++ b/ihatemoney/templates/layout.html @@ -12,6 +12,7 @@ + {%- if request.path == "/dashboard" %} @@ -45,6 +46,7 @@ + {% endblock %} diff --git a/ihatemoney/web.py b/ihatemoney/web.py index 55b4fd65..60cb8454 100644 --- a/ihatemoney/web.py +++ b/ihatemoney/web.py @@ -31,6 +31,7 @@ from flask import ( from flask_babel import get_locale, gettext as _ from flask_mail import Message from sqlalchemy import orm +from sqlalchemy_continuum import Operation from werkzeug.exceptions import NotFound from werkzeug.security import check_password_hash, generate_password_hash @@ -46,7 +47,8 @@ from ihatemoney.forms import ( get_billform_for, UploadForm, ) -from ihatemoney.models import db, Project, Person, Bill +from ihatemoney.history import get_history_queries, get_history +from ihatemoney.models import db, Project, Person, Bill, LoggingMode from ihatemoney.utils import ( Redirect303, list_of_dicts2json, @@ -741,6 +743,49 @@ def settle_bill(): return render_template("settle_bills.html", bills=bills, current_view="settle_bill") +@main.route("//history") +def history(): + """Query for the version entries associated with this project.""" + history = get_history(g.project, human_readable_names=True) + + any_ip_addresses = False + for event in history: + if event["ip"]: + any_ip_addresses = True + break + + return render_template( + "history.html", + current_view="history", + history=history, + any_ip_addresses=any_ip_addresses, + LoggingMode=LoggingMode, + OperationType=Operation, + current_log_pref=g.project.logging_preference, + ) + + +@main.route("//erase_history", methods=["POST"]) +def erase_history(): + """Erase all history entries associated with this project.""" + for query in get_history_queries(g.project): + query.delete(synchronize_session="fetch") + + db.session.commit() + return redirect(url_for(".history")) + + +@main.route("//strip_ip_addresses", methods=["POST"]) +def strip_ip_addresses(): + """Strip ip addresses from history entries associated with this project.""" + for query in get_history_queries(g.project): + for version_object in query.all(): + version_object.transaction.remote_addr = None + + db.session.commit() + return redirect(url_for(".history")) + + @main.route("//statistics") def statistics(): """Compute what each member has paid and spent and display it"""