proper import form (fix messy errors)

This commit is contained in:
Youe Graillot 2021-11-28 00:59:57 +01:00
parent d0a8668de6
commit d24efac10a
4 changed files with 149 additions and 128 deletions

View file

@ -182,13 +182,16 @@ class EditProjectForm(FlaskForm):
return project return project
class UploadForm(FlaskForm): class ImportProjectForm(FlaskForm):
file = FileField( file = FileField(
"File",
validators=[
FileRequired(),
"JSON", "JSON",
validators=[FileRequired(), FileAllowed(["json", "JSON"], "JSON only!")], validators=[FileRequired(), FileAllowed(["json", "JSON"], "JSON only!")],
],
description=_("Import previously exported JSON file"), description=_("Import previously exported JSON file"),
) )
submit = SubmitField(_("Import"))
class ProjectForm(EditProjectForm): class ProjectForm(EditProjectForm):
@ -240,7 +243,7 @@ class ProjectFormWithCaptcha(ProjectForm):
raise ValidationError(Markup(message)) raise ValidationError(Markup(message))
class DestructiveActionProjectForm(FlaskForm): class DeleteProjectForm(FlaskForm):
"""Used for any important "delete" action linked to a project: """Used for any important "delete" action linked to a project:
- delete project itself - delete project itself

View file

@ -29,7 +29,7 @@
<div class="container edit-project"> <div class="container edit-project">
<h2>{{ _("Edit project") }}</h2> <h2>{{ _("Edit project") }}</h2>
<form class="form-horizontal" method="post"> <form id="edit-project" class="form-horizontal" method="post">
{{ forms.edit_project(edit_form) }} {{ forms.edit_project(edit_form) }}
</form> </form>
@ -38,23 +38,10 @@
{{ forms.delete_project(delete_form) }} {{ forms.delete_project(delete_form) }}
</form> </form>
<h2>{{ _("Import JSON") }}</h2>
<form class="form-horizontal" method="post" enctype="multipart/form-data">
{{ import_form.hidden_tag() }}
<div class="custom-file"> <h2>{{ _("Import project") }}</h2>
<div class="form-group"> <form id="import-project" class="form-horizontal" action="{{ url_for(".import_project") }}" method="post" enctype="multipart/form-data">
{{ import_form.file(class="custom-file-input") }} {{ forms.import_project(import_form) }}
<small class="form-text text-muted">
{{ import_form.file.description }}
</small>
</div>
<label class="custom-file-label" for="customFile">{{ _('Choose file') }}</label>
</div>
<div class="actions">
{{ import_form.submit(class="btn btn-primary") }}
</div>
</form> </form>
<h2>{{ _("Download project's data") }}</h2> <h2>{{ _("Download project's data") }}</h2>

View file

@ -118,13 +118,25 @@
{% endmacro %} {% endmacro %}
{% macro upload_json(form) %} {% macro import_project(form) %}
{% include "display_errors.html" %} {% include "display_errors.html" %}
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ form.file }}
<div class="custom-file">
<div class="form-group">
{{ form.file(class="custom-file-input") }}
<small class="form-text text-muted">
{{ form.file.description }}
</small>
</div>
<label class="custom-file-label" for="customFile">{{ _('Choose file') }}</label>
</div>
<div class="actions"> <div class="actions">
<button class="btn btn-primary">{{ _("Import") }}</button> <button class="btn btn-primary">{{ _("Import") }}</button>
</div> </div>
{% endmacro %} {% endmacro %}
{% macro delete_project_history(form) %} {% macro delete_project_history(form) %}

View file

@ -41,16 +41,16 @@ from ihatemoney.emails import send_creation_email
from ihatemoney.forms import ( from ihatemoney.forms import (
AdminAuthenticationForm, AdminAuthenticationForm,
AuthenticationForm, AuthenticationForm,
DestructiveActionProjectForm, DeleteProjectForm,
EditProjectForm, EditProjectForm,
EmptyForm, EmptyForm,
ImportProjectForm,
InviteForm, InviteForm,
MemberForm, MemberForm,
PasswordReminder, PasswordReminder,
ProjectForm, ProjectForm,
ProjectFormWithCaptcha, ProjectFormWithCaptcha,
ResetPasswordForm, ResetPasswordForm,
UploadForm,
get_billform_for, get_billform_for,
) )
from ihatemoney.history import get_history, get_history_queries from ihatemoney.history import get_history, get_history_queries
@ -412,17 +412,8 @@ def reset_password():
@main.route("/<project_id>/edit", methods=["GET", "POST"]) @main.route("/<project_id>/edit", methods=["GET", "POST"])
def edit_project(): def edit_project():
edit_form = EditProjectForm(id=g.project.id) edit_form = EditProjectForm(id=g.project.id)
delete_form = DestructiveActionProjectForm(id=g.project.id) import_form = ImportProjectForm(id=g.project.id)
import_form = UploadForm() delete_form = DeleteProjectForm(id=g.project.id)
# 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")
# Edit form # Edit form
if edit_form.validate_on_submit(): if edit_form.validate_on_submit():
@ -446,108 +437,136 @@ def edit_project():
return render_template( return render_template(
"edit_project.html", "edit_project.html",
edit_form=edit_form, edit_form=edit_form,
delete_form=delete_form,
import_form=import_form, import_form=import_form,
delete_form=delete_form,
current_view="edit_project", current_view="edit_project",
) )
def import_project(file, project): @main.route("/<project_id>/import", methods=["POST"])
json_file = json.load(file) 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 # Check if JSON is correct
attr = ["what", "payer_name", "payer_weight", "amount", "currency", "date", "owers"] attr = [
attr.sort() "what",
currencies = set() "payer_name",
for e in json_file: "payer_weight",
# If currency is absent, empty, or explicitly set to XXX "amount",
# set it to project default. "currency",
if e.get("currency", "") in ["", "XXX"]: "date",
e["currency"] = project.default_currency "owers",
if len(e) != len(attr): ]
raise ValueError(_("Invalid JSON")) attr.sort()
list_attr = [] currencies = set()
for i in e: for e in json_file:
list_attr.append(i) # If currency is absent, empty, or explicitly set to XXX
list_attr.sort() # set it to project default.
if list_attr != attr: if e.get("currency", "") in ["", "XXX"]:
raise ValueError(_("Invalid JSON")) e["currency"] = g.project.default_currency
# Keep track of currencies if len(e) != len(attr):
currencies.add(e["currency"]) 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 # Additional checks if project has no default currency
if project.default_currency == CurrencyConverter.no_currency: if g.project.default_currency == CurrencyConverter.no_currency:
# If bills have currencies, they must be consistent # If bills have currencies, they must be consistent
if len(currencies - {CurrencyConverter.no_currency}) >= 2: if len(currencies - {CurrencyConverter.no_currency}) >= 2:
raise ValueError( raise ValueError(
_( _(
"Cannot add bills in multiple currencies to a project without default currency" "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) else:
for e in json_file: for component, errors in form.errors.items():
e["currency"] = CurrencyConverter.no_currency flash(_(component + ": ") + ", ".join(errors), category="danger")
return redirect(url_for(".edit_project"))
# 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()
@main.route("/<project_id>/delete", methods=["POST"]) @main.route("/<project_id>/delete", methods=["POST"])
def delete_project(): def delete_project():
form = DestructiveActionProjectForm(id=g.project.id) form = DeleteProjectForm(id=g.project.id)
if form.validate(): if form.validate():
g.project.remove_project() g.project.remove_project()
flash(_("Project successfully deleted")) flash(_("Project successfully deleted"))
@ -838,7 +857,7 @@ def history():
any_ip_addresses = any(event["ip"] for event in history) any_ip_addresses = any(event["ip"] for event in history)
delete_form = DestructiveActionProjectForm() delete_form = DeleteProjectForm()
return render_template( return render_template(
"history.html", "history.html",
current_view="history", current_view="history",
@ -854,7 +873,7 @@ def history():
@main.route("/<project_id>/erase_history", methods=["POST"]) @main.route("/<project_id>/erase_history", methods=["POST"])
def erase_history(): def erase_history():
"""Erase all history entries associated with this project.""" """Erase all history entries associated with this project."""
form = DestructiveActionProjectForm(id=g.project.id) form = DeleteProjectForm(id=g.project.id)
if not form.validate(): if not form.validate():
flash( flash(
format_form_errors(form, _("Error deleting project history")), format_form_errors(form, _("Error deleting project history")),
@ -873,7 +892,7 @@ def erase_history():
@main.route("/<project_id>/strip_ip_addresses", methods=["POST"]) @main.route("/<project_id>/strip_ip_addresses", methods=["POST"])
def strip_ip_addresses(): def strip_ip_addresses():
"""Strip ip addresses from history entries associated with this project.""" """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(): if not form.validate():
flash( flash(
format_form_errors(form, _("Error deleting recorded IP addresses")), format_form_errors(form, _("Error deleting recorded IP addresses")),