diff --git a/ihatemoney/forms.py b/ihatemoney/forms.py index 4e241c86..e4375b87 100644 --- a/ihatemoney/forms.py +++ b/ihatemoney/forms.py @@ -182,13 +182,16 @@ class EditProjectForm(FlaskForm): return project -class UploadForm(FlaskForm): +class ImportProjectForm(FlaskForm): file = FileField( + "File", + validators=[ + FileRequired(), "JSON", validators=[FileRequired(), FileAllowed(["json", "JSON"], "JSON only!")], + ], description=_("Import previously exported JSON file"), ) - submit = SubmitField(_("Import")) class ProjectForm(EditProjectForm): @@ -240,7 +243,7 @@ class ProjectFormWithCaptcha(ProjectForm): raise ValidationError(Markup(message)) -class DestructiveActionProjectForm(FlaskForm): +class DeleteProjectForm(FlaskForm): """Used for any important "delete" action linked to a project: - delete project itself diff --git a/ihatemoney/templates/edit_project.html b/ihatemoney/templates/edit_project.html index 7fcc725e..7ea47d9f 100644 --- a/ihatemoney/templates/edit_project.html +++ b/ihatemoney/templates/edit_project.html @@ -29,7 +29,7 @@

{{ _("Edit project") }}

-
+ {{ forms.edit_project(edit_form) }}
@@ -38,23 +38,10 @@ {{ forms.delete_project(delete_form) }} -

{{ _("Import JSON") }}

-
- {{ import_form.hidden_tag() }} -
-
- {{ import_form.file(class="custom-file-input") }} - - {{ import_form.file.description }} - -
- -
- -
- {{ import_form.submit(class="btn btn-primary") }} -
+

{{ _("Import project") }}

+ + {{ forms.import_project(import_form) }}

{{ _("Download project's data") }}

diff --git a/ihatemoney/templates/forms.html b/ihatemoney/templates/forms.html index e6662b76..548f4d03 100644 --- a/ihatemoney/templates/forms.html +++ b/ihatemoney/templates/forms.html @@ -118,13 +118,25 @@ {% endmacro %} -{% macro upload_json(form) %} +{% macro import_project(form) %} + {% include "display_errors.html" %} {{ form.hidden_tag() }} - {{ form.file }} + +
+
+ {{ form.file(class="custom-file-input") }} + + {{ form.file.description }} + +
+ +
+
+ {% endmacro %} {% macro delete_project_history(form) %} diff --git a/ihatemoney/web.py b/ihatemoney/web.py index d9e7ec08..de5cac61 100644 --- a/ihatemoney/web.py +++ b/ihatemoney/web.py @@ -41,16 +41,16 @@ from ihatemoney.emails import send_creation_email from ihatemoney.forms import ( AdminAuthenticationForm, AuthenticationForm, - DestructiveActionProjectForm, + DeleteProjectForm, EditProjectForm, EmptyForm, + ImportProjectForm, InviteForm, MemberForm, PasswordReminder, ProjectForm, ProjectFormWithCaptcha, ResetPasswordForm, - UploadForm, get_billform_for, ) from ihatemoney.history import get_history, get_history_queries @@ -412,17 +412,8 @@ def reset_password(): @main.route("//edit", methods=["GET", "POST"]) def edit_project(): edit_form = EditProjectForm(id=g.project.id) - delete_form = DestructiveActionProjectForm(id=g.project.id) - import_form = UploadForm() - # Import form - if import_form.validate_on_submit(): - try: - import_project(import_form.file.data.stream, g.project) - flash(_("Project successfully uploaded")) - - return redirect(url_for("main.list_bills")) - except ValueError as e: - flash(e.args[0], category="danger") + import_form = ImportProjectForm(id=g.project.id) + delete_form = DeleteProjectForm(id=g.project.id) # Edit form if edit_form.validate_on_submit(): @@ -446,108 +437,136 @@ def edit_project(): return render_template( "edit_project.html", edit_form=edit_form, - delete_form=delete_form, import_form=import_form, + delete_form=delete_form, current_view="edit_project", ) -def import_project(file, project): - json_file = json.load(file) +@main.route("//import", methods=["POST"]) +def import_project(): + form = ImportProjectForm() + if form.validate(): + try: + data = form.file.data + if data.mimetype == "application/json": + json_file = json.load(data.stream) + else: + raise ValueError("Unsupported file type") - # Check if JSON is correct - attr = ["what", "payer_name", "payer_weight", "amount", "currency", "date", "owers"] - attr.sort() - currencies = set() - for e in json_file: - # If currency is absent, empty, or explicitly set to XXX - # set it to project default. - if e.get("currency", "") in ["", "XXX"]: - e["currency"] = project.default_currency - if len(e) != len(attr): - raise ValueError(_("Invalid JSON")) - list_attr = [] - for i in e: - list_attr.append(i) - list_attr.sort() - if list_attr != attr: - raise ValueError(_("Invalid JSON")) - # Keep track of currencies - currencies.add(e["currency"]) + # Check if JSON is correct + attr = [ + "what", + "payer_name", + "payer_weight", + "amount", + "currency", + "date", + "owers", + ] + attr.sort() + currencies = set() + for e in json_file: + # If currency is absent, empty, or explicitly set to XXX + # set it to project default. + if e.get("currency", "") in ["", "XXX"]: + e["currency"] = g.project.default_currency + if len(e) != len(attr): + raise ValueError(_("Invalid JSON")) + list_attr = [] + for i in e: + list_attr.append(i) + list_attr.sort() + if list_attr != attr: + raise ValueError(_("Invalid JSON")) + # Keep track of currencies + currencies.add(e["currency"]) - # Additional checks if project has no default currency - if project.default_currency == CurrencyConverter.no_currency: - # If bills have currencies, they must be consistent - if len(currencies - {CurrencyConverter.no_currency}) >= 2: - raise ValueError( - _( - "Cannot add bills in multiple currencies to a project without default currency" - ) + # Additional checks if project has no default currency + if g.project.default_currency == CurrencyConverter.no_currency: + # If bills have currencies, they must be consistent + if len(currencies - {CurrencyConverter.no_currency}) >= 2: + raise ValueError( + _( + "Cannot add bills in multiple currencies to a project without default currency" + ) + ) + # Strip currency from bills (since it's the same for every bill) + for e in json_file: + e["currency"] = CurrencyConverter.no_currency + + # From json : export list of members + members_json = get_members(json_file) + members = g.project.members + members_already_here = list() + for m in members: + members_already_here.append(str(m)) + + # List all members not in the project and weight associated + # List of tuples (name,weight) + members_to_add = list() + for i in members_json: + if str(i[0]) not in members_already_here: + members_to_add.append(i) + + # List bills not in the project + # Same format than JSON element + project_bills = g.project.get_pretty_bills() + bill_to_add = list() + for j in json_file: + same = False + for p in project_bills: + if same_bill(p, j): + same = True + break + if not same: + bill_to_add.append(j) + + # Add users to DB + for m in members_to_add: + Person(name=m[0], project=g.project, weight=m[1]) + db.session.commit() + + id_dict = {} + for i in g.project.members: + id_dict[i.name] = i.id + + # Create bills + for b in bill_to_add: + owers_id = list() + for ower in b["owers"]: + owers_id.append(id_dict[ower]) + + bill = Bill() + form = get_billform_for(g.project) + form.what = b["what"] + form.amount = b["amount"] + form.original_currency = b["currency"] + form.date = parse(b["date"]) + form.payer = id_dict[b["payer_name"]] + form.payed_for = owers_id + + db.session.add(form.fake_form(bill, g.project)) + + # Add bills to DB + db.session.commit() + flash(_("Project successfully uploaded")) + return redirect(url_for("main.list_bills")) + except ValueError as e: + flash(e.args[0], category="danger") + return render_template( + "edit_project.html", + current_view="edit_project", ) - # Strip currency from bills (since it's the same for every bill) - for e in json_file: - e["currency"] = CurrencyConverter.no_currency - - # From json : export list of members - members_json = get_members(json_file) - members = project.members - members_already_here = list() - for m in members: - members_already_here.append(str(m)) - - # List all members not in the project and weight associated - # List of tuples (name,weight) - members_to_add = list() - for i in members_json: - if str(i[0]) not in members_already_here: - members_to_add.append(i) - - # List bills not in the project - # Same format than JSON element - project_bills = project.get_pretty_bills() - bill_to_add = list() - for j in json_file: - same = False - for p in project_bills: - if same_bill(p, j): - same = True - break - if not same: - bill_to_add.append(j) - - # Add users to DB - for m in members_to_add: - Person(name=m[0], project=project, weight=m[1]) - db.session.commit() - - id_dict = {} - for i in project.members: - id_dict[i.name] = i.id - - # Create bills - for b in bill_to_add: - owers_id = list() - for ower in b["owers"]: - owers_id.append(id_dict[ower]) - - bill = Bill() - form = get_billform_for(project) - form.what = b["what"] - form.amount = b["amount"] - form.original_currency = b["currency"] - form.date = parse(b["date"]) - form.payer = id_dict[b["payer_name"]] - form.payed_for = owers_id - - db.session.add(form.fake_form(bill, project)) - - # Add bills to DB - db.session.commit() + else: + for component, errors in form.errors.items(): + flash(_(component + ": ") + ", ".join(errors), category="danger") + return redirect(url_for(".edit_project")) @main.route("//delete", methods=["POST"]) def delete_project(): - form = DestructiveActionProjectForm(id=g.project.id) + form = DeleteProjectForm(id=g.project.id) if form.validate(): g.project.remove_project() flash(_("Project successfully deleted")) @@ -838,7 +857,7 @@ def history(): any_ip_addresses = any(event["ip"] for event in history) - delete_form = DestructiveActionProjectForm() + delete_form = DeleteProjectForm() return render_template( "history.html", current_view="history", @@ -854,7 +873,7 @@ def history(): @main.route("//erase_history", methods=["POST"]) def erase_history(): """Erase all history entries associated with this project.""" - form = DestructiveActionProjectForm(id=g.project.id) + form = DeleteProjectForm(id=g.project.id) if not form.validate(): flash( format_form_errors(form, _("Error deleting project history")), @@ -873,7 +892,7 @@ def erase_history(): @main.route("//strip_ip_addresses", methods=["POST"]) def strip_ip_addresses(): """Strip ip addresses from history entries associated with this project.""" - form = DestructiveActionProjectForm(id=g.project.id) + form = DeleteProjectForm(id=g.project.id) if not form.validate(): flash( format_form_errors(form, _("Error deleting recorded IP addresses")),