From 0cffc7ec7ea3f1645ef59cde60352c40ffa91b8f Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Mon, 25 Jan 2021 11:07:49 +0100 Subject: [PATCH] Handle duplicate references #89 --- migrations/versions/35a6577b6af8_.py | 46 ++++++++++++++++++ project/models.py | 16 +++++- .../translations/de/LC_MESSAGES/messages.mo | Bin 23934 -> 24138 bytes .../translations/de/LC_MESSAGES/messages.po | 12 +++-- project/views/utils.py | 22 ++++++--- tests/utils.py | 4 +- tests/views/test_event.py | 3 +- tests/views/test_reference.py | 21 ++++++++ tests/views/test_reference_request.py | 30 ++++++++++++ 9 files changed, 141 insertions(+), 13 deletions(-) create mode 100644 migrations/versions/35a6577b6af8_.py diff --git a/migrations/versions/35a6577b6af8_.py b/migrations/versions/35a6577b6af8_.py new file mode 100644 index 0000000..89886f9 --- /dev/null +++ b/migrations/versions/35a6577b6af8_.py @@ -0,0 +1,46 @@ +"""empty message + +Revision ID: 35a6577b6af8 +Revises: a0a248667cd8 +Create Date: 2021-01-25 10:37:41.116909 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils +from project import dbtypes + + +# revision identifiers, used by Alembic. +revision = "35a6577b6af8" +down_revision = "a0a248667cd8" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint( + "eventreference_event_id_admin_unit_id", + "eventreference", + ["event_id", "admin_unit_id"], + ) + op.create_unique_constraint( + "eventreferencerequest_event_id_admin_unit_id", + "eventreferencerequest", + ["event_id", "admin_unit_id"], + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint( + "eventreferencerequest_event_id_admin_unit_id", + "eventreferencerequest", + type_="unique", + ) + op.drop_constraint( + "eventreference_event_id_admin_unit_id", "eventreference", type_="unique" + ) + # ### end Alembic commands ### diff --git a/project/models.py b/project/models.py index 384a1cd..9a5660d 100644 --- a/project/models.py +++ b/project/models.py @@ -373,6 +373,11 @@ def purge_event_organizer(mapper, connect, self): class EventReference(db.Model, TrackableMixin): __tablename__ = "eventreference" + __table_args__ = ( + UniqueConstraint( + "event_id", "admin_unit_id", name="eventreference_event_id_admin_unit_id" + ), + ) id = Column(Integer(), primary_key=True) event_id = db.Column(db.Integer, db.ForeignKey("event.id"), nullable=False) admin_unit_id = db.Column(db.Integer, db.ForeignKey("adminunit.id"), nullable=False) @@ -384,6 +389,13 @@ class EventReference(db.Model, TrackableMixin): class EventReferenceRequest(db.Model, TrackableMixin): __tablename__ = "eventreferencerequest" + __table_args__ = ( + UniqueConstraint( + "event_id", + "admin_unit_id", + name="eventreferencerequest_event_id_admin_unit_id", + ), + ) id = Column(Integer(), primary_key=True) event_id = db.Column(db.Integer, db.ForeignKey("event.id"), nullable=False) admin_unit_id = db.Column(db.Integer, db.ForeignKey("adminunit.id"), nullable=False) @@ -431,7 +443,9 @@ class EventMixin(object): class EventSuggestion(db.Model, TrackableMixin, EventMixin): __tablename__ = "eventsuggestion" __table_args__ = ( - CheckConstraint("NOT(event_place_id IS NULL AND event_place_text IS NULL)"), + CheckConstraint( + "NOT(event_place_id IS NULL AND event_place_text IS NULL)", + ), CheckConstraint("NOT(organizer_id IS NULL AND organizer_text IS NULL)"), ) id = Column(Integer(), primary_key=True) diff --git a/project/translations/de/LC_MESSAGES/messages.mo b/project/translations/de/LC_MESSAGES/messages.mo index 867963842914b5161dae27ebd96861cb23414c64..77be93a54dcd7f1a7c16529159df187bd1bdf160 100644 GIT binary patch delta 5815 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 K-uvBGw)`I~Nvoj% delta 5612 zcmYM%3viED8prXIdmi2D|+sIsM{!TOhm)-GBV+D@(8 z>a>jryU}*UTHCZ{R;le$6^+e|YG!L#)zU6y*zeE#%&f`ebI$pn_dU;f&hx(i`0Ou! z4{H5TE7Xy>0iS@ z{I8AQH~r(BOLETVLTTv2bTb>ZKt6`xNQ}a9n2d9fAGeNQT6hO)p}nZstC1VKBiJ4< zVhg;3=@=aE++fVcW~}d)(uibWHMYdf7>2tZO>pjg45fb<)9@H}#w(bQ;jO&@2jL|8 zPv8Ws!eaDq<7HqpX6SQF#?_d@`fisEe2j_ozd`NHwe>O(jLJX+D&@)6&&DqF2V!$v zggU~NNNny+)P@e(=O3fypFw`yJ%0IgHgWB|h1#KxA{BMROw__XQ7IgOy5S`Ie4h26 zM+LSXbrjoB8Qg=)+~1BIvz6`?XT8nxkRNG5&m8T+8jT#u@Ot*8|5wedr! z)E_fXqEda{`c2mV0d>7!M{mKFsQV@3Ft^W8o9dQ-r;yKJ?eHX&0QrQPpwWatvF2tqy z5Cd^3Zylxb1x&`5%zdaMIDtCjCe#M}dEEtL1gam4A((bF*#eue1a+e_ zb2X}HUP2XTg^gF52krC2sGXj`F#IQ~C>zXQ%r-nC+EA}d@~;2}GoaMZ#2{R1mYF`( zQ?kz7Z5~DK^efZ`{)PRq36-I=EU#aT3Tz{4-kYdQ?)K5p!tY})zoP>A3U$FH z^IOz{*Uej~r{EW}Sr;$RwiwNL4^#$*n@^eRQN`(dhX!9%t^pOmZPYveAu82TUA^}} zXVek&N6jCDnl~S{)0H?2_o9k0h_{s%?tpra3`9Mald%QPLpJ7f%V=n)t5GQ_N1gp9 zjKJ5iIqt!5tVX>dYt1jQ8~q!ofRlJFQje|&K8`bRG*+RC{61=(1TIR{^PfgTJ0FaN zSb_>*14iOzb0_Mks!^$}Lj```Y(O2&W%E1pd({1YMs4IC>OP@7dM#PsCD0%!*A;n- zy6H&v-HYY{NHJ;$lWly4IR_QU0#pXxLS6q3 z#$pv}V|AziPx)w25AGW31(Q(V1<=zRidt|2>a1sB8kVDuq7s#Xqc|RKpcWoPB`cF9 zsClKROwTu;v(J4#8VX*p0yHnyfe0v}Z~D&xM?KG;tm+HwV(r~p4f-Txdau&b#1e~*Da8UX`H3x*<_bqjF_Rw9M!ZsI}g%=u`C7f?I6 zg>x{N5}$}qBggOR%=^d`m(NQ>zJU6?78THK^y#^7R^;udHR`cQMpbPN>Ie!@J0FV) zI1M{s8K&Y6^KYmc_!f0tIJ?l%rJ@2Ji^|*_)cqF@A^$p~r3@&Qt5CJ@3aWbFKrOt- z`c}x6He!1s)Sd zjhfdEb>kFNk!7If6`+c55C-5>491zL>Yt0s;BQd@_%_g(O``%s@DA!iH{1&>5cL8I z!|fP{D$)bU_mR7XJuz{F_w6|WRcxE6Oy+}P zOu}c(O{g0kz;HZ`p;(V9s>>LLx3LvIK;B(0rr7%*Q3;0AUxu2u7M1ahkNTYd78<(Y z+t>;BVGf?M@dv02LLT>S&>VHcI8+Kdpsw$V3N#;6aDsil9CZY1QTN$kZpK7C|66G& zMSns~Jc<7J1?q7;ht09c{1LUlL)2rKQsTX0i%|0xpaNNry6<+>(N?1Z_z-o(N71KZ zIZi`wuzFPW|A?9xIoi9h6_Ny(fL(CD^|zxIyn@QWP1HhnQ7;(3G2R`m2#YC4BDBGzKtm1K-8$Qtw6=us{8~I1CFWdquhiwS%`&0UkjW z)2FB%e}M|%BF5r>tRFDN8;?hQi)LXQ>$_nz+A=T$btYw~jQk$+aUaHG1Gd6jsNxHq z>MfX#9qD(s{zQzUKNHJwnT_8=1spid%Wx#N8^S;w4W%{*^RN&H;9|_gL)a4=Pz%RQ wcWx-=qMqXw(`zbYf8$s4T>L+hLh_Qk7Zi5uRWq_@qyN?iBeQFc6mCiVAGd5)-v9sr diff --git a/project/translations/de/LC_MESSAGES/messages.po b/project/translations/de/LC_MESSAGES/messages.po index 992dc0d..298f193 100644 --- a/project/translations/de/LC_MESSAGES/messages.po +++ b/project/translations/de/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-01-25 09:17+0100\n" +"POT-Creation-Date: 2021-01-25 10:17+0100\n" "PO-Revision-Date: 2020-06-07 18:51+0200\n" "Last-Translator: FULL NAME \n" "Language: de\n" @@ -1634,12 +1634,18 @@ msgstr "Empfehlung erfolgreich erstellt" msgid "Request successfully updated" msgstr "Empfehlungsanfrage erfolgreich aktualisiert" -#: project/views/utils.py:53 +#: project/views/utils.py:30 +msgid "" +"An entry with the entered values ​​already exists. Duplicate entries are " +"not allowed." +msgstr "Ein Eintrag mit den eingegebenen Werten existiert bereits. Doppelte Einträge sind nicht erlaubt." + +#: project/views/utils.py:61 #, python-format msgid "Error in the %s field - %s" msgstr "Fehler im Feld %s: %s" -#: project/views/utils.py:61 +#: project/views/utils.py:69 msgid "Show" msgstr "Anzeigen" diff --git a/project/views/utils.py b/project/views/utils.py index 733f69d..4d24c06 100644 --- a/project/views/utils.py +++ b/project/views/utils.py @@ -4,6 +4,7 @@ from flask_babelex import gettext from flask import request, url_for, render_template, flash, redirect, Markup from flask_mail import Message from sqlalchemy.exc import SQLAlchemyError +from psycopg2.errorcodes import UNIQUE_VIOLATION def track_analytics(key, value1, value2): @@ -19,12 +20,21 @@ def track_analytics(key, value1, value2): def handleSqlError(e: SQLAlchemyError) -> str: - if e.orig: - message = str(e.orig) - else: - message = str(e) - print(message) - return message + if not e.orig: + return str(e) + + prefix = None + message = str(e.orig) + + if e.orig.pgcode == UNIQUE_VIOLATION: + prefix = gettext( + "An entry with the entered values ​​already exists. Duplicate entries are not allowed." + ) + + if not prefix: + return message + + return "%s (%s)" % (prefix, message) def get_pagination_urls(pagination, **kwargs): diff --git a/tests/utils.py b/tests/utils.py index 51983e7..e405712 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -86,10 +86,10 @@ class UtilActions(object): assert response.content_type == "application/json" return response.json - def mock_db_commit(self, mocker): + def mock_db_commit(self, mocker, orig=None): mocked_commit = mocker.patch("project.db.session.commit") mocked_commit.side_effect = IntegrityError( - "MockException", "MockException", None + "MockException", "MockException", orig ) def mock_send_mails(self, mocker): diff --git a/tests/views/test_event.py b/tests/views/test_event.py index 0828982..ddaaa93 100644 --- a/tests/views/test_event.py +++ b/tests/views/test_event.py @@ -1,4 +1,5 @@ import pytest +from psycopg2.errors import UniqueViolation def test_read(client, seeder, utils): @@ -19,7 +20,7 @@ def test_create(client, app, utils, seeder, mocker, db_error): response = utils.get_ok(url) if db_error: - utils.mock_db_commit(mocker) + utils.mock_db_commit(mocker, UniqueViolation("MockException", "MockException")) response = utils.post_form( url, diff --git a/tests/views/test_reference.py b/tests/views/test_reference.py index 7e7e833..2f267e8 100644 --- a/tests/views/test_reference.py +++ b/tests/views/test_reference.py @@ -49,6 +49,27 @@ def test_create(client, app, utils, seeder, mocker, db_error): assert reference is not None +def test_create_duplicateNotAllowed(client, seeder, utils, app): + user_id, admin_unit_id = seeder.setup_base() + ( + other_user_id, + other_admin_unit_id, + event_id, + reference_id, + ) = seeder.create_any_reference(admin_unit_id) + + url = utils.get_url("event_reference_create", event_id=event_id) + response = utils.get_ok(url) + + response = utils.post_form( + url, + response, + {"admin_unit_id": admin_unit_id}, + ) + utils.assert_response_ok(response) + assert b"duplicate key" in response.data + + def test_create_401(client, app, utils, seeder, mocker): seeder.create_user() seeder._utils.login() diff --git a/tests/views/test_reference_request.py b/tests/views/test_reference_request.py index c528e54..06e2ae5 100644 --- a/tests/views/test_reference_request.py +++ b/tests/views/test_reference_request.py @@ -50,6 +50,36 @@ def test_create(client, app, utils, seeder, mocker, db_error): ) +def test_create_duplicateNotAllowed(client, app, utils, seeder): + user_id, admin_unit_id = seeder.setup_base() + event_id = seeder.create_event(admin_unit_id) + other_user_id = seeder.create_user("other@test.de") + other_admin_unit_id = seeder.create_admin_unit(other_user_id, "Other Crew") + + # First + url = utils.get_url("event_reference_request_create", event_id=event_id) + response = utils.get_ok(url) + response = utils.post_form( + url, + response, + {"admin_unit_id": other_admin_unit_id}, + ) + utils.assert_response_redirect( + response, "manage_admin_unit_reference_requests_outgoing", id=admin_unit_id + ) + + # Second + url = utils.get_url("event_reference_request_create", event_id=event_id) + response = utils.get_ok(url) + response = utils.post_form( + url, + response, + {"admin_unit_id": other_admin_unit_id}, + ) + utils.assert_response_ok(response) + assert b"duplicate key" in response.data + + def test_admin_unit_reference_requests_incoming(client, seeder, utils): user_id, admin_unit_id = seeder.setup_base() seeder.create_incoming_reference_request(admin_unit_id)