diff --git a/migrations/versions/60a681c463a2_create_donors_override.py b/migrations/versions/60a681c463a2_create_donors_override.py new file mode 100644 index 00000000..62dc9b88 --- /dev/null +++ b/migrations/versions/60a681c463a2_create_donors_override.py @@ -0,0 +1,33 @@ +"""create_donors_override + +Revision ID: 60a681c463a2 +Revises: 467ee396a68e +Create Date: 2021-05-25 12:50:58.779359 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "60a681c463a2" +down_revision = "467ee396a68e" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "donors_override", + sa.Column("rodne_cislo", sa.CHAR(length=10), nullable=False), + sa.Column("first_name", sa.String(), nullable=True), + sa.Column("last_name", sa.String(), nullable=True), + sa.Column("address", sa.String(), nullable=True), + sa.Column("city", sa.String(), nullable=True), + sa.Column("postal_code", sa.CHAR(length=5), nullable=True), + sa.Column("kod_pojistovny", sa.CHAR(length=3), nullable=True), + sa.PrimaryKeyConstraint("rodne_cislo"), + ) + + +def downgrade(): + op.drop_table("donors_override") diff --git a/registry/donor/forms.py b/registry/donor/forms.py index a70b6984..5b7b12a8 100644 --- a/registry/donor/forms.py +++ b/registry/donor/forms.py @@ -2,7 +2,8 @@ from wtforms import BooleanField, HiddenField, StringField, TextAreaField from wtforms.validators import DataRequired -from registry.donor.models import AwardedMedals +from registry.donor.models import AwardedMedals, DonorsOverride +from registry.utils import NumericValidator class RemoveMedalForm(FlaskForm): @@ -46,3 +47,41 @@ class IgnoreDonorForm(FlaskForm): class RemoveFromIgnoredForm(FlaskForm): rodne_cislo = HiddenField(validators=[DataRequired()]) + + +class DonorsOverrideForm(FlaskForm): + rodne_cislo = StringField("Rodné číslo", validators=[DataRequired()]) + first_name = StringField("Jméno") + last_name = StringField("Příjmení") + address = StringField("Adresa") + city = StringField("Město") + postal_code = StringField("PSČ", validators=[NumericValidator(5)]) + kod_pojistovny = StringField("Pojišťovna", validators=[NumericValidator(3)]) + + _fields_ = [ + "rodne_cislo", + "first_name", + "last_name", + "address", + "city", + "postal_code", + "kod_pojistovny", + ] + + def init_fields(self, rodne_cislo): + override = DonorsOverride.query.get(rodne_cislo) + + if override is not None: + for field in self._fields_: + data = getattr(override, field) + if data is not None: + getattr(self, field).data = data + + self.rodne_cislo.data = rodne_cislo + + def get_field_data(self): + field_data = {} + for field in self._fields_: + field_data[field] = getattr(self, field).data or None + + return field_data diff --git a/registry/donor/models.py b/registry/donor/models.py index 897aeee3..bb1a5269 100644 --- a/registry/donor/models.py +++ b/registry/donor/models.py @@ -1,3 +1,5 @@ +import sqlite3 + from registry.extensions import db from registry.list.models import DonationCenter, Medals @@ -186,6 +188,111 @@ def dict_for_frontend(self): donor_dict["donations"]["total"] = self.donation_count_total return donor_dict + @classmethod + def refresh_override(cls, commit=True): + if sqlite3.sqlite_version_info >= (3, 33): + # The UPDATE - FROM syntax is supported from SQLite version 3.33.0 + db.session.execute( + """ +-- Rewrite rows in "donors_overview" that have a record in "donors_override" +-- with the corresponding values. +UPDATE "donors_overview" +SET + "first_name" = COALESCE( + "donors_override"."first_name", + "donors_overview"."first_name" + ), + "last_name" = COALESCE( + "donors_override"."last_name", + "donors_overview"."last_name" + ), + "address" = COALESCE( + "donors_override"."address", + "donors_overview"."address" + ), + "city" = COALESCE( + "donors_override"."city", + "donors_overview"."city" + ), + "postal_code" = COALESCE( + "donors_override"."postal_code", + "donors_overview"."postal_code" + ), + "kod_pojistovny" = COALESCE( + "donors_override"."kod_pojistovny", + "donors_overview"."kod_pojistovny" + ) +FROM "donors_override" +WHERE "donors_override"."rodne_cislo" = "donors_overview"."rodne_cislo"; + """ + ) + + # Note: because UPDATE - FROM is not a standard SQL construct, it is + # implemented differently in each database system. The SQLite query + # should be complatible with PostgreSQL, but for MySQL it would look + # something like this: + # UPDATE "donors_overview" + # INNER JOIN "donors_override" + # ON "donors_overview"."rodne_cislo" = + # "donors_override"."rodne_cislo" + # and there would be no FROM of WHERE clause. + + # TODO?: build this into the refresh_overview query, like this: + # INSERT INTO donors_overview (...) + # SELECT records.rodne_cislo, + # COALESCE(donors_override.first_name, + # records.first_name), + # ... + # FROM ... + # LEFT JOIN donors_override + # ON donors_override.rodne_cislo = records.rodne_cislo + else: + db.session.execute( + """ +UPDATE "donors_overview" +SET +"first_name" = COALESCE( + (SELECT "first_name" + FROM "donors_override" + WHERE "donors_override"."rodne_cislo" = "donors_overview"."rodne_cislo"), + "donors_overview"."first_name" +), +"last_name" = COALESCE( + (SELECT "last_name" + FROM "donors_override" + WHERE "donors_override"."rodne_cislo" = "donors_overview"."rodne_cislo"), + "donors_overview"."last_name" +), +"address" = COALESCE( + (SELECT "address" + FROM "donors_override" + WHERE "donors_override"."rodne_cislo" = "donors_overview"."rodne_cislo"), + "donors_overview"."address" +), +"city" = COALESCE( + (SELECT "city" + FROM "donors_override" + WHERE "donors_override"."rodne_cislo" = "donors_overview"."rodne_cislo"), + "donors_overview"."city" +), +"postal_code" = COALESCE( + (SELECT "postal_code" + FROM "donors_override" + WHERE "donors_override"."rodne_cislo" = "donors_overview"."rodne_cislo"), + "donors_overview"."postal_code" +), +"kod_pojistovny" = COALESCE( + (SELECT "kod_pojistovny" + FROM "donors_override" + WHERE "donors_override"."rodne_cislo" = "donors_overview"."rodne_cislo"), + "donors_overview"."kod_pojistovny" +); + """ + ) + + if commit: + db.session.commit() + @classmethod def refresh_overview(cls): cls.query.delete() @@ -437,7 +544,9 @@ def refresh_overview(cls): JOIN "records" ON "records"."id" = "recent_records"."record_id";""" ) + cls.remove_ignored() + cls.refresh_override(commit=False) db.session.commit() @@ -445,3 +554,32 @@ class Note(db.Model): __tablename__ = "notes" rodne_cislo = db.Column(db.String(10), primary_key=True) note = db.Column(db.Text) + + +class DonorsOverride(db.Model): + __tablename__ = "donors_override" + rodne_cislo = db.Column(db.String(10), primary_key=True) + first_name = db.Column(db.String) + last_name = db.Column(db.String) + address = db.Column(db.String) + city = db.Column(db.String) + postal_code = db.Column(db.String(5)) + kod_pojistovny = db.Column(db.String(3)) + + def to_dict(self): + result = {} + for field in [ + "rodne_cislo", + "first_name", + "last_name", + "address", + "city", + "postal_code", + "kod_pojistovny", + ]: + if getattr(self, field) is not None: + result[field] = str(getattr(self, field)) + else: + result[field] = None + + return result diff --git a/registry/donor/views.py b/registry/donor/views.py index 6b260eb1..26cc08a7 100644 --- a/registry/donor/views.py +++ b/registry/donor/views.py @@ -15,15 +15,24 @@ from registry.extensions import db from registry.list.models import DonationCenter, Medals +from registry.utils import flash_errors from .forms import ( AwardMedalForm, + DonorsOverrideForm, IgnoreDonorForm, NoteForm, RemoveFromIgnoredForm, RemoveMedalForm, ) -from .models import AwardedMedals, DonorsOverview, IgnoredDonors, Note, Record +from .models import ( + AwardedMedals, + DonorsOverride, + DonorsOverview, + IgnoredDonors, + Note, + Record, +) blueprint = Blueprint("donor", __name__, static_folder="../static") @@ -105,6 +114,9 @@ def detail(rc): note_form = NoteForm() if overview.note: note_form.note.data = overview.note.note + donors_override_form = DonorsOverrideForm() + donors_override_form.init_fields(rc) + return render_template( "donor/detail.html", overview=overview, @@ -115,6 +127,7 @@ def detail(rc): remove_medal_form=remove_medal_form, award_medal_form=award_medal_form, note_form=note_form, + donors_override_form=donors_override_form, ) @@ -263,3 +276,46 @@ def unignore_donor(): else: flash("Při odebírání ze seznamu ignorovaných dárců došlo k chybě", "danger") return redirect(url_for("donor.show_ignored")) + + +@blueprint.post("/override/") +@login_required +def save_override(): + form = DonorsOverrideForm() + delete = "delete_btn" in request.form + + if form.validate_on_submit(): + if not delete: + # Save the override + override = DonorsOverride(**form.get_field_data()) + db.session.merge(override) + db.session.commit() + + DonorsOverview.refresh_overview() + flash("Výjimka uložena", "success") + else: + # Delete the override + override = DonorsOverride.query.get(form.rodne_cislo.data) + if override is not None: + db.session.delete(override) + db.session.commit() + + DonorsOverview.refresh_overview() + flash("Výjimka smazána", "success") + else: + flash("Není co mazat", "warning") + else: + flash_errors(form) + + return redirect(url_for("donor.detail", rc=form.rodne_cislo.data)) + + +@blueprint.get("/override/all") +@login_required +def get_overrides(): + overrides_dict = {} + + for override in DonorsOverride.query.all(): + overrides_dict[override.rodne_cislo] = override.to_dict() + + return jsonify(overrides_dict) diff --git a/registry/static/donors_override_highlight.js b/registry/static/donors_override_highlight.js new file mode 100644 index 00000000..c3835a04 --- /dev/null +++ b/registry/static/donors_override_highlight.js @@ -0,0 +1,39 @@ +/** + * Sets up highlighting of values overriden in donors_override + * @param {string} url The URL to request (url_for('donor.get_overrides')) + * @param {object} columnDefs DataTables columnDefs + * @param {Function} onDataReady Called when information about overrides is downloaded + */ +function highlightOverridenValues(url, columnDefs, onDataReady) { + let overrides = {}; // Will be set by an AJAX request + + for (const column of ["first_name", "last_name", "address", "city", "postal_code", "kod_pojistovny"]) { + columnDefs.push({ + "targets": column, + "name": column, + "render": function (data, type, row, meta) { + // We only want to highlight the value that is displayed + // to the user, not the ones used for searching and sorting + if (type != "display") return data; + + let rodneCislo; + if (row instanceof Array) + rodneCislo = row[0] + else + rodneCislo = row.rodne_cislo; + + if (overrides.hasOwnProperty(rodneCislo) && overrides[rodneCislo][column]) { + return `${data}`; + } else { + return data; + } + } + }); + } + + // Request the list of overrides + $.getJSON(url, function (data) { + overrides = data; + onDataReady && onDataReady(); + }); +} \ No newline at end of file diff --git a/registry/templates/donor/award_prep.html b/registry/templates/donor/award_prep.html index e8189471..17da6896 100644 --- a/registry/templates/donor/award_prep.html +++ b/registry/templates/donor/award_prep.html @@ -14,13 +14,13 @@

Přehled dárců k ocenění: {{ medal.title }}

- - - - - - - + + + + + + + @@ -51,33 +51,42 @@

Přehled dárců k ocenění: {{ medal.title }}

{% endblock %} {% block js %} + diff --git a/registry/templates/layout.html b/registry/templates/layout.html index 45ae9ee3..39bc62b0 100644 --- a/registry/templates/layout.html +++ b/registry/templates/layout.html @@ -15,6 +15,13 @@ Evidence dárců ČČK Frýdek-Místek {% endblock %} + + + {% block css %}{% endblock %} diff --git a/tests/test_donor.py b/tests/test_donor.py index da3c9dbd..a5c9d051 100644 --- a/tests/test_donor.py +++ b/tests/test_donor.py @@ -87,3 +87,47 @@ def test_ignore(self, user, testapp, rodne_cislo): do = testapp.get(url_for("donor.detail", rc=rodne_cislo), status=200) assert do.status_code == 200 + + +class TestOverride: + @pytest.mark.parametrize("rodne_cislo", sample_of_rc(5)) + def test_override(self, user, testapp, rodne_cislo): + login(user, testapp) + res = testapp.get(url_for("donor.detail", rc=rodne_cislo)) + + old_data = DonorsOverview.query.get(rodne_cislo) + + # Test save + form = res.forms["donorsOverrideForm"] + form["first_name"] = "--First--" + form["last_name"] = "--Last--" + res = form.submit("save_btn").follow() + + assert "Výjimka uložena" in res + assert "Jméno: --First--" in res + assert "Příjmení: --Last--" in res + + # Test repeated save + form = res.forms["donorsOverrideForm"] + res = form.submit("save_btn").follow() + + assert "Výjimka uložena" in res + assert "Jméno: --First--" in res + assert "Příjmení: --Last--" in res + + # Test removing one field's value but keeping the other + form = res.forms["donorsOverrideForm"] + form["first_name"] = "" + res = form.submit("save_btn").follow() + + assert "Výjimka uložena" in res + assert ("Jméno: " + str(old_data.first_name)) in res + assert "Příjmení: --Last--" in res + + # Test deleting the override + form = res.forms["donorsOverrideForm"] + res = form.submit("delete_btn").follow() + + assert "Výjimka smazána" in res + assert ("Jméno: " + str(old_data.first_name)) in res + assert ("Příjmení: " + str(old_data.last_name)) in res
Rodné čísloJménoPřijmeníAdresaMěstoPSČPojišťovnaRodné čísloJménoPřijmeníAdresaMěstoPSČPojišťovna Darování Podpis Vybrat vše