From f88eae64ab57e7ddf393f555239d70f0588ccc35 Mon Sep 17 00:00:00 2001 From: Pitastic Date: Fri, 3 Oct 2025 21:57:31 +0200 Subject: [PATCH 01/12] =?UTF-8?q?Rudiment=C3=A4rer=20neuer=20Flow=20(meist?= =?UTF-8?q?en=20API=20Functions=20fehlen=20noch)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/css/style.css | 7 +- app/static/js/functions.js | 24 ++++++ app/static/js/index.js | 101 +++++++++++++++++++++--- app/static/js/welcome.js | 31 ++++++++ app/templates/index.html | 157 ++++++++++++++++--------------------- app/templates/layout.html | 7 -- app/templates/welcome.html | 50 ++++++++++++ app/ui.py | 43 +++++----- handler/BaseDb.py | 57 ++++++++++---- handler/MongoDb.py | 9 +++ handler/TinyDb.py | 9 +++ tests/test_integ_basics.py | 24 +++--- 12 files changed, 359 insertions(+), 160 deletions(-) create mode 100644 app/static/js/welcome.js create mode 100644 app/templates/welcome.html diff --git a/app/static/css/style.css b/app/static/css/style.css index 9193f2a..34803dd 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -11,9 +11,14 @@ html, body { background-color: silver; } +.popup { + border: 1px solid silver; + display: inline-block; +} + #result-text { display: block; - background-color: silver; + background-color: green; padding: 1em; margin: 1em; } diff --git a/app/static/js/functions.js b/app/static/js/functions.js index f1072a7..5e070f2 100644 --- a/app/static/js/functions.js +++ b/app/static/js/functions.js @@ -1,5 +1,29 @@ "use strict"; +// ---------------------------------------------------------------------------- +// -- DOM Functions ---------------------------------------------------------- +// ---------------------------------------------------------------------------- + + +/** + * Opens a popup to display details + * + * @param {string} id - The ID of the HTML element to display as a popup. + */ +function openPopup(id) { + document.getElementById(id).style.display = 'block'; +} + +/** + * Closes a popup by setting its display style to 'none'. + * + * @param {string} popupId - The ID of the popup element to be closed. + */ +function closePopup(popupId) { + document.getElementById(popupId).style.display = 'none'; +} + + // ---------------------------------------------------------------------------- // -- AJAX Functions ---------------------------------------------------------- // ---------------------------------------------------------------------------- diff --git a/app/static/js/index.js b/app/static/js/index.js index f87b8a5..e936b10 100644 --- a/app/static/js/index.js +++ b/app/static/js/index.js @@ -1,23 +1,104 @@ "use strict"; +let rowCheckboxes = null; + document.addEventListener('DOMContentLoaded', function () { - // Upload Button Listener - //document.getElementById('uploadButton').addEventListener('click', uploadFile); + // PopUps + document.getElementById('settings-button').addEventListener('click', function () { + openPopup('settings-popup'); + }); + + // Additional JavaScript for enabling/disabling the edit button based on checkbox selection + const selectAllCheckbox = document.getElementById('select-all'); + rowCheckboxes = document.querySelectorAll('.row-checkbox'); + + selectAllCheckbox.addEventListener('change', function () { + rowCheckboxes.forEach(checkbox => { + checkbox.checked = selectAllCheckbox.checked; + }); + updateEditButtonState(); + }); + + rowCheckboxes.forEach(checkbox => { + checkbox.addEventListener('change', function () { + if (!this.checked) { + selectAllCheckbox.checked = false; + } else if (Array.from(rowCheckboxes).every(cb => cb.checked)) { + selectAllCheckbox.checked = true; + } + updateEditButtonState(); + }); + }); }); +// ---------------------------------------------------------------------------- +// -- DOM Functions ----------------------------------------------------------- +// ---------------------------------------------------------------------------- + +/** + * Opens a popup to display details for a specific element and optionally fetches transaction details. + * + * @param {string} id - The ID of the HTML element to display as a popup. + * @param {string|null} [tx_hash=null] - Optional transaction hash to fetch additional details for. + */ +function openDetailsPopup(id, tx_hash = null) { + if (tx_hash) { + // Use AJAX to fetch and populate details for the transaction with the given ID + console.log(`Fetching details for transaction hash: ${tx_hash}`); + const currentURI = window.location.pathname; + const iban = currentURI.split('/').pop(); + resetDetails(); + getInfo(iban, tx_hash, fillTxDetails); + openPopup(id); + + } else { + openPopup(id); + + } +} + + +/** + * Clears information from a result Box +* +*/ +function resetDetails() { + const box = document.getElementById('result-text'); + box.innerHTML = ""; +} + /** * Shows a given Result in the Result-Box. * * @param {string} result - The text to be shwon. */ -function printResult(result){ +function fillTxDetails(result){ const box = document.getElementById('result-text'); box.innerHTML = result; } +/** + * Updates the state of the "Edit Selected" button based on the checkbox selections. + * + * This function checks if any row checkboxes are selected and enables or disables + * the "Edit Selected" button accordingly. It also updates the button's title to + * reflect the number of selected checkboxes. + * + * Assumes that `rowCheckboxes` is a collection of checkbox elements and that + * there is a button with the ID `edit-selected` in the DOM. + */ +function updateEditButtonState() { + const anyChecked = Array.from(rowCheckboxes).some(cb => cb.checked); + const editButton = document.getElementById('edit-selected'); + editButton.disabled = !anyChecked; + editButton.title = anyChecked + ? `Edit selected (${Array.from(rowCheckboxes).filter(cb => cb.checked).length} selected)` + : 'Edit selected (0 selected)'; +} + /** * Sends a file to the server for upload. @@ -188,21 +269,19 @@ function manualTagEntries() { /** - * Fetches information based on the provided UUID and IBAN input value. + * Fetches information based on the provided IBAN and UUID, and processes the response. * - * @param {string} uuid - The unique identifier used to fetch specific information. - * - * This function retrieves the info for a given uuid from the server. + * @param {string} iban - The International Bank Account Number (IBAN) to identify the account. + * @param {string} uuid - The unique identifier associated with the request. + * @param {Function} [callback=alert] - A callback function to handle the response text. Defaults to `alert`. */ -function getInfo(uuid) { - const iban = document.getElementById('input_iban').value; - +function getInfo(iban, uuid, callback = alert) { apiGet('/'+iban+'/'+uuid, {}, function (responseText, error) { if (error) { printResult('getTx failed: ' + '(' + error + ')' + responseText); } else { - alert(responseText); + callback(responseText); } }); diff --git a/app/static/js/welcome.js b/app/static/js/welcome.js new file mode 100644 index 0000000..69fbbc4 --- /dev/null +++ b/app/static/js/welcome.js @@ -0,0 +1,31 @@ +"use strict"; + +document.addEventListener('DOMContentLoaded', function () { + + // PopUps + document.getElementById('add-button').addEventListener('click', function () { + openPopup('add-popup'); + }); + + // Import Input + const dropArea = document.getElementById('file-drop-area'); + const fileInput = document.getElementById('file-input'); + + dropArea.addEventListener('click', () => fileInput.click()); + + dropArea.addEventListener('dragover', (event) => { + event.preventDefault(); + dropArea.style.borderColor = '#000'; + }); + + dropArea.addEventListener('dragleave', () => { + dropArea.style.borderColor = '#ccc'; + }); + + dropArea.addEventListener('drop', (event) => { + event.preventDefault(); + dropArea.style.borderColor = '#ccc'; + fileInput.files = event.dataTransfer.files; + }); + +}); \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index d3bc34c..9260f6e 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,114 +1,89 @@ + {% extends 'layout.html' %} {% block content %} -
- +
+ +
+ +
- - - {% for entry in table_header %} - - {% endfor %} - - - + + + + + + + + + + + - {% for row in table_data %} - - - {% for key in table_header %} - {% if key != 'parsed' %} - - {% endif %} - {% endfor %} - + + + + + + {% endfor %}
 {{ entry }} 
DateTextCategoryTagsBetrag 
{{ row.get(key) or '' }} - {% for info in row.get('parsed') %} -

{{ info }}

+ {% for transaction in transactions %} +
{{ transaction.date_tx }}{{ transaction.text_tx }} + 🔖 + + {% for tag in transaction.tags %} + + {{ tag }} + {% endfor %} {{ transaction.currency }} {{ transaction.betrag }} - (i) +
+
+ + - - -
- -

Actions for

- - -
-
-

Datenbank

- - - -
-
-

Tagging

- - - - - - -
-
-

Metadata

- - - -
-
- + - -
- -

Message Box

-
 
- + -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/layout.html b/app/templates/layout.html index 8f6934e..01c6f62 100644 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -8,13 +8,6 @@ -
- -
{% block content %} diff --git a/app/templates/welcome.html b/app/templates/welcome.html new file mode 100644 index 0000000..feaebbf --- /dev/null +++ b/app/templates/welcome.html @@ -0,0 +1,50 @@ +{% extends 'layout.html' %} + +{% block content %} + +
+ {% if ibans %} + Wähle ein Konto oder Gruppe: +
    + {% for iban in ibans %} +
  • {{iban}}
  • + {% endfor %} +
+ {% else %} + Es sind noch keine Konten vorhanden. Starte mit deinem ersten Import... + {% endif %} + +

+ +

+
+ + + + + + +{% endblock %} diff --git a/app/ui.py b/app/ui.py index 5cd306e..96529fb 100644 --- a/app/ui.py +++ b/app/ui.py @@ -6,7 +6,7 @@ import json import logging from datetime import datetime -from flask import request, current_app, render_template, make_response, send_from_directory +from flask import request, current_app, render_template, redirect, make_response, send_from_directory # Add Parent for importing Classes parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -80,12 +80,18 @@ def index(iban) -> str: Returns: html: Startseite mit Navigation """ + if iban is None: + # No IBAN selected, show Welcome page with IBANs + ibans = self.db_handler.list_ibans() + return render_template('welcome.html', ibans=ibans) + + if not self.db_handler.check_collection_is_iban(iban): + # Do not treat URI as IBAN + return "", 404 + # Check filter args condition = [] - if iban is None: - iban = current_app.config['IBAN'] - start_date = request.args.get('startDate') if start_date is not None: # Convert to valid date format @@ -116,22 +122,19 @@ def index(iban) -> str: # Table with Transactions rows = self.db_handler.select(iban, condition) - table_header = ['date_tx', 'betrag', 'currency', - 'category', 'tags', - 'prio', 'parsed'] - - # Rules for Selection - rules = self.db_handler.filter_metadata({ - 'key': 'metatype', - 'value': 'rule' - }) - rule_list = [] - for rule in rules: - rule_list.append(rule.get('name')) - - return render_template('index.html', iban=iban, - table_header=table_header, - table_data=rows, rule_list=rule_list) + ibans = self.db_handler.get_group_ibans(iban, check_before=True) + + return render_template('index.html', transactions=rows) + + @current_app.route('/logout', methods=['GET']) + def logout(): + """ + Loggt den User aus der Session aus und leitet zur Startseite weiter. + + Returns: + redirect: Weiterleitung zur Startseite + """ + return redirect('/') @current_app.route('/sw.js') def sw(): diff --git a/handler/BaseDb.py b/handler/BaseDb.py index eac3762..f8f6863 100644 --- a/handler/BaseDb.py +++ b/handler/BaseDb.py @@ -7,8 +7,8 @@ import logging import glob import json +from datetime import datetime from natsort import natsorted -from flask import current_app class BaseDb(): @@ -21,7 +21,7 @@ def create(self): """Erstellen des Datenbankspeichers""" raise NotImplementedError() - def select(self, collection:str=None, condition: dict|list[dict]=None, multi: str='AND'): + def select(self, collection:str, condition: dict|list[dict]=None, multi: str='AND'): """ Handler für das Vorbereiten der '_select' Methode, welche Datensätze aus der Datenbank selektiert, die die angegebene Bedingung erfüllen. @@ -30,7 +30,6 @@ def select(self, collection:str=None, condition: dict|list[dict]=None, multi: st collection (str, optional): Name der Collection oder Gruppe, aus der selektiert werden soll. Es erfolgt automatisch eine Unterscheidung, ob es sich um eine IBAN oder einen Gruppenname handelt. - Default: IBAN aus der Config. condition (dict | list(dict)): Bedingung als Dictionary - 'key', str : Spalten- oder Schlüsselname, - 'value', any : Wert der bei 'key' verglichen werden soll @@ -48,11 +47,7 @@ def select(self, collection:str=None, condition: dict|list[dict]=None, multi: st # Catch empty lists condition = None - if collection is None: - # Default Collection - collection = current_app.config['IBAN'] - - if not self._check_collection_is_iban(collection): + if not self.check_collection_is_iban(collection): # collection is a group group_ibans = self.get_group_ibans(collection) if not group_ibans: @@ -65,7 +60,13 @@ def select(self, collection:str=None, condition: dict|list[dict]=None, multi: st if not isinstance(collection, list): collection = [collection] - return self._select(collection, condition, multi) + result_list = self._select(collection, condition, multi) + for r in result_list: + # Format Datestrings + r['date_tx'] = datetime.fromtimestamp(r['date_tx']).strftime('%d.%m.%Y') + r['date_wert'] = datetime.fromtimestamp(r['date_wert']).strftime('%d.%m.%Y') + + return result_list def _select(self, collection: str, condition: dict|list[dict], multi: str): """ @@ -78,22 +79,18 @@ def _select(self, collection: str, condition: dict|list[dict], multi: str): """ raise NotImplementedError() - def insert(self, data: dict|list[dict], collection: str=None): + def insert(self, data: dict|list[dict], collection: str): """ Fügt einen oder mehrere Datensätze in die Datenbank ein. Args: data (dict or list): Einzelner Datensatz oder eine Liste von Datensätzen collection (str, optional): Name der Collection, in die Werte eingefügt werden sollen. - Default: IBAN aus der Config. Returns: dict: - inserted, int: Zahl der neu eingefügten IDs """ - if collection is None: - collection = current_app.config['IBAN'] - - if not self._check_collection_is_iban(collection): + if not self.check_collection_is_iban(collection): raise ValueError(f"Collection {collection} is not a valid IBAN") # Always create a list of transactions for loop @@ -220,15 +217,21 @@ def set_metadata(self, entry: dict, overwrite: bool=True): """ raise NotImplementedError() - def get_group_ibans(self, group: str): + def get_group_ibans(self, group: str, check_before: bool=False): """ Ruft die Liste von IBANs einer Gruppe aus der Datenbank ab. Args: group (str): Name der Gruppe. + check_before (bool): Wenn True, wird überprüft, ob es + sich um eine Gruppe oder IBAN handelt. + Default: False Returns: list: Die IBANs der abgerufene Gruppe. """ + if check_before and self.check_collection_is_iban(group): + return [group] + meta_results = self.filter_metadata([ { 'key': 'metatype', @@ -248,6 +251,26 @@ def get_group_ibans(self, group: str): return ibans + def list_ibans(self): + """ + Listet alle in der Datenbank vorhandenen IBAN-Collections auf. + + Returns: + list: Liste der IBAN-Collections. + """ + all_collections = self._get_collections() + ibans = [col for col in all_collections if self.check_collection_is_iban(col)] + return ibans + + def _get_collections(self): + """ + Ruft alle Collections in der Datenbank ab. + + Returns: + list: Liste der Collections. + """ + raise NotImplementedError() + def _generate_unique(self, tx_entry: dict | list[dict]): """ Erstellt einen einmaligen ID für jede Transaktion aus den Transaktionsdaten. @@ -391,7 +414,7 @@ def import_metadata(self, path: str=None, metatype: str='rule'): logging.info(f"Stored {inserted} imported metadata from {path}") return {'inserted': inserted} - def _check_collection_is_iban(self, collection: str): + def check_collection_is_iban(self, collection: str): """ Überprüft, ob die angegebene Collection eine IBAN ist. diff --git a/handler/MongoDb.py b/handler/MongoDb.py index 925e95a..0b8c18c 100644 --- a/handler/MongoDb.py +++ b/handler/MongoDb.py @@ -361,3 +361,12 @@ def _form_complete_query(self, condition, multi='AND'): query = self._form_condition(condition) return query + + def _get_collections(self): + """ + Liste alle collections der Datenbank. + + Returns: + list: A list of collection names. + """ + return self.connection.list_collection_names() diff --git a/handler/TinyDb.py b/handler/TinyDb.py index 504480c..e4b6cb4 100644 --- a/handler/TinyDb.py +++ b/handler/TinyDb.py @@ -417,3 +417,12 @@ def _double_check(self, collection: str, data: list|dict): def _none_of_test(self, value, forbidden_values): """Benutzerdefinierter Test: Keines der Elemente ist in einer Liste vorhanden""" return not any(item in forbidden_values for item in value) + + def _get_collections(self): + """ + Liste alle tables der Datenbank. + + Returns: + list: A list of table names. + """ + return self.connection.tables() diff --git a/tests/test_integ_basics.py b/tests/test_integ_basics.py index 52195bb..31e487f 100644 --- a/tests/test_integ_basics.py +++ b/tests/test_integ_basics.py @@ -47,7 +47,7 @@ def test_upload_csv_commerzbank(test_app): # Visit Form result = client.get('/') assert result.status_code == 200, "Der Statuscode der Startseite war falsch" - assert 'Message Box' in result.text, \ + assert 'All rights reserved.' in result.text, \ "Special Heading not found in the response" # Prepare File @@ -65,16 +65,18 @@ def test_upload_csv_commerzbank(test_app): assert result.json.get('filename') == 'commerzbank.csv', \ "Angaben zum Upload wurden nicht gefunden" - # Aufruf der Transaktionen auf verschiedene Weisen - response1 = client.get("/") + # Aufruf der Transaktionen response2 = client.get(f"/{test_app.config['IBAN']}") - assert response1.status_code == response2.status_code == 200, \ + assert response2.status_code == 200, \ "Die Ergebnisseite mit den Transaktionen ist nicht (richtig) erreichbar" - assert response2.text == response1.text, \ - "Der Aufruf des DEFAULT Kontos aus der Konfig ist nicht richtig" + + # Aufruf der Umleitung von Logout nach Welcome + response3 = client.get('/logout') + assert response3.status_code == 302, \ + "Die Umleitung von Logout nach Welcome ist nicht (richtig) erreichbar" # -- Check Parsing -- - soup = BeautifulSoup(response1.text, features="html.parser") + soup = BeautifulSoup(response2.text, features="html.parser") # 1. Example tx_hash = 'cf1fb4e6c131570e4f3b2ac857dead40' @@ -83,7 +85,7 @@ def test_upload_csv_commerzbank(test_app): f"Es wurden {len(row1)} rows für das erste Beispiel gefunden" content = row1[0].css.filter('.td-betrag')[0].contents[0] - assert content == '-11.63', \ + assert content == 'EUR -11.63', \ f"Der Content von {tx_hash} ist anders als erwartet: '{content}'" # 2. Example @@ -93,13 +95,9 @@ def test_upload_csv_commerzbank(test_app): f"Es wurden {len(row2)} rows für das zweite Beispiel gefunden" content = row2[0].css.filter('.td-betrag')[0].contents[0] - assert content == '-221.98', \ + assert content == 'EUR -221.98', \ f"Der Content von {tx_hash} / 'betrag' ist anders als erwartet: '{content}'" - content = [child.contents[0] for child in row2[0].select('.td-parsed p')] - assert 'Mandatsreferenz' in content, \ - f"Der Content von {tx_hash} / 'parsed' ist anders als erwartet: '{content}'" - def test_reachable_endpoints(test_app): """ From 53c2ab1d6f569bc69e95b88aa11684d0f7c89ff4 Mon Sep 17 00:00:00 2001 From: Pitastic Date: Fri, 3 Oct 2025 22:00:56 +0200 Subject: [PATCH 02/12] Fix Pytests --- handler/BaseDb.py | 7 +++++-- tests/test_unit_handler_DB.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/handler/BaseDb.py b/handler/BaseDb.py index f8f6863..a4e6b77 100644 --- a/handler/BaseDb.py +++ b/handler/BaseDb.py @@ -63,8 +63,11 @@ def select(self, collection:str, condition: dict|list[dict]=None, multi: str='AN result_list = self._select(collection, condition, multi) for r in result_list: # Format Datestrings - r['date_tx'] = datetime.fromtimestamp(r['date_tx']).strftime('%d.%m.%Y') - r['date_wert'] = datetime.fromtimestamp(r['date_wert']).strftime('%d.%m.%Y') + if isinstance(r.get('date_tx'), int): + r['date_tx'] = datetime.fromtimestamp(r['date_tx']).strftime('%d.%m.%Y') + + if isinstance(r.get('date_wert'), int): + r['date_wert'] = datetime.fromtimestamp(r['date_wert']).strftime('%d.%m.%Y') return result_list diff --git a/tests/test_unit_handler_DB.py b/tests/test_unit_handler_DB.py index 81abbbc..0d6eeee 100644 --- a/tests/test_unit_handler_DB.py +++ b/tests/test_unit_handler_DB.py @@ -74,7 +74,7 @@ def test_select_filter(test_app): assert len(result_filtered) == 1, \ f"Es wurde die falsche Zahl an Datensätzenzurückgegeben: {len(result_filtered)}" for entry in result_filtered: - check_entry(entry, key_vals={'date_tx': 1672617600, 'betrag': -118.94}) + check_entry(entry, key_vals={'date_tx': '02.01.2023', 'betrag': -118.94}) # Selektieren mit Filter (by Art) query = {'key': 'art', 'value': 'Lastschrift'} From 7225550e7c19d7d165405fba057afa5b939bc425 Mon Sep 17 00:00:00 2001 From: Pitastic Date: Sat, 4 Oct 2025 21:47:07 +0200 Subject: [PATCH 03/12] Pytests and Fixes --- handler/BaseDb.py | 4 +++- handler/TinyDb.py | 16 +++++++++---- tests/commerzbank.json | 18 ++++---------- tests/commerzbank2.json | 8 ++----- tests/test_unit_handler_DB.py | 44 +++++++++++++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 25 deletions(-) diff --git a/handler/BaseDb.py b/handler/BaseDb.py index a4e6b77..4a996dc 100644 --- a/handler/BaseDb.py +++ b/handler/BaseDb.py @@ -111,7 +111,9 @@ def insert(self, data: dict|list[dict], collection: str): # - IBAN, Tagging priority, empty Tag list transaction['iban'] = collection transaction['prio'] = 0 - transaction['tags'] = [] + transaction['category'] = transaction.get('category') + if not transaction.get('tags'): + transaction['tags'] = [] return self._insert(tx_list, collection) diff --git a/handler/TinyDb.py b/handler/TinyDb.py index e4b6cb4..3f22e36 100644 --- a/handler/TinyDb.py +++ b/handler/TinyDb.py @@ -293,11 +293,17 @@ def _form_where(self, condition): condition_method = condition.get('compare', '==') condition_key = condition.get('key') condition_val = condition.get('value') - try: - # Transfer to a number for comparison - condition_val = float(condition_val) - except (TypeError, ValueError): - pass + + if isinstance(condition_val, list): + # All types in query have to be hashable in TinyDB + condition_val = tuple(condition_val) + + else: + try: + # Transfer to a number for comparison + condition_val = float(condition_val) + except (TypeError, ValueError): + pass # Nested or Plain Key if isinstance(condition_key, dict): diff --git a/tests/commerzbank.json b/tests/commerzbank.json index aaa61c1..5a4f9a6 100644 --- a/tests/commerzbank.json +++ b/tests/commerzbank.json @@ -7,9 +7,7 @@ "betrag": -11.63, "iban": "DE89370400440532013000", "currency": "EUR", - "parsed": {}, - "category": null, - "tags": null + "parsed": {} }, { "date_tx": 1672617600, @@ -19,9 +17,7 @@ "betrag": -118.94, "iban": "DE89370400440532013000", "currency": "EUR", - "parsed": {}, - "category": null, - "tags": null + "parsed": {} }, { "date_tx": 1672704000, @@ -32,8 +28,7 @@ "iban": "DE89370400440532013000", "currency": "EUR", "parsed": {}, - "category": null, - "tags": null + "tags": ["TestTag3"] }, { "date_tx": 1672790400, @@ -43,9 +38,7 @@ "betrag": -71.35, "iban": "DE89370400440532013000", "currency": "EUR", - "parsed": {}, - "category": null, - "tags": null + "parsed": {} }, { "date_tx": 1672876800, @@ -56,7 +49,6 @@ "iban": "DE89370400440532013000", "currency": "EUR", "parsed": {}, - "category": null, - "tags": null + "tags": ["TestTag1", "TestTag2"] } ] \ No newline at end of file diff --git a/tests/commerzbank2.json b/tests/commerzbank2.json index 73635db..3e3e022 100644 --- a/tests/commerzbank2.json +++ b/tests/commerzbank2.json @@ -7,9 +7,7 @@ "betrag": -71.35, "iban": "DE89370400440532011111", "currency": "EUR", - "parsed": {}, - "category": null, - "tags": null + "parsed": {} }, { "date_tx": 1672876800, @@ -19,8 +17,6 @@ "betrag": -221.98, "iban": "DE89370400440532011111", "currency": "EUR", - "parsed": {}, - "category": null, - "tags": null + "parsed": {} } ] diff --git a/tests/test_unit_handler_DB.py b/tests/test_unit_handler_DB.py index 0d6eeee..618fdef 100644 --- a/tests/test_unit_handler_DB.py +++ b/tests/test_unit_handler_DB.py @@ -132,6 +132,41 @@ def test_select_not_eq(test_app): for entry in result_filtered: check_entry(entry) +def test_select_list_filters(test_app): + """Testet das Auslesen von Datensätzen mit 'in' und 'not in'""" + with test_app.app_context(): + # IN + query = {'key': 'tags', 'compare': 'in', 'value': ['TestTag1', 'TestTag3']} + result_filtered = test_app.host.db_handler.select(test_app.config['IBAN'], + condition=query) + assert len(result_filtered) == 2, \ + f"Es wurde die falsche Zahl an Datensätzenzurückgegeben: {len(result_filtered)}" + for entry in result_filtered: + check_entry(entry) + assert entry.get('betrag') in [-221.98, -99.58], \ + f"Es wurde der falsche Eintrag zurückgegeben: {entry.get('betrag')}" + + # NOT IN + query = {'key': 'tags', 'compare': 'notin', 'value': ['TestTag1']} + result_filtered = test_app.host.db_handler.select(test_app.config['IBAN'], + condition=query) + assert len(result_filtered) == 4, \ + f"Es wurde die falsche Zahl an Datensätzenzurückgegeben: {len(result_filtered)}" + for entry in result_filtered: + check_entry(entry) + assert entry.get('betrag') != -221.98, \ + f"Es wurde der falsche Eintrag zurückgegeben: {entry.get('betrag')}" + + # all + query = {'key': 'tags', 'compare': 'all', 'value': ['TestTag1', 'TestTag2']} + result_filtered = test_app.host.db_handler.select(test_app.config['IBAN'], + condition=query) + assert len(result_filtered) == 1, \ + f"Es wurde die falsche Zahl an Datensätzenzurückgegeben: {len(result_filtered)}" + entry = result_filtered[0] + check_entry(entry) + assert entry.get('betrag') == -221.98, \ + f"Es wurde der falsche Eintrag zurückgegeben: {entry.get('betrag')}" def test_select_regex(test_app): """Testet das Auslesen von Datensätzen mit Textfiltern (regex)""" @@ -176,6 +211,15 @@ def test_select_multi(test_app): check_entry(entry) +def test_list_ibans(test_app): + """Testet das Auslesen aller IBANs""" + with test_app.app_context(): + ibans = test_app.host.db_handler.list_ibans() + assert isinstance(ibans, list), "Die IBANs wurden nicht als Liste zurückgegeben" + assert len(ibans) >= 2, "Es wurden nicht alle IBANs zurückgegeben" + assert test_app.config['IBAN'] in ibans, "Die Test-IBAN wurde nicht zurückgegeben" + assert 'DE89370400440532011111' in ibans, "Die zweite Test-IBAN wurde nicht zurückgegeben" + def test_update(test_app): """Testet das Aktualisieren von Datensätzen""" with test_app.app_context(): From 7ee6a426c665e64d2c5cf34237499fef3b8029df Mon Sep 17 00:00:00 2001 From: Pitastic Date: Sat, 4 Oct 2025 23:06:52 +0200 Subject: [PATCH 04/12] =?UTF-8?q?IBAN=20aus=20Config=20gestrichen;=20Metho?= =?UTF-8?q?den=20f=C3=BCr=20neue=20IBAN=20und=20Group;=20Tests=20erstellt?= =?UTF-8?q?=20(unstable)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config.py | 2 - app/ui.py | 35 +++++++++++++ handler/BaseDb.py | 71 +++++++++++++++++++++++--- handler/MongoDb.py | 49 ++++++++++-------- handler/TinyDb.py | 39 +++++++------- tests/config.py | 3 +- tests/conftest.py | 1 - tests/test_integ_app_protected.py | 19 ++++++- tests/test_integ_basics.py | 85 ++++++++++++++++++------------- tests/test_unit_handler_DB.py | 81 ++++++++++++++++++++--------- 10 files changed, 272 insertions(+), 113 deletions(-) diff --git a/app/config.py b/app/config.py index 10c35f0..e4aa375 100644 --- a/app/config.py +++ b/app/config.py @@ -15,5 +15,3 @@ # For tiny: Filename ('testdata.json') # For mongo: Collection name ('testdata') DATABASE_NAME = 'testdata.json' - -IBAN = 'DE89370400440532013000' diff --git a/app/ui.py b/app/ui.py index 96529fb..7289bb2 100644 --- a/app/ui.py +++ b/app/ui.py @@ -150,6 +150,41 @@ def sw(): # - API Endpoints - - - - - - - - - - - - - - - - - - - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @current_app.route('/api/add/', methods=['PUT']) + def addIban(iban): + """ + Fügt eine neue IBAN als Collection in der Datenbank hinzu. + Args (uri): + iban, str: IBAN + Returns: + json: Informationen zur neu angelegten IBAN + """ + r = self.db_handler.add_iban(iban) + if not r.get('added'): + return {'error': 'No IBAN added', 'reason': r.get('error')}, 400 + + return r, 201 + + @current_app.route('/api/addgroup/', methods=['PUT']) + def addGroup(groupname): + """ + Erstellt eine Gruppe mit zugeordneten IBANs. + Args (uri / json): + groupname, str: Name der Gruppe + ibans, list[str]: Liste mit IBANs, die der Gruppe zugeordnet werden sollen + Returns: + json: Informationen zur neu angelegten Gruppe + """ + #TODO: User muss Rechte an allen IBANs der neuen Gruppe haben (related #7) + data = request.json + ibans = data.get('ibans') + assert ibans is not None, 'No IBANs provided' + r = self.db_handler.add_iban_group(groupname, ibans) + if not r.get('added'): + return {'error': 'No Group added', 'reason': r.get('error')}, 400 + + return r, 201 + @current_app.route('/api//', methods=['GET']) def getTx(iban, t_id): """ diff --git a/handler/BaseDb.py b/handler/BaseDb.py index 4a996dc..a9c63b9 100644 --- a/handler/BaseDb.py +++ b/handler/BaseDb.py @@ -21,15 +21,75 @@ def create(self): """Erstellen des Datenbankspeichers""" raise NotImplementedError() + def add_iban(self, iban: str): + """ + Fügt eine neue IBAN-Collection in die Datenbank ein. + + Args: + iban (str): Die hinzuzufügende IBAN. + Returns: + dict: + - added, int: Zahl der neu eingefügten IDs + """ + try: + if self.check_collection_is_iban(iban) is False: + raise ValueError(f"IBAN '{iban}' ist ungültig !") + + if iban in self.list_ibans(): + raise ValueError(f"IBAN '{iban}' existiert bereits !") + + return self._add_iban(iban) + + except Exception as ex: # pylint: disable=broad-except + # Catch all errors also from the different implementations of _add_iban + logging.error(f'Fehler beim Anlegen der Collection für IBAN {iban}: {ex}') + return {'added': 0, 'error': str(ex)} + + def _add_iban(self, iban: str): + """ + Private Methode zum Anlegen einer neuen IBAN-Collection in der Datenbank. + Siehe 'add_iban' Methode. + + Returns: + dict: + - added, int: Zahl der neu eingefügten IDs + """ + raise NotImplementedError() + + def add_iban_group(self, groupname: str, ibans: list): + """ + Fügt eine neue Gruppe mit IBANs in die Datenbank ein oder + ändert eine bestehende Gruppe mit der Zuordnung, die übergeben wurde. + + Args: + groupname (str): Name der Gruppe. + ibans (list): Liste der IBANs, die zur Gruppe hinzugefügt werden sollen. + Returns: + list: Liste aller IBANs dieser Gruppe. + """ + for iban in ibans: + if self.check_collection_is_iban(iban) is False: + raise ValueError(f"IBAN '{iban}' ist ungültig !") + + new_group = { + 'metatype': 'config', + 'name': 'group', + 'uuid': groupname, + 'groupname': groupname, + 'ibans': ibans, + 'members': [] + } + return self.set_metadata(new_group, overwrite=True) + def select(self, collection:str, condition: dict|list[dict]=None, multi: str='AND'): """ Handler für das Vorbereiten der '_select' Methode, welche Datensätze aus der Datenbank selektiert, die die angegebene Bedingung erfüllen. Args: - collection (str, optional): Name der Collection oder Gruppe, aus der selektiert werden - soll. Es erfolgt automatisch eine Unterscheidung, ob es - sich um eine IBAN oder einen Gruppenname handelt. + collection (str): Name der Collection oder Gruppe, aus der selektiert werden + soll. Es erfolgt automatisch eine Unterscheidung, ob es + sich um eine IBAN oder einen Gruppenname handelt. condition (dict | list(dict)): Bedingung als Dictionary - 'key', str : Spalten- oder Schlüsselname, - 'value', any : Wert der bei 'key' verglichen werden soll @@ -177,8 +237,7 @@ def truncate(self, collection: str): """Löscht alle Datensätze aus einer Tabelle/Collection Args: - collection (str, optional): Name der Collection, in die Werte eingefügt werden sollen. - Default: IBAN aus der Config. + collection (str): Name der Collection, in die Werte eingefügt werden sollen. Returns: dict: - deleted, int: Anzahl der gelöschten Datensätze @@ -245,7 +304,7 @@ def get_group_ibans(self, group: str, check_before: bool=False): 'key': 'name', 'value': 'group' },{ - 'key': 'groupname', + 'key': 'uuid', 'value': group } ], multi='AND') diff --git a/handler/MongoDb.py b/handler/MongoDb.py index 0b8c18c..593a4a3 100644 --- a/handler/MongoDb.py +++ b/handler/MongoDb.py @@ -30,14 +30,6 @@ def create(self): Erstellt eine Collection je Konto und legt Indexes/Constraints fest. Außerdem wird die Collection für Metadaten erstellt, falls sie noch nicht existiert. """ - # Collection für Transaktionen (je Konto) - iban = current_app.config['IBAN'] - if iban not in self.connection.list_collection_names(): - self.connection.create_collection(iban) - self.connection[iban].create_index( - [("uuid", pymongo.TEXT)], unique=True - ) - # Collection für Metadaten if 'metadata' not in self.connection.list_collection_names(): self.connection.create_collection('metadata') @@ -45,6 +37,28 @@ def create(self): [("uuid", pymongo.TEXT)], unique=True ) + def _add_iban(self, iban): + """ + Fügt eine neue IBAN-Collection in die Datenbank ein. + + Args: + iban (str): Die hinzuzufügende IBAN. + Returns: + dict: + - added, int: Zahl der neu eingefügten IDs + """ + try: + self.connection.create_collection(iban) + self.connection[iban].create_index( + [("uuid", pymongo.TEXT)], unique=True + ) + + except pymongo.errors.PyMongoError as ex: + logging.error(f'Fehler beim Anlegen der Collection für IBAN {iban}: {ex}') + return {'added': 0, 'error': str(ex)} + + return {'added': 1} + def _select(self, collection: list, condition=None, multi='AND'): """ Selektiert Datensätze aus der Datenbank, die die angegebene Bedingung erfüllen. @@ -113,14 +127,13 @@ def _insert(self, data: dict|list[dict], collection: str): except pymongo.errors.BulkWriteError: return {'inserted': 0} - def update(self, data, collection=None, condition=None, multi='AND'): + def update(self, data, collection, condition=None, multi='AND'): """ Aktualisiert Datensätze in der Datenbank, die die angegebene Bedingung erfüllen. Args: data (dict): Aktualisierte Daten für die passenden Datensätze - collection (str, optional): Name der Collection, in die Werte eingefügt werden sollen. - Default: IBAN aus der Config. + collection (str): Name der Collection, in die Werte eingefügt werden sollen. condition (dict | list(dict)): Bedingung als Dictionary - 'key', str : Spalten- oder Schlüsselname, - 'value', any : Wert der bei 'key' verglichen werden soll @@ -134,8 +147,6 @@ def update(self, data, collection=None, condition=None, multi='AND'): dict: - updated, int: Anzahl der aktualisierten Datensätze """ - if collection is None: - collection = current_app.config['IBAN'] collection = self.connection[collection] # Form condition into a query @@ -163,13 +174,12 @@ def update(self, data, collection=None, condition=None, multi='AND'): update_result = collection.update_many(query, update_op) return {'updated': update_result.modified_count} - def delete(self, collection=None, condition=None, multi='AND'): + def delete(self, collection, condition=None, multi='AND'): """ Löscht Datensätze in der Datenbank, die die angegebene Bedingung erfüllen. Args: - collection (str, optional): Name der Collection, in die Werte eingefügt werden sollen. - Default: IBAN aus der Config. + collection (str): Name der Collection, in die Werte eingefügt werden sollen. condition (dict | list of dicts): Bedingung als Dictionary - 'key', str : Spalten- oder Schlüsselname, - 'value', any : Wert der bei 'key' verglichen werden soll @@ -183,8 +193,6 @@ def delete(self, collection=None, condition=None, multi='AND'): dict: - deleted, int: Anzahl der gelöschten Datensätze """ - if collection is None: - collection = current_app.config['IBAN'] collection = self.connection[collection] # Form condition into a query @@ -193,13 +201,12 @@ def delete(self, collection=None, condition=None, multi='AND'): delete_result = collection.delete_many(query) return {'deleted': delete_result.deleted_count} - def truncate(self, collection=None): + def truncate(self, collection): """ Löscht alle Datensätze aus einer Tabelle/Collection Args: - collection (str, optional): Name der Collection, in die Werte eingefügt werden sollen. - Default: None -> Default der delete Methode + collection (str): Name der Collection, in die Werte eingefügt werden sollen. Returns: dict: - deleted, int: Anzahl der gelöschten Datensätze diff --git a/handler/TinyDb.py b/handler/TinyDb.py index 3f22e36..df02751 100644 --- a/handler/TinyDb.py +++ b/handler/TinyDb.py @@ -40,12 +40,23 @@ def create(self): Erstellt einen Table je Konto und legt Indexes/Constraints fest. Außerdem wird der Table für Metadaten erstellt, falls er noch nicht existiert. """ - # Touch Table für Transaktionen (je Konto) - self.connection.table(current_app.config['IBAN']) - # Table für Metadaten self.connection.table('metadata') + def _add_iban(self, iban): + """ + Fügt eine neue IBAN-Collection in die Datenbank ein. + + Args: + iban (str): Die hinzuzufügende IBAN. + Returns: + dict: + - added, int: Zahl der neu eingefügten IDs + """ + # Touch Table für Transaktionen + self.connection.table(iban) + return {'added': 1} + def _select(self, collection: list, condition=None, multi='AND'): """ Selektiert Datensätze aus der Datenbank, die die angegebene Bedingung erfüllen. @@ -128,14 +139,13 @@ def _insert(self, data: dict|list[dict], collection: str): result = self.connection.table(collection).insert(data) return {'inserted': (1 if result else 0)} - def update(self, data, collection=None, condition=None, multi='AND'): + def update(self, data, collection, condition=None, multi='AND'): """ Aktualisiert Datensätze in der Datenbank, die die angegebene Bedingung erfüllen. Args: data (dict): Aktualisierte Daten für die passenden Datensätze - collection (str, optional): Name der Collection, in die Werte eingefügt werden sollen. - Default: IBAN aus der Config. + collection (str): Name der Collection, in die Werte eingefügt werden sollen. condition (dict | list(dict)): Bedingung als Dictionary - 'key', str : Spalten- oder Schlüsselname, - 'value', any : Wert der bei 'key' verglichen werden soll @@ -149,9 +159,6 @@ def update(self, data, collection=None, condition=None, multi='AND'): dict: - updated, int: Anzahl der aktualisierten Datensätze """ - if collection is None: - collection = current_app.config['IBAN'] - # Form condition into a query and run if condition is None: query = Query().noop() @@ -191,13 +198,12 @@ def update(self, data, collection=None, condition=None, multi='AND'): return { 'updated': len(update_result) } - def delete(self, collection=None, condition=None, multi='AND'): + def delete(self, collection, condition=None, multi='AND'): """ Löscht Datensätze in der Datenbank, die die angegebene Bedingung erfüllen. Args: - collection (str, optional): Name der Collection, in die Werte eingefügt werden sollen. - Default: IBAN aus der Config. + collection (str): Name der Collection, in die Werte eingefügt werden sollen. condition (dict | list(dict)): Bedingung als Dictionary - 'key', str : Spalten- oder Schlüsselname, - 'value', any : Wert der bei 'key' verglichen werden soll @@ -211,8 +217,6 @@ def delete(self, collection=None, condition=None, multi='AND'): dict: - deleted, int: Anzahl der gelöschten Datensätze """ - if collection is None: - collection = current_app.config['IBAN'] collection = self.connection.table(collection) # Form condition into a query @@ -224,18 +228,15 @@ def delete(self, collection=None, condition=None, multi='AND'): deleted_ids = collection.remove(query) return {'deleted': len(deleted_ids)} - def truncate(self, collection=None): + def truncate(self, collection): """Löscht alle Datensätze aus einer Tabelle/Collection Args: - collection (str, optional): Name der Collection, in die Werte eingefügt werden sollen. - Default: IBAN aus der Config. + collection (str): Name der Collection, in die Werte eingefügt werden sollen. Returns: dict: - deleted, int: Anzahl der gelöschten Datensätze """ - if collection is None: - collection = current_app.config['IBAN'] table = self.connection.table(collection) r = table.remove(lambda x: True) return {'deleted': len(r)} diff --git a/tests/config.py b/tests/config.py index e4c513d..43a980c 100644 --- a/tests/config.py +++ b/tests/config.py @@ -14,6 +14,5 @@ # For tiny: Filename ('testdata.json') # For mongo: Collection name ('testdata') +#DATABASE_NAME = 'testdata' DATABASE_NAME = 'testdata.json' - -IBAN = 'DE89370400440532013000' diff --git a/tests/conftest.py b/tests/conftest.py index d382d22..361bb1d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,7 +30,6 @@ def test_app(): # App Context app = create_app(config_path) with app.app_context(): - app.host.db_handler.truncate() yield app shutil.rmtree("/tmp/pynance-test", ignore_errors=True) diff --git a/tests/test_integ_app_protected.py b/tests/test_integ_app_protected.py index d51693c..1688eec 100644 --- a/tests/test_integ_app_protected.py +++ b/tests/test_integ_app_protected.py @@ -5,6 +5,21 @@ import pytest +def test_add_iban(test_app): + """ + Testet das Hinzufügen einer IBAN in der Instanz. + """ + with test_app.app_context(): + + with test_app.test_client() as client: + result = client.put("/api/add/DE89370400440532013000") + assert result.status_code == 201, 'Die IBAN wurde nicht hinzugefügt.' + + # No Doublettes + result = client.put("/api/add/DE89370400440532013000") + assert result.status_code == 400, 'Die IBAN wurde doppelt hinzugefügt.' + + def test_read_input_csv(test_app): """ Testet den Handler für das Einlesen übermittelter Daten im CSV Format. @@ -22,7 +37,7 @@ def test_read_input_csv(test_app): assert found_rows_len == 5, (f'Es wurden {found_rows_len} statt der ' 'erwarteten 5 Einträge aus der Datei eingelesen.') # Savev to DB for next Tests - r = test_app.host.db_handler.insert(found_rows, test_app.config['IBAN']) + r = test_app.host.db_handler.insert(found_rows, "DE89370400440532013000") assert r.get('inserted') == 5, \ "Es wurden nicht alle Einträge in die DB eingefügt." @@ -50,7 +65,7 @@ def test_set_manual_tag(test_app): Testet das Setzen eines Tags für einen Eintrag in der Instanz. """ with test_app.app_context(): - iban = test_app.config['IBAN'] + iban = "DE89370400440532013000" t_id = '6884802db5e07ee68a68e2c64f9c0cdd' # Setzen des Tags diff --git a/tests/test_integ_basics.py b/tests/test_integ_basics.py index 31e487f..bef0930 100644 --- a/tests/test_integ_basics.py +++ b/tests/test_integ_basics.py @@ -19,12 +19,27 @@ ) +def test_add_iban(test_app): + """ + Testet das Hinzufügen einer IBAN in der Instanz. + """ + with test_app.app_context(): + + with test_app.test_client() as client: + result = client.put("/api/add/DE89370400440532013000") + assert result.status_code == 201, 'Die IBAN wurde nicht hinzugefügt.' + + # No Doublettes + result = client.put("/api/add/DE89370400440532013000") + assert result.status_code == 400, 'Die IBAN wurde doppelt hinzugefügt.' + + def test_truncate(test_app): """Leert die Datenbank und dient als Hilfsfunktion für folgende Tests""" with test_app.app_context(): with test_app.test_client() as client: - result = client.delete(f"/api/truncateDatabase/{test_app.config['IBAN']}") + result = client.delete("/api/truncateDatabase/DE89370400440532013000") assert result.status_code == 200, "Fehler beim Leeren der Datenbank" @@ -40,7 +55,7 @@ def test_upload_csv_commerzbank(test_app): with test_app.test_client() as client: # Cleared DB ? - result = client.get(f"/{test_app.config['IBAN']}") + result = client.get("/DE89370400440532013000") assert "', 'value': 0} result_filtered = test_app.host.db_handler.select( - test_app.config['IBAN'], + "DE89370400440532013000", condition=query ) assert len(result_filtered) == 1, \ @@ -362,7 +377,7 @@ def test_tag_custom_rules(test_app): {'key':'text_tx', 'value': r'EDEKA', 'compare': 'regex'} ], } - result = client.put(f"/api/tag-and-cat/{test_app.config['IBAN']}", json=parameters) + result = client.put("/api/tag-and-cat/DE89370400440532013000", json=parameters) result = result.json # Es sollte eine Transaktion zutreffen, @@ -388,7 +403,7 @@ def test_categorize_custom_rules(test_app): 'category': "Overwriting Cat", 'tags': ['Stadt'] } - result = client.put(f"/api/tag-and-cat/{test_app.config['IBAN']}", json=parameters) + result = client.put("/api/tag-and-cat/DE89370400440532013000", json=parameters) result = result.json # Es sollte eine Transaktion zutreffen, @@ -407,7 +422,7 @@ def test_categorize_custom_rules(test_app): 'prio': 9, 'prio_set': 3, } - result = client.put(f"/api/tag-and-cat/{test_app.config['IBAN']}", json=parameters) + result = client.put("/api/tag-and-cat/DE89370400440532013000", json=parameters) result = result.json assert result.get('categorized') == 1, \ @@ -427,7 +442,7 @@ def test_tag_manual(test_app): 'tags': ['Test_TAG'] } r = client.put( - f"/api/setManualTag/{test_app.config['IBAN']}/6884802db5e07ee68a68e2c64f9c0cdd", + "/api/setManualTag/DE89370400440532013000/6884802db5e07ee68a68e2c64f9c0cdd", json=new_tag ) r = r.json @@ -435,7 +450,7 @@ def test_tag_manual(test_app): # Check if new values correct stored r = client.get( - f"/api/{test_app.config['IBAN']}/6884802db5e07ee68a68e2c64f9c0cdd" + "/api/DE89370400440532013000/6884802db5e07ee68a68e2c64f9c0cdd" ) r = r.json assert isinstance(r.get('tags'), list), "Tags wurde nicht als Liste gespeichert" @@ -447,7 +462,7 @@ def test_tag_manual(test_app): 'tags': ['Test_Another_SECONDARY'] } r = client.put( - f"/api/setManualTag/{test_app.config['IBAN']}/6884802db5e07ee68a68e2c64f9c0cdd", + "/api/setManualTag/DE89370400440532013000/6884802db5e07ee68a68e2c64f9c0cdd", json=new_tag ) r = r.json @@ -455,7 +470,7 @@ def test_tag_manual(test_app): # Check if new values correct stored r = client.get( - f'/api/{test_app.config['IBAN']}/6884802db5e07ee68a68e2c64f9c0cdd' + '/api/DE89370400440532013000/6884802db5e07ee68a68e2c64f9c0cdd' ) r = r.json assert isinstance(r.get('tags'), list), "Tags wurde nicht als Liste gespeichert" @@ -474,7 +489,7 @@ def test_categorize_manual(test_app): 'category': 'Test_CAT' } r = client.put( - f"/api/setManualCat/{test_app.config['IBAN']}/6884802db5e07ee68a68e2c64f9c0cdd", + "/api/setManualCat/DE89370400440532013000/6884802db5e07ee68a68e2c64f9c0cdd", json=new_cat ) r = r.json @@ -482,7 +497,7 @@ def test_categorize_manual(test_app): # Check if new values correct stored r = client.get( - f"/api/{test_app.config['IBAN']}/6884802db5e07ee68a68e2c64f9c0cdd" + "/api/DE89370400440532013000/6884802db5e07ee68a68e2c64f9c0cdd" ) r = r.json assert 'Test_CAT' == r.get('category'), \ @@ -502,7 +517,7 @@ def test_tag_manual_multi(test_app): "fdd4649484137572ac642e2c0f34f9af"] } r = client.put( - f"/api/setManualTags/{test_app.config['IBAN']}", + "/api/setManualTags/DE89370400440532013000", json=new_tag ) r = r.json @@ -510,11 +525,11 @@ def test_tag_manual_multi(test_app): # Check if new values correct stored r = client.get( - f"/api/{test_app.config['IBAN']}/6884802db5e07ee68a68e2c64f9c0cdd" + "/api/DE89370400440532013000/6884802db5e07ee68a68e2c64f9c0cdd" ) r1 = r.json r = client.get( - f"/api/{test_app.config['IBAN']}/fdd4649484137572ac642e2c0f34f9af" + "/api/DE89370400440532013000/fdd4649484137572ac642e2c0f34f9af" ) r2 = r.json assert "Test_SECONDARY_2" in r1.get('tags', []) and \ @@ -536,7 +551,7 @@ def test_categorize_manual_multi(test_app): "fdd4649484137572ac642e2c0f34f9af"] } r = client.put( - f"/api/setManualCats/{test_app.config['IBAN']}", + "/api/setManualCats/DE89370400440532013000", json=new_cat ) r = r.json @@ -544,7 +559,7 @@ def test_categorize_manual_multi(test_app): # Check if new values correct stored r = client.get( - f"/api/{test_app.config['IBAN']}/fdd4649484137572ac642e2c0f34f9af" + "/api/DE89370400440532013000/fdd4649484137572ac642e2c0f34f9af" ) r = r.json assert 'Multi-Category' == r.get('category'), \ @@ -561,7 +576,7 @@ def test_remove_category(test_app): with test_app.test_client() as client: # Remove Cat result = client.put( - f"/api/removeCat/{test_app.config['IBAN']}/fdd4649484137572ac642e2c0f34f9af" + "/api/removeCat/DE89370400440532013000/fdd4649484137572ac642e2c0f34f9af" ) result = result.json assert result.get('updated') == 1, \ @@ -569,7 +584,7 @@ def test_remove_category(test_app): # Check if new values correct stored result = client.get( - f"/api/{test_app.config['IBAN']}/fdd4649484137572ac642e2c0f34f9af" + "/api/DE89370400440532013000/fdd4649484137572ac642e2c0f34f9af" ) result = result.json assert result.get('category') is None, \ @@ -588,7 +603,7 @@ def test_remove_tag(test_app): with test_app.test_client() as client: # Remove Tag result = client.put( - f"/api/removeTag/{test_app.config['IBAN']}/6884802db5e07ee68a68e2c64f9c0cdd" + "/api/removeTag/DE89370400440532013000/6884802db5e07ee68a68e2c64f9c0cdd" ) result = result.json assert result.get('updated') == 1, \ @@ -596,7 +611,7 @@ def test_remove_tag(test_app): # Check if new values correct stored result = client.get( - f"/api/{test_app.config['IBAN']}/6884802db5e07ee68a68e2c64f9c0cdd" + "/api/DE89370400440532013000/6884802db5e07ee68a68e2c64f9c0cdd" ) result = result.json assert result.get('category') is not None, \ @@ -615,7 +630,7 @@ def test_remove_tag_multi(test_app): with test_app.test_client() as client: # Remove Tag result = client.put( - f"/api/removeTags/{test_app.config['IBAN']}", + "/api/removeTags/DE89370400440532013000", json={ 't_ids': ["786e1d4e16832aa321a0176c854fe087", "fdd4649484137572ac642e2c0f34f9af"] @@ -627,7 +642,7 @@ def test_remove_tag_multi(test_app): # Check if new values correct stored result = client.get( - f"/api/{test_app.config['IBAN']}/786e1d4e16832aa321a0176c854fe087" + "/api/DE89370400440532013000/786e1d4e16832aa321a0176c854fe087" ) result = result.json assert result.get('category') is not None, \ diff --git a/tests/test_unit_handler_DB.py b/tests/test_unit_handler_DB.py index 618fdef..930a90a 100644 --- a/tests/test_unit_handler_DB.py +++ b/tests/test_unit_handler_DB.py @@ -10,6 +10,37 @@ from helper import generate_fake_data, check_entry +def test_add_iban(test_app): + """ + Testet das Hinzufügen einer IBAN in der Instanz. + """ + with test_app.app_context(): + + result = test_app.add_iban("DE89370400440532013000") + assert result.get('added') == 1, 'Die IBAN wurde nicht hinzugefügt.' + + # No Doublettes + result = test_app.add_iban("DE89370400440532013000") + assert result.get('added') == 0, 'Die IBAN wurde nicht hinzugefügt.' + + # Add second IBAN + result = test_app.add_iban("DE89370400440532011111") + assert result.get('added') == 1, 'Die zweite IBAN wurde nicht hinzugefügt.' + +def test_add_group(test_app): + """ + Testet das Hinzufügen einer Gruppe in der Instanz. + """ + with test_app.app_context(): + + result = test_app.add_group("testgroup", ["DE89370400440532013000"]) + assert result == ["DE89370400440532013000"], 'Die Gruppe wurde nicht hinzugefügt.' + + # No Doublettes + result = test_app.add_group("testgroup", + ["DE89370400440532013000", "DE89370400440532011111"]) + assert result == ["DE89370400440532013000", "DE89370400440532011111"], \ + 'Die Gruppe wurde nicht geupdated.' def test_insert(test_app): """Testet das Einfügen von Datensätzen""" @@ -17,26 +48,26 @@ def test_insert(test_app): # Einzelner Datensatz data = generate_fake_data(1)[0] inserted_db = test_app.host.db_handler.insert(data, - collection=test_app.config['IBAN']) + collection="DE89370400440532013000") id_count = inserted_db.get('inserted') assert id_count == 1, \ f"Es wurde nicht die erwartete Anzahl an Datensätzen eingefügt: {id_count}" # Zwischendurch leeren - deleted_db = test_app.host.db_handler.truncate() + deleted_db = test_app.host.db_handler.truncate('DE89370400440532013000') delete_count = deleted_db.get('deleted') assert delete_count == 1, "Die Datenbank konnte während des Tests nicht geleert werden" # Liste von Datensätzen data = generate_fake_data(4) - inserted_db = test_app.host.db_handler.insert(data, collection=test_app.config['IBAN']) + inserted_db = test_app.host.db_handler.insert(data, collection="DE89370400440532013000") id_count = inserted_db.get('inserted') assert id_count == 4, \ f"Es wurde nicht die erwartete Anzahl an Datensätzen eingefügt: {id_count}" # Keine Duplikate data = generate_fake_data(5) - inserted_db = test_app.host.db_handler.insert(data, collection=test_app.config['IBAN']) + inserted_db = test_app.host.db_handler.insert(data, collection="DE89370400440532013000") id_count = inserted_db.get('inserted') assert id_count == 1, \ f"Es wurden doppelte Datensätze eingefügt: {id_count}" @@ -52,12 +83,12 @@ def test_select_all(test_app): """Testet das Auslesen von allen Datensätzen""" with test_app.app_context(): # Liste von Datensätzen einfügen - test_app.host.db_handler.truncate() + test_app.host.db_handler.truncate('DE89370400440532013000') data = generate_fake_data(5) - test_app.host.db_handler.insert(data, collection=test_app.config['IBAN']) + test_app.host.db_handler.insert(data, collection="DE89370400440532013000") # Alles selektieren - result_all = test_app.host.db_handler.select(test_app.config['IBAN']) + result_all = test_app.host.db_handler.select("DE89370400440532013000") assert len(result_all) == 5, \ f"Es wurde die falsche Zahl an Datensätzenzurückgegeben: {len(result_all)}" for entry in result_all: @@ -69,7 +100,7 @@ def test_select_filter(test_app): with test_app.app_context(): # Selektieren mit Filter (by Hash) query = {'key': 'uuid', 'value': '13d505688ab3b940dbed47117ffddf95'} - result_filtered = test_app.host.db_handler.select(test_app.config['IBAN'], + result_filtered = test_app.host.db_handler.select("DE89370400440532013000", condition=query) assert len(result_filtered) == 1, \ f"Es wurde die falsche Zahl an Datensätzenzurückgegeben: {len(result_filtered)}" @@ -78,7 +109,7 @@ def test_select_filter(test_app): # Selektieren mit Filter (by Art) query = {'key': 'art', 'value': 'Lastschrift'} - result_filtered = test_app.host.db_handler.select(test_app.config['IBAN'], condition=query) + result_filtered = test_app.host.db_handler.select("DE89370400440532013000", condition=query) assert len(result_filtered) == 5, \ f"Es wurde die falsche Zahl an Datensätzenzurückgegeben: {len(result_filtered)}" for entry in result_filtered: @@ -90,7 +121,7 @@ def test_select_like(test_app): with test_app.app_context(): # Selektieren mit Filter (by LIKE Text-Content) query = {'key': 'text_tx', 'compare': 'like', 'value': 'Garten'} - result_filtered = test_app.host.db_handler.select(test_app.config['IBAN'], + result_filtered = test_app.host.db_handler.select("DE89370400440532013000", condition=query) assert len(result_filtered) == 1, \ f"Es wurde die falsche Zahl an Datensätzenzurückgegeben: {len(result_filtered)}" @@ -102,7 +133,7 @@ def test_select_lt(test_app): """Testet das Auslesen von Datensätzen mit 'kleiner als'""" with test_app.app_context(): query = {'key': 'betrag', 'compare': '<', 'value': -100} - result_filtered = test_app.host.db_handler.select(test_app.config['IBAN'], + result_filtered = test_app.host.db_handler.select("DE89370400440532013000", condition=query) assert len(result_filtered) == 2, \ f"Es wurde die falsche Zahl an Datensätzenzurückgegeben: {len(result_filtered)}" @@ -114,7 +145,7 @@ def test_select_lt_eq(test_app): """Testet das Auslesen von Datensätzen mit 'kleiner als, gleich'""" with test_app.app_context(): query = {'key': 'betrag', 'compare': '<=', 'value': -71.35} - result_filtered = test_app.host.db_handler.select(test_app.config['IBAN'], condition=query) + result_filtered = test_app.host.db_handler.select("DE89370400440532013000", condition=query) assert len(result_filtered) == 4, \ f"Es wurde die falsche Zahl an Datensätzenzurückgegeben: {len(result_filtered)}" for entry in result_filtered: @@ -125,7 +156,7 @@ def test_select_not_eq(test_app): """Testet das Auslesen von Datensätzen mit 'ungleich'""" with test_app.app_context(): query = {'key': 'date_wert', 'compare': '!=', 'value': 1684108800} - result_filtered = test_app.host.db_handler.select(test_app.config['IBAN'], + result_filtered = test_app.host.db_handler.select("DE89370400440532013000", condition=query) assert len(result_filtered) == 1, \ f"Es wurde die falsche Zahl an Datensätzenzurückgegeben: {len(result_filtered)}" @@ -137,7 +168,7 @@ def test_select_list_filters(test_app): with test_app.app_context(): # IN query = {'key': 'tags', 'compare': 'in', 'value': ['TestTag1', 'TestTag3']} - result_filtered = test_app.host.db_handler.select(test_app.config['IBAN'], + result_filtered = test_app.host.db_handler.select("DE89370400440532013000", condition=query) assert len(result_filtered) == 2, \ f"Es wurde die falsche Zahl an Datensätzenzurückgegeben: {len(result_filtered)}" @@ -148,7 +179,7 @@ def test_select_list_filters(test_app): # NOT IN query = {'key': 'tags', 'compare': 'notin', 'value': ['TestTag1']} - result_filtered = test_app.host.db_handler.select(test_app.config['IBAN'], + result_filtered = test_app.host.db_handler.select("DE89370400440532013000", condition=query) assert len(result_filtered) == 4, \ f"Es wurde die falsche Zahl an Datensätzenzurückgegeben: {len(result_filtered)}" @@ -159,7 +190,7 @@ def test_select_list_filters(test_app): # all query = {'key': 'tags', 'compare': 'all', 'value': ['TestTag1', 'TestTag2']} - result_filtered = test_app.host.db_handler.select(test_app.config['IBAN'], + result_filtered = test_app.host.db_handler.select("DE89370400440532013000", condition=query) assert len(result_filtered) == 1, \ f"Es wurde die falsche Zahl an Datensätzenzurückgegeben: {len(result_filtered)}" @@ -172,7 +203,7 @@ def test_select_regex(test_app): """Testet das Auslesen von Datensätzen mit Textfiltern (regex)""" with test_app.app_context(): query = {'key': 'text_tx', 'compare': 'regex', 'value': r'KFN\s[0-9]\s[A-Z]{2}\s[0-9]{3,4}'} - result_filtered = test_app.host.db_handler.select(test_app.config['IBAN'], condition=query) + result_filtered = test_app.host.db_handler.select("DE89370400440532013000", condition=query) assert len(result_filtered) == 4, \ f"Es wurde die falsche Zahl an Datensätzenzurückgegeben: {len(result_filtered)}" for entry in result_filtered: @@ -188,7 +219,7 @@ def test_select_multi(test_app): {'key': 'betrag', 'compare': '>', 'value': -100}, {'key': 'betrag', 'compare': '<', 'value': -50}, ] - result_filtered = test_app.host.db_handler.select(test_app.config['IBAN'], + result_filtered = test_app.host.db_handler.select("DE89370400440532013000", condition=query, multi='AND') assert len(result_filtered) == 2, \ @@ -202,7 +233,7 @@ def test_select_multi(test_app): {'key': 'text_tx', 'compare': 'like', 'value': 'Frankfurt'}, {'key': 'text_tx', 'compare': 'like', 'value': 'FooBar not exists'}, ] - result_filtered = test_app.host.db_handler.select(test_app.config['IBAN'], + result_filtered = test_app.host.db_handler.select("DE89370400440532013000", condition=query, multi='OR') assert len(result_filtered) == 2, \ @@ -217,7 +248,7 @@ def test_list_ibans(test_app): ibans = test_app.host.db_handler.list_ibans() assert isinstance(ibans, list), "Die IBANs wurden nicht als Liste zurückgegeben" assert len(ibans) >= 2, "Es wurden nicht alle IBANs zurückgegeben" - assert test_app.config['IBAN'] in ibans, "Die Test-IBAN wurde nicht zurückgegeben" + assert "DE89370400440532013000" in ibans, "Die Test-IBAN wurde nicht zurückgegeben" assert 'DE89370400440532011111' in ibans, "Die zweite Test-IBAN wurde nicht zurückgegeben" def test_update(test_app): @@ -234,7 +265,7 @@ def test_update(test_app): assert update_two == 2, \ f'Es wurde nicht die richtige Anzahl geupdated (update_two): {update_two}' - result_one = test_app.host.db_handler.select(test_app.config['IBAN'], condition=query) + result_one = test_app.host.db_handler.select("DE89370400440532013000", condition=query) for entry in result_one: check_entry(entry, data) @@ -245,7 +276,7 @@ def test_update(test_app): assert update_all == 5, \ f'Es wurde nicht die richtige Anzahl geupdated (update_all): {update_all}' - result_all = test_app.host.db_handler.select(test_app.config['IBAN']) + result_all = test_app.host.db_handler.select("DE89370400440532013000") for entry in result_all: check_entry(entry, data) @@ -257,7 +288,7 @@ def test_update(test_app): assert update_nested == 1, \ f'Es wurde nicht die richtige Anzahl geupdated (update_nested): {update_nested}' - result_nested = test_app.host.db_handler.select(test_app.config['IBAN'], condition=query) + result_nested = test_app.host.db_handler.select("DE89370400440532013000", condition=query) data['uuid'] = 'ba9e5795e4029213ae67ac052d378d84' for entry in result_nested: check_entry(entry, data) @@ -267,7 +298,7 @@ def test_select_nested(test_app): """Testet das Auslesen von verschachtelten Datenätzen""" with test_app.app_context(): query = {'key': {'parsed': 'Mandatsreferenz'}, 'value': 'M1111111'} - result_filtered = test_app.host.db_handler.select(test_app.config['IBAN'], condition=query) + result_filtered = test_app.host.db_handler.select("DE89370400440532013000", condition=query) assert len(result_filtered) == 1, \ f"Es wurde die falsche Zahl an Datensätzenzurückgegeben: {len(result_filtered)}" for entry in result_filtered: @@ -301,7 +332,7 @@ def test_set_metadata(test_app): "name": "group", "groupname": "testgroup", "ibans": [ - test_app.config['IBAN'], + "DE89370400440532013000", 'DE89370400440532011111' ], "members": [ From 97334610b9123ef64b5bcda7ab9eb53a7cdb155a Mon Sep 17 00:00:00 2001 From: Pitastic Date: Sun, 5 Oct 2025 22:07:13 +0200 Subject: [PATCH 05/12] Finishing Pytests --- handler/BaseDb.py | 45 +------------------------------ handler/MongoDb.py | 29 +++++--------------- handler/Tags.py | 29 ++++++++++---------- handler/TinyDb.py | 14 ---------- tests/test_integ_app_protected.py | 15 ----------- tests/test_integ_basics.py | 15 ----------- tests/test_unit_handler_DB.py | 43 ++++++++++++----------------- 7 files changed, 39 insertions(+), 151 deletions(-) diff --git a/handler/BaseDb.py b/handler/BaseDb.py index a9c63b9..bc83d29 100644 --- a/handler/BaseDb.py +++ b/handler/BaseDb.py @@ -21,41 +21,6 @@ def create(self): """Erstellen des Datenbankspeichers""" raise NotImplementedError() - def add_iban(self, iban: str): - """ - Fügt eine neue IBAN-Collection in die Datenbank ein. - - Args: - iban (str): Die hinzuzufügende IBAN. - Returns: - dict: - - added, int: Zahl der neu eingefügten IDs - """ - try: - if self.check_collection_is_iban(iban) is False: - raise ValueError(f"IBAN '{iban}' ist ungültig !") - - if iban in self.list_ibans(): - raise ValueError(f"IBAN '{iban}' existiert bereits !") - - return self._add_iban(iban) - - except Exception as ex: # pylint: disable=broad-except - # Catch all errors also from the different implementations of _add_iban - logging.error(f'Fehler beim Anlegen der Collection für IBAN {iban}: {ex}') - return {'added': 0, 'error': str(ex)} - - def _add_iban(self, iban: str): - """ - Private Methode zum Anlegen einer neuen IBAN-Collection in der Datenbank. - Siehe 'add_iban' Methode. - - Returns: - dict: - - added, int: Zahl der neu eingefügten IDs - """ - raise NotImplementedError() - def add_iban_group(self, groupname: str, ibans: list): """ Fügt eine neue Gruppe mit IBANs in die Datenbank ein oder @@ -65,7 +30,7 @@ def add_iban_group(self, groupname: str, ibans: list): groupname (str): Name der Gruppe. ibans (list): Liste der IBANs, die zur Gruppe hinzugefügt werden sollen. Returns: - list: Liste aller IBANs dieser Gruppe. + dict: Informationen über den Speichervorgang. """ for iban in ibans: if self.check_collection_is_iban(iban) is False: @@ -217,7 +182,6 @@ def delete(self, collection: str, condition: dict | list[dict]): Args: collection (str, optional): Name der Collection, in die Werte eingefügt werden sollen. - Default: IBAN aus der Config. condition (dict | list(dict)): Bedingung als Dictionary - 'key', str : Spalten- oder Schlüsselname, - 'value', any : Wert der bei 'key' verglichen werden soll @@ -421,13 +385,6 @@ def _load_metadata(self): if not isinstance(parsed_data, list): parsed_data = [parsed_data] - #for i, _ in enumerate(parsed_data): - # parsed_data[i]['metatype'] = metatype - - #else: - # parsed_data['metatype'] = metatype - # parsed_data = [parsed_data] - # Store in DB (do not overwrite) inserted = 0 for data in parsed_data: diff --git a/handler/MongoDb.py b/handler/MongoDb.py index 593a4a3..ca27a0b 100644 --- a/handler/MongoDb.py +++ b/handler/MongoDb.py @@ -37,28 +37,6 @@ def create(self): [("uuid", pymongo.TEXT)], unique=True ) - def _add_iban(self, iban): - """ - Fügt eine neue IBAN-Collection in die Datenbank ein. - - Args: - iban (str): Die hinzuzufügende IBAN. - Returns: - dict: - - added, int: Zahl der neu eingefügten IDs - """ - try: - self.connection.create_collection(iban) - self.connection[iban].create_index( - [("uuid", pymongo.TEXT)], unique=True - ) - - except pymongo.errors.PyMongoError as ex: - logging.error(f'Fehler beim Anlegen der Collection für IBAN {iban}: {ex}') - return {'added': 0, 'error': str(ex)} - - return {'added': 1} - def _select(self, collection: list, condition=None, multi='AND'): """ Selektiert Datensätze aus der Datenbank, die die angegebene Bedingung erfüllen. @@ -108,6 +86,13 @@ def _insert(self, data: dict|list[dict], collection: str): dict: - inserted, int: Zahl der neu eingefügten IDs """ + # Da eine collection mit dem ersten Insert erstellt wird, + # muss ggf. direkt der Index zunächst gesetzt werden. + if collection not in self._get_collections(): + self.connection[collection].create_index( + [("uuid", pymongo.TEXT)], unique=True + ) + if isinstance(data, list): # Insert Many (INSERT IGNORE) try: diff --git a/handler/Tags.py b/handler/Tags.py index f81a428..e4c1ce1 100644 --- a/handler/Tags.py +++ b/handler/Tags.py @@ -99,11 +99,11 @@ def categorize(self, iban: str, rule_name: str = None, # Allgemeiner Startfilter und spezielle Conditions einer Rule if prio is not None: # prio values override - query_args = self._form_tag_query(prio, iban) + query_args = self._form_tag_query(iban, prio) else: # use rule prio or default - query_args = self._form_tag_query(rule.get('prio', 1)) + query_args = self._form_tag_query(iban, rule.get('prio', 1)) # -- Add all Filters for f in rule.get('filter', []): @@ -153,7 +153,7 @@ def categorize(self, iban: str, rule_name: str = None, continue query = {'key': 'uuid', 'value': uuid} - updated = self.db_handler.update(data=new_categories, condition=query) + updated = self.db_handler.update(new_categories, iban, query) # soft Exception Handling if not updated: @@ -165,7 +165,7 @@ def categorize(self, iban: str, rule_name: str = None, return result - def tag(self, iban: str=None, rule_name: str=None, dry_run: bool=False) -> dict: + def tag(self, iban: str, rule_name: str=None, dry_run: bool=False) -> dict: """ Tagged Transaktionen anhand von Regeln in der Datenbank. @@ -195,7 +195,7 @@ def tag(self, iban: str=None, rule_name: str=None, dry_run: bool=False) -> dict: raise ValueError('Es existieren noch keine Regeln für den Benutzer') # Allgemeine Startfilter für die Condition (ignore Prio bei Tagging) - query_args = self._form_tag_query(99, iban) + query_args = self._form_tag_query(iban, 99) for r_name, rule in tagging_rules.items(): logging.info(f"RegEx Tagging mit Rule {r_name}...") @@ -264,7 +264,7 @@ def tag(self, iban: str=None, rule_name: str=None, dry_run: bool=False) -> dict: continue query = {'key': 'uuid', 'value': uuid} - updated = self.db_handler.update(data={'tags': tags_to_set}, condition=query) + updated = self.db_handler.update({'tags': tags_to_set}, iban, query) # soft Exception Handling if not updated: @@ -276,13 +276,12 @@ def tag(self, iban: str=None, rule_name: str=None, dry_run: bool=False) -> dict: return result - def tag_ai(self, iban: str=None, dry_run: bool=False) -> dict: + def tag_ai(self, iban: str, dry_run: bool=False) -> dict: """ Automatisches Tagging mit AI. Args: iban: Name der Collection, in die Werte eingefügt werden sollen. - Default: IBAN aus der Config. dry_run Switch to show, which TX would be updated. Do not update. Returns: dict: @@ -292,7 +291,7 @@ def tag_ai(self, iban: str=None, dry_run: bool=False) -> dict: logging.info("Tagging with AI....") # Allgemeine Startfilter für die Condition - query_args = self._form_tag_query(collection=iban, ai=True) + query_args = self._form_tag_query(iban, ai=True) matched = self.db_handler.select(**query_args) tagged = 0 @@ -317,7 +316,7 @@ def tag_ai(self, iban: str=None, dry_run: bool=False) -> dict: new_category = { 'guess': entry.get('guess') } - updated = self.db_handler.update(data=new_category, condition=query) + updated = self.db_handler.update(new_category, iban, query) updated = updated.get('updated') # soft Exception Handling @@ -419,7 +418,7 @@ def tag_or_cat_custom(self, iban: str, category: str = None, if category is not None: # Set Category: Tags are filter arguments; Prio matters update_data['prio'] = prio_set - query_args = self._form_tag_query(prio, iban) + query_args = self._form_tag_query(iban, prio) if tags: query_args['condition'].append({ @@ -433,7 +432,7 @@ def tag_or_cat_custom(self, iban: str, category: str = None, else: # Set Tags: Prio does not matter (tags to set will be uniqued later) - query_args = self._form_tag_query(99, iban) + query_args = self._form_tag_query(iban, 99) # Add all Filters filters = [] if filters is None else filters @@ -484,7 +483,7 @@ def tag_or_cat_custom(self, iban: str, category: str = None, existing_tags = row.get('tags', []) update_data['tags'] = [t for t in tags if t not in existing_tags] - updated = self.db_handler.update(data=update_data, condition=query) + updated = self.db_handler.update(update_data, iban, query) # soft Exception Handling if not updated: @@ -502,13 +501,13 @@ def tag_or_cat_custom(self, iban: str, category: str = None, return result - def _form_tag_query(self, prio: int=1, collection: str=None, ai=False) -> dict: + def _form_tag_query(self, collection: str, prio: int=1, ai=False) -> dict: """ Erstellt die Standardabfrage-Filter für den Ausgangsdatensatz eines Taggings. Args: - prio, int: Filter more important tags collection, str: Collection to select from + prio, int: Filter more important tags ai, bool: True if AI Tagging Return: dict: Query Dict for db_handler.select() diff --git a/handler/TinyDb.py b/handler/TinyDb.py index df02751..5081a18 100644 --- a/handler/TinyDb.py +++ b/handler/TinyDb.py @@ -43,20 +43,6 @@ def create(self): # Table für Metadaten self.connection.table('metadata') - def _add_iban(self, iban): - """ - Fügt eine neue IBAN-Collection in die Datenbank ein. - - Args: - iban (str): Die hinzuzufügende IBAN. - Returns: - dict: - - added, int: Zahl der neu eingefügten IDs - """ - # Touch Table für Transaktionen - self.connection.table(iban) - return {'added': 1} - def _select(self, collection: list, condition=None, multi='AND'): """ Selektiert Datensätze aus der Datenbank, die die angegebene Bedingung erfüllen. diff --git a/tests/test_integ_app_protected.py b/tests/test_integ_app_protected.py index 1688eec..b876df2 100644 --- a/tests/test_integ_app_protected.py +++ b/tests/test_integ_app_protected.py @@ -5,21 +5,6 @@ import pytest -def test_add_iban(test_app): - """ - Testet das Hinzufügen einer IBAN in der Instanz. - """ - with test_app.app_context(): - - with test_app.test_client() as client: - result = client.put("/api/add/DE89370400440532013000") - assert result.status_code == 201, 'Die IBAN wurde nicht hinzugefügt.' - - # No Doublettes - result = client.put("/api/add/DE89370400440532013000") - assert result.status_code == 400, 'Die IBAN wurde doppelt hinzugefügt.' - - def test_read_input_csv(test_app): """ Testet den Handler für das Einlesen übermittelter Daten im CSV Format. diff --git a/tests/test_integ_basics.py b/tests/test_integ_basics.py index bef0930..9887612 100644 --- a/tests/test_integ_basics.py +++ b/tests/test_integ_basics.py @@ -19,21 +19,6 @@ ) -def test_add_iban(test_app): - """ - Testet das Hinzufügen einer IBAN in der Instanz. - """ - with test_app.app_context(): - - with test_app.test_client() as client: - result = client.put("/api/add/DE89370400440532013000") - assert result.status_code == 201, 'Die IBAN wurde nicht hinzugefügt.' - - # No Doublettes - result = client.put("/api/add/DE89370400440532013000") - assert result.status_code == 400, 'Die IBAN wurde doppelt hinzugefügt.' - - def test_truncate(test_app): """Leert die Datenbank und dient als Hilfsfunktion für folgende Tests""" with test_app.app_context(): diff --git a/tests/test_unit_handler_DB.py b/tests/test_unit_handler_DB.py index 930a90a..7da1cf3 100644 --- a/tests/test_unit_handler_DB.py +++ b/tests/test_unit_handler_DB.py @@ -10,38 +10,28 @@ from helper import generate_fake_data, check_entry -def test_add_iban(test_app): - """ - Testet das Hinzufügen einer IBAN in der Instanz. - """ - with test_app.app_context(): - - result = test_app.add_iban("DE89370400440532013000") - assert result.get('added') == 1, 'Die IBAN wurde nicht hinzugefügt.' - # No Doublettes - result = test_app.add_iban("DE89370400440532013000") - assert result.get('added') == 0, 'Die IBAN wurde nicht hinzugefügt.' - - # Add second IBAN - result = test_app.add_iban("DE89370400440532011111") - assert result.get('added') == 1, 'Die zweite IBAN wurde nicht hinzugefügt.' - -def test_add_group(test_app): +def test_add_and_get_group(test_app): """ Testet das Hinzufügen einer Gruppe in der Instanz. """ with test_app.app_context(): - result = test_app.add_group("testgroup", ["DE89370400440532013000"]) - assert result == ["DE89370400440532013000"], 'Die Gruppe wurde nicht hinzugefügt.' + result = test_app.host.db_handler.add_iban_group("testgroup", ["DE89370400440532013000"]) + assert result == {'inserted': 1}, 'Die Gruppe wurde nicht hinzugefügt.' # No Doublettes - result = test_app.add_group("testgroup", + result = test_app.host.db_handler.add_iban_group("testgroup", ["DE89370400440532013000", "DE89370400440532011111"]) - assert result == ["DE89370400440532013000", "DE89370400440532011111"], \ + assert result == {'inserted': 1}, \ 'Die Gruppe wurde nicht geupdated.' + result = test_app.host.db_handler.get_group_ibans("testgroup") + assert isinstance(result, list), 'Die IBANs wurden nicht als Liste zurückgegeben.' + assert len(result) == 2, 'Die Gruppe enthält nicht die erwartete Anzahl an IBANs.' + assert "DE89370400440532013000" in result, 'Die erste IBAN wurde nicht zurückgegeben.' + assert 'DE89370400440532011111' in result, 'Die zweite IBAN wurde nicht zurückgegeben.' + def test_insert(test_app): """Testet das Einfügen von Datensätzen""" with test_app.app_context(): @@ -260,7 +250,8 @@ def test_update(test_app): {'key': 'uuid', 'value': '13d505688ab3b940dbed47117ffddf95'}, {'key': 'text_tx', 'value': 'Wucherpfennig', 'compare': 'like'} ] - updated_db = test_app.host.db_handler.update(data, condition=query, multi='OR') + updated_db = test_app.host.db_handler.update(data, 'DE89370400440532013000', + query, multi='OR') update_two = updated_db.get('updated') assert update_two == 2, \ f'Es wurde nicht die richtige Anzahl geupdated (update_two): {update_two}' @@ -271,7 +262,7 @@ def test_update(test_app): # Update all with one field data = {'art': 'Überweisung'} - updated_db = test_app.host.db_handler.update(data) + updated_db = test_app.host.db_handler.update(data, 'DE89370400440532013000') update_all = updated_db.get('updated') assert update_all == 5, \ f'Es wurde nicht die richtige Anzahl geupdated (update_all): {update_all}' @@ -283,7 +274,7 @@ def test_update(test_app): # Update one set nested field data = {'parsed': {'Mandatsreferenz': 'M1111111'}} query = {'key': 'uuid', 'value': 'ba9e5795e4029213ae67ac052d378d84'} - updated_db = test_app.host.db_handler.update(data, condition=query) + updated_db = test_app.host.db_handler.update(data, 'DE89370400440532013000', query) update_nested = updated_db.get('updated') assert update_nested == 1, \ f'Es wurde nicht die richtige Anzahl geupdated (update_nested): {update_nested}' @@ -394,7 +385,7 @@ def test_delete(test_app): with test_app.app_context(): # Einzelnen Datensatz löschen query = {'key': 'uuid', 'value': '13d505688ab3b940dbed47117ffddf95'} - deleted_db = test_app.host.db_handler.delete(condition=query) + deleted_db = test_app.host.db_handler.delete('DE89370400440532013000', query) delete_one = deleted_db.get('deleted') assert delete_one == 1, \ f'Es wurde nicht die richtige Anzahl an Datensätzen gelöscht: {delete_one}' @@ -404,7 +395,7 @@ def test_delete(test_app): {'key': 'currency', 'value': 'EUR'}, {'key': 'currency', 'value': 'USD'} ] - deleted_db = test_app.host.db_handler.delete(condition=query, multi='OR') + deleted_db = test_app.host.db_handler.delete('DE89370400440532013000', query, multi='OR') delete_many = deleted_db.get('deleted') assert delete_many == 4, \ f'Es wurde nicht die richtige Anzahl an Datensätzen gelöscht: {delete_many}' From 09315cad008c1347190406d8ca6e2a804041f688 Mon Sep 17 00:00:00 2001 From: Pitastic Date: Sun, 5 Oct 2025 22:23:27 +0200 Subject: [PATCH 06/12] Testdetail for Mongo Backend (truncate counts more in Mongo than in Tiny because of its truncate approach) --- tests/test_unit_handler_DB.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_unit_handler_DB.py b/tests/test_unit_handler_DB.py index 7da1cf3..6664e44 100644 --- a/tests/test_unit_handler_DB.py +++ b/tests/test_unit_handler_DB.py @@ -46,7 +46,7 @@ def test_insert(test_app): # Zwischendurch leeren deleted_db = test_app.host.db_handler.truncate('DE89370400440532013000') delete_count = deleted_db.get('deleted') - assert delete_count == 1, "Die Datenbank konnte während des Tests nicht geleert werden" + assert delete_count > 0, "Die Datenbank konnte während des Tests nicht geleert werden" # Liste von Datensätzen data = generate_fake_data(4) From 5b54bce3efd643a425caca5aa530a163c7f35cbf Mon Sep 17 00:00:00 2001 From: Pitastic Date: Fri, 10 Oct 2025 23:01:07 +0200 Subject: [PATCH 07/12] iban upload and group creation frontend --- app/server.py | 2 +- app/static/js/iban.js | 283 +++++++++++++++++++++++++++++++++ app/static/js/index.js | 314 +++++++------------------------------ app/static/js/welcome.js | 31 ---- app/templates/iban.html | 89 +++++++++++ app/templates/index.html | 154 +++++++++--------- app/templates/welcome.html | 50 ------ app/ui.py | 32 ++-- handler/BaseDb.py | 22 +++ 9 files changed, 542 insertions(+), 435 deletions(-) create mode 100644 app/static/js/iban.js delete mode 100644 app/static/js/welcome.js create mode 100644 app/templates/iban.html delete mode 100644 app/templates/welcome.html diff --git a/app/server.py b/app/server.py index ca5ff08..3681e24 100644 --- a/app/server.py +++ b/app/server.py @@ -56,4 +56,4 @@ def create_app(config_path: str) -> Flask: 'config.py' ) application = create_app(config) - application.run(host='0.0.0.0', port=8110) + application.run(host='0.0.0.0', port=8110, debug=True) diff --git a/app/static/js/iban.js b/app/static/js/iban.js new file mode 100644 index 0000000..9c730fa --- /dev/null +++ b/app/static/js/iban.js @@ -0,0 +1,283 @@ +"use strict"; + +let rowCheckboxes = null; + +document.addEventListener('DOMContentLoaded', function () { + + // PopUps + document.getElementById('settings-button').addEventListener('click', function () { + openPopup('settings-popup'); + }); + + // Additional JavaScript for enabling/disabling the edit button based on checkbox selection + const selectAllCheckbox = document.getElementById('select-all'); + rowCheckboxes = document.querySelectorAll('.row-checkbox'); + + selectAllCheckbox.addEventListener('change', function () { + rowCheckboxes.forEach(checkbox => { + checkbox.checked = selectAllCheckbox.checked; + }); + updateEditButtonState(); + }); + + rowCheckboxes.forEach(checkbox => { + checkbox.addEventListener('change', function () { + if (!this.checked) { + selectAllCheckbox.checked = false; + } else if (Array.from(rowCheckboxes).every(cb => cb.checked)) { + selectAllCheckbox.checked = true; + } + updateEditButtonState(); + }); + }); + +}); + +// ---------------------------------------------------------------------------- +// -- DOM Functions ----------------------------------------------------------- +// ---------------------------------------------------------------------------- + +/** + * Opens a popup to display details for a specific element and optionally fetches transaction details. + * + * @param {string} id - The ID of the HTML element to display as a popup. + * @param {string|null} [tx_hash=null] - Optional transaction hash to fetch additional details for. + */ +function openDetailsPopup(id, tx_hash = null) { + if (tx_hash) { + // Use AJAX to fetch and populate details for the transaction with the given ID + console.log(`Fetching details for transaction hash: ${tx_hash}`); + const currentURI = window.location.pathname; + const iban = currentURI.split('/').pop(); + resetDetails(); + getInfo(iban, tx_hash, fillTxDetails); + openPopup(id); + + } else { + openPopup(id); + + } +} + + +/** + * Clears information from a result Box +* +*/ +function resetDetails() { + const box = document.getElementById('result-text'); + box.innerHTML = ""; +} + + +/** + * Shows a given Result in the Result-Box. + * + * @param {string} result - The text to be shwon. + */ +function fillTxDetails(result){ + const box = document.getElementById('result-text'); + box.innerHTML = result; +} + +/** + * Updates the state of the "Edit Selected" button based on the checkbox selections. + * + * This function checks if any row checkboxes are selected and enables or disables + * the "Edit Selected" button accordingly. It also updates the button's title to + * reflect the number of selected checkboxes. + * + * Assumes that `rowCheckboxes` is a collection of checkbox elements and that + * there is a button with the ID `edit-selected` in the DOM. + */ +function updateEditButtonState() { + const anyChecked = Array.from(rowCheckboxes).some(cb => cb.checked); + const editButton = document.getElementById('edit-selected'); + editButton.disabled = !anyChecked; + editButton.title = anyChecked + ? `Edit selected (${Array.from(rowCheckboxes).filter(cb => cb.checked).length} selected)` + : 'Edit selected (0 selected)'; +} + + +/** + * Truncates the database. + * An optional IBAN to truncate is selected by input with ID 'iban'. + */ +function truncateDB() { + const iban = document.getElementById('input_iban').value; + + apiGet('truncateDatabase/'+iban, {}, function (responseText, error) { + if (error) { + printResult('Truncate failed: ' + '(' + error + ')' + responseText); + + } else { + alert('Database truncated successfully!' + responseText); + window.location.reload(); + + } + }, 'DELETE'); + +} + + +/** + * Tags the entries in the database. + * Optional Tagging commands are read from the input with ID + * 'input_tagging_name' (more in the Future) + */ +function tagEntries() { + const iban = document.getElementById('input_iban').value; + const rule_name = document.getElementById('tagging_name').value; + let rules = {} + if (rule_name) { + rules['rule_name'] = rule_name + } + + apiSubmit('tag/'+iban, rules, function (responseText, error) { + if (error) { + printResult('Tagging failed: ' + '(' + error + ')' + responseText); + + } else { + alert('Entries tagged successfully!' + responseText); + window.location.reload(); + + } + }, false); +} + + +function removeTags() { + const iban = document.getElementById('input_iban').value; + const checkboxes = document.querySelectorAll('input[name="entry-select[]"]'); + const t_ids = []; + checkboxes.forEach((checkbox) => { + if (checkbox.checked) { + t_ids.push(checkbox.value); + } + }); + + if (!iban) { + alert('Please provide an IBAN.'); + return; + } + if (!t_ids) { + alert('Please provide a Transaction ID (checkbox).'); + return; + } + + let api_function; + let tags = {}; + if (t_ids.length == 1) { + api_function = 'removeTag/'+iban+'/'+t_ids[0]; + } else { + api_function = 'removeTags/'+iban; + tags['t_ids'] = t_ids; + }; + + apiSubmit(api_function, tags, function (responseText, error) { + if (error) { + printResult('Tagging failed: ' + '(' + error + ')' + responseText); + + } else { + alert('Entries tagged successfully!' + responseText); + window.location.reload(); + + } + }, false); +} + + +/** + * Tags the entries in the database in a direct manner (assign Categories, no rules) + * Optional Tagging commands are read from the inputs with IDs + * 'input_manual_category' , 'input_manual_tags' , 'input_iban' and 'input_tid'. + * While the IBAN and Transaction_ID are mandatory, the other inputs are optional. + */ +function manualTagEntries() { + const category = document.getElementById('input_manual_category').value; + let tags = document.getElementById('input_manual_tags').value; + const iban = document.getElementById('input_iban').value; + + const checkboxes = document.querySelectorAll('input[name="entry-select[]"]'); + const t_ids = []; + checkboxes.forEach((checkbox) => { + if (checkbox.checked) { + t_ids.push(checkbox.value); + } + }); + + if (!iban) { + alert('Please provide an IBAN.'); + return; + } + if (!t_ids) { + alert('Please provide a Transaction ID (checkbox).'); + return; + } + + let tagging = { + 'category': category, + 'tags': tags + } + + let api_function; + if (t_ids.length == 1) { + api_function = 'setManualTag/'+iban+'/'+t_ids[0]; + } else { + api_function = 'setManualTags/'+iban; + tagging['t_ids'] = t_ids; + }; + + apiSubmit(api_function, tagging, function (responseText, error) { + if (error) { + printResult('Tagging failed: ' + '(' + error + ')' + responseText); + + } else { + alert('Entries tagged successfully!' + responseText); + window.location.reload(); + + } + }, false); +} + + +/** + * Fetches information based on the provided IBAN and UUID, and processes the response. + * + * @param {string} iban - The International Bank Account Number (IBAN) to identify the account. + * @param {string} uuid - The unique identifier associated with the request. + * @param {Function} [callback=alert] - A callback function to handle the response text. Defaults to `alert`. + */ +function getInfo(iban, uuid, callback = alert) { + apiGet('/'+iban+'/'+uuid, {}, function (responseText, error) { + if (error) { + printResult('getTx failed: ' + '(' + error + ')' + responseText); + + } else { + callback(responseText); + + } + }); +} + + +function saveMeta() { + const meta_type = document.getElementById('select_meta').value; + const fileInput = document.getElementById('input-json'); + if (fileInput.files.length === 0) { + alert('Please select a file to upload.'); + return; + } + + const params = { file: 'input_file' }; // The key 'file' corresponds to the input element's ID + apiSubmit('upload/metadata/'+meta_type, params, function (responseText, error) { + if (error) { + printResult('Rule saving failed: ' + '(' + error + ')' + responseText); + + } else { + alert('Rule saved successfully!' + responseText); + + } + }, true); +} diff --git a/app/static/js/index.js b/app/static/js/index.js index e936b10..60a0c01 100644 --- a/app/static/js/index.js +++ b/app/static/js/index.js @@ -1,34 +1,49 @@ "use strict"; -let rowCheckboxes = null; - document.addEventListener('DOMContentLoaded', function () { // PopUps - document.getElementById('settings-button').addEventListener('click', function () { - openPopup('settings-popup'); + document.getElementById('add-iban-btn').addEventListener('click', function () { + openPopup('add-iban'); + }); + document.getElementById('add-group-btn').addEventListener('click', function () { + openPopup('add-group'); + }); + + // Import Input + const fileInput = document.getElementById('file-input'); + const fileLabel = document.getElementById('file-label'); + const fileDropArea = document.getElementById('file-drop-area'); + + fileDropArea.addEventListener('click', () => { + fileInput.click(); + }); + + fileInput.addEventListener('change', () => { + if (fileInput.files.length > 0) { + fileLabel.textContent = fileInput.files[0].name; + } else { + fileLabel.textContent = 'Datei hier ablegen oder auswählen (PDF / CSV / HTML)'; + } }); - // Additional JavaScript for enabling/disabling the edit button based on checkbox selection - const selectAllCheckbox = document.getElementById('select-all'); - rowCheckboxes = document.querySelectorAll('.row-checkbox'); + fileDropArea.addEventListener('dragover', (e) => { + e.preventDefault(); + fileDropArea.style.borderColor = '#000'; + }); - selectAllCheckbox.addEventListener('change', function () { - rowCheckboxes.forEach(checkbox => { - checkbox.checked = selectAllCheckbox.checked; - }); - updateEditButtonState(); + fileDropArea.addEventListener('dragleave', () => { + fileDropArea.style.borderColor = '#ccc'; }); - rowCheckboxes.forEach(checkbox => { - checkbox.addEventListener('change', function () { - if (!this.checked) { - selectAllCheckbox.checked = false; - } else if (Array.from(rowCheckboxes).every(cb => cb.checked)) { - selectAllCheckbox.checked = true; + fileDropArea.addEventListener('drop', (e) => { + e.preventDefault(); + fileDropArea.style.borderColor = '#ccc'; + const files = e.dataTransfer.files; + if (files.length > 0) { + fileInput.files = files; + fileLabel.textContent = files[0].name; } - updateEditButtonState(); - }); }); }); @@ -37,273 +52,60 @@ document.addEventListener('DOMContentLoaded', function () { // -- DOM Functions ----------------------------------------------------------- // ---------------------------------------------------------------------------- -/** - * Opens a popup to display details for a specific element and optionally fetches transaction details. - * - * @param {string} id - The ID of the HTML element to display as a popup. - * @param {string|null} [tx_hash=null] - Optional transaction hash to fetch additional details for. - */ -function openDetailsPopup(id, tx_hash = null) { - if (tx_hash) { - // Use AJAX to fetch and populate details for the transaction with the given ID - console.log(`Fetching details for transaction hash: ${tx_hash}`); - const currentURI = window.location.pathname; - const iban = currentURI.split('/').pop(); - resetDetails(); - getInfo(iban, tx_hash, fillTxDetails); - openPopup(id); - - } else { - openPopup(id); - - } -} - - -/** - * Clears information from a result Box -* -*/ -function resetDetails() { - const box = document.getElementById('result-text'); - box.innerHTML = ""; -} - - -/** - * Shows a given Result in the Result-Box. - * - * @param {string} result - The text to be shwon. - */ -function fillTxDetails(result){ - const box = document.getElementById('result-text'); - box.innerHTML = result; -} -/** - * Updates the state of the "Edit Selected" button based on the checkbox selections. - * - * This function checks if any row checkboxes are selected and enables or disables - * the "Edit Selected" button accordingly. It also updates the button's title to - * reflect the number of selected checkboxes. - * - * Assumes that `rowCheckboxes` is a collection of checkbox elements and that - * there is a button with the ID `edit-selected` in the DOM. - */ -function updateEditButtonState() { - const anyChecked = Array.from(rowCheckboxes).some(cb => cb.checked); - const editButton = document.getElementById('edit-selected'); - editButton.disabled = !anyChecked; - editButton.title = anyChecked - ? `Edit selected (${Array.from(rowCheckboxes).filter(cb => cb.checked).length} selected)` - : 'Edit selected (0 selected)'; -} +// ---------------------------------------------------------------------------- +// -- API Functions ----------------------------------------------------------- +// ---------------------------------------------------------------------------- /** * Sends a file to the server for upload. - * The file is selected via the file input element 'input_file'. + * The file is selected via the file input element 'file-input'. */ function uploadFile() { - const iban = document.getElementById('input_iban').value; - const fileInput = document.getElementById('input_file'); + const iban = document.getElementById('iban-input').value; + const fileInput = document.getElementById('file-input'); if (fileInput.files.length === 0) { alert('Please select a file to upload.'); return; } - const params = { file: 'input_file' }; // The key 'file' corresponds to the input element's ID + const params = { file: 'file-input' }; // The value of 'file' corresponds to the input element's ID apiSubmit('upload/' + iban, params, function (responseText, error) { if (error) { - printResult('File upload failed: ' + '(' + error + ')' + responseText); + alert('File upload failed: ' + '(' + error + ')' + responseText); } else { alert('File uploaded successfully!' + responseText); - window.location.reload(); + window.location.href = '/' + iban; } }, true); } - -/** - * Truncates the database. - * An optional IBAN to truncate is selected by input with ID 'iban'. - */ -function truncateDB() { - const iban = document.getElementById('input_iban').value; - - apiGet('truncateDatabase/'+iban, {}, function (responseText, error) { - if (error) { - printResult('Truncate failed: ' + '(' + error + ')' + responseText); - - } else { - alert('Database truncated successfully!' + responseText); - window.location.reload(); - - } - }, 'DELETE'); - -} - - -/** - * Tags the entries in the database. - * Optional Tagging commands are read from the input with ID - * 'input_tagging_name' (more in the Future) - */ -function tagEntries() { - const iban = document.getElementById('input_iban').value; - const rule_name = document.getElementById('tagging_name').value; - let rules = {} - if (rule_name) { - rules['rule_name'] = rule_name - } - - apiSubmit('tag/'+iban, rules, function (responseText, error) { - if (error) { - printResult('Tagging failed: ' + '(' + error + ')' + responseText); - - } else { - alert('Entries tagged successfully!' + responseText); - window.location.reload(); - - } - }, false); -} - - -function removeTags() { - const iban = document.getElementById('input_iban').value; - const checkboxes = document.querySelectorAll('input[name="entry-select[]"]'); - const t_ids = []; - checkboxes.forEach((checkbox) => { - if (checkbox.checked) { - t_ids.push(checkbox.value); - } - }); - - if (!iban) { - alert('Please provide an IBAN.'); - return; - } - if (!t_ids) { - alert('Please provide a Transaction ID (checkbox).'); - return; - } - - let api_function; - let tags = {}; - if (t_ids.length == 1) { - api_function = 'removeTag/'+iban+'/'+t_ids[0]; - } else { - api_function = 'removeTags/'+iban; - tags['t_ids'] = t_ids; - }; - - apiSubmit(api_function, tags, function (responseText, error) { - if (error) { - printResult('Tagging failed: ' + '(' + error + ')' + responseText); - - } else { - alert('Entries tagged successfully!' + responseText); - window.location.reload(); - - } - }, false); -} - - /** - * Tags the entries in the database in a direct manner (assign Categories, no rules) - * Optional Tagging commands are read from the inputs with IDs - * 'input_manual_category' , 'input_manual_tags' , 'input_iban' and 'input_tid'. - * While the IBAN and Transaction_ID are mandatory, the other inputs are optional. + * Saves a group with the specified name and associated IBANs. + * + * This function retrieves the group name from an input field and the selected IBANs + * from checkboxes. It then sends the data to the server using the `apiSubmit` function. + * If the operation is successful, the page is reloaded; otherwise, an error message is displayed. */ -function manualTagEntries() { - const category = document.getElementById('input_manual_category').value; - let tags = document.getElementById('input_manual_tags').value; - const iban = document.getElementById('input_iban').value; - - const checkboxes = document.querySelectorAll('input[name="entry-select[]"]'); - const t_ids = []; - checkboxes.forEach((checkbox) => { - if (checkbox.checked) { - t_ids.push(checkbox.value); - } - }); - - if (!iban) { - alert('Please provide an IBAN.'); - return; - } - if (!t_ids) { - alert('Please provide a Transaction ID (checkbox).'); - return; - } - - let tagging = { - 'category': category, - 'tags': tags - } - - let api_function; - if (t_ids.length == 1) { - api_function = 'setManualTag/'+iban+'/'+t_ids[0]; - } else { - api_function = 'setManualTags/'+iban; - tagging['t_ids'] = t_ids; - }; +function saveGroup() { + const groupname = document.getElementById("groupname-input").value; + const checkboxes = document.querySelectorAll('input[name="iban-checkbox"]:checked'); + const selectedIbans = Array.from(checkboxes).map(checkbox => checkbox.value); + const params = {'ibans': selectedIbans} - apiSubmit(api_function, tagging, function (responseText, error) { + apiSubmit('addgroup/' + groupname, params, function (responseText, error) { if (error) { - printResult('Tagging failed: ' + '(' + error + ')' + responseText); + alert('Gruppe nicht angelegt: ' + '(' + error + ')' + responseText); } else { - alert('Entries tagged successfully!' + responseText); + alert('Gruppe gespeichert!' + responseText); window.location.reload(); } }, false); -} - - -/** - * Fetches information based on the provided IBAN and UUID, and processes the response. - * - * @param {string} iban - The International Bank Account Number (IBAN) to identify the account. - * @param {string} uuid - The unique identifier associated with the request. - * @param {Function} [callback=alert] - A callback function to handle the response text. Defaults to `alert`. - */ -function getInfo(iban, uuid, callback = alert) { - apiGet('/'+iban+'/'+uuid, {}, function (responseText, error) { - if (error) { - printResult('getTx failed: ' + '(' + error + ')' + responseText); - - } else { - callback(responseText); - - } - }); -} - - -function saveMeta() { - const meta_type = document.getElementById('select_meta').value; - const fileInput = document.getElementById('input-json'); - if (fileInput.files.length === 0) { - alert('Please select a file to upload.'); - return; - } - - const params = { file: 'input_file' }; // The key 'file' corresponds to the input element's ID - apiSubmit('upload/metadata/'+meta_type, params, function (responseText, error) { - if (error) { - printResult('Rule saving failed: ' + '(' + error + ')' + responseText); - } else { - alert('Rule saved successfully!' + responseText); - - } - }, true); + return selectedIbans; } diff --git a/app/static/js/welcome.js b/app/static/js/welcome.js deleted file mode 100644 index 69fbbc4..0000000 --- a/app/static/js/welcome.js +++ /dev/null @@ -1,31 +0,0 @@ -"use strict"; - -document.addEventListener('DOMContentLoaded', function () { - - // PopUps - document.getElementById('add-button').addEventListener('click', function () { - openPopup('add-popup'); - }); - - // Import Input - const dropArea = document.getElementById('file-drop-area'); - const fileInput = document.getElementById('file-input'); - - dropArea.addEventListener('click', () => fileInput.click()); - - dropArea.addEventListener('dragover', (event) => { - event.preventDefault(); - dropArea.style.borderColor = '#000'; - }); - - dropArea.addEventListener('dragleave', () => { - dropArea.style.borderColor = '#ccc'; - }); - - dropArea.addEventListener('drop', (event) => { - event.preventDefault(); - dropArea.style.borderColor = '#ccc'; - fileInput.files = event.dataTransfer.files; - }); - -}); \ No newline at end of file diff --git a/app/templates/iban.html b/app/templates/iban.html new file mode 100644 index 0000000..ff6a7b0 --- /dev/null +++ b/app/templates/iban.html @@ -0,0 +1,89 @@ + +{% extends 'layout.html' %} + +{% block content %} + +
+ +
+ +
+ + + + + + + + + + + + + + {% for transaction in transactions %} + + + + + + + + + + {% endfor %} + +
DateTextCategoryTagsBetrag 
{{ transaction.date_tx }}{{ transaction.text_tx }} + 🔖 + + {% for tag in transaction.tags %} + + {{ tag }} + + {% endfor %} + {{ transaction.currency }} {{ transaction.betrag }} + +
+
+ + + + + + + + + + +{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html index 9260f6e..3d6a166 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,87 +1,93 @@ - {% extends 'layout.html' %} {% block content %} -
- -
-
- - - - - - - - - - - - - - {% for transaction in transactions %} - - - - - - - - - - {% endfor %} - -
DateTextCategoryTagsBetrag 
{{ transaction.date_tx }}{{ transaction.text_tx }} - 🔖 - - {% for tag in transaction.tags %} - - {{ tag }} - - {% endfor %} - {{ transaction.currency }} {{ transaction.betrag }} - -
+ {% if groups or ibans %} + Wähle + {% endif %} + {% if groups %} + eine Gruppe: +
    + {% for group in groups %} +
  • {{group}}
  • + {% endfor %} +
+ {% if ibans %} + oder + {% endif %} + {% endif %} + {% if ibans %} + ein Konto: +
    + {% for iban in ibans %} +
  • {{iban}}
  • + {% endfor %} +
+ {% else %} + Es sind noch keine Konten vorhanden. Starte mit deinem ersten Import... + {% endif %} + +

+ + +

- - diff --git a/app/ui.py b/app/ui.py index 5af00f8..ce920e1 100644 --- a/app/ui.py +++ b/app/ui.py @@ -311,8 +311,8 @@ def uploadRules(metadata): _ = self._mv_fileupload(input_file, path) return self._read_settings(path, metatype=metadata) - @current_app.route('/api/truncateDatabase/', methods=['DELETE']) - def truncateDatabase(iban): + @current_app.route('/api/deleteDatabase/', methods=['DELETE']) + def deleteDatabase(iban): """ Leert die Datenbank zu einer IBAN Args (uri): diff --git a/handler/BaseDb.py b/handler/BaseDb.py index 26968db..ca266fd 100644 --- a/handler/BaseDb.py +++ b/handler/BaseDb.py @@ -198,10 +198,36 @@ def delete(self, collection: str, condition: dict | list[dict]): raise NotImplementedError() def truncate(self, collection: str): - """Löscht alle Datensätze aus einer Tabelle/Collection + """Löscht eine Tabelle/Collection Args: collection (str): Name der Collection, in die Werte eingefügt werden sollen. + Returns: + dict: + - deleted, int: Anzahl der gelöschten Datensätze + """ + if not self.check_collection_is_iban(collection): + # Delete group config from metadata + return self.delete('metadata', [ + { + 'key': 'metatype', + 'value': 'config' + },{ + 'key': 'name', + 'value': 'group' + },{ + 'key': 'uuid', + 'value': collection + } + ]) + + return self._truncate(collection) + + def _truncate(self, collection): + """ + Private Methode zum Löschen einer Tabelle/Collection. + Siehe 'truncate' Methode. + Returns: dict: - deleted, int: Anzahl der gelöschten Datensätze diff --git a/handler/MongoDb.py b/handler/MongoDb.py index e5921d1..5157f66 100644 --- a/handler/MongoDb.py +++ b/handler/MongoDb.py @@ -186,9 +186,9 @@ def delete(self, collection, condition=None, multi='AND'): delete_result = collection.delete_many(query) return {'deleted': delete_result.deleted_count} - def truncate(self, collection): + def _truncate(self, collection): """ - Löscht alle Datensätze aus einer Tabelle/Collection + Löscht eine Tabelle/Collection Args: collection (str): Name der Collection, in die Werte eingefügt werden sollen. diff --git a/handler/TinyDb.py b/handler/TinyDb.py index df0f808..7338dfe 100644 --- a/handler/TinyDb.py +++ b/handler/TinyDb.py @@ -214,8 +214,8 @@ def delete(self, collection, condition=None, multi='AND'): deleted_ids = collection.remove(query) return {'deleted': len(deleted_ids)} - def truncate(self, collection): - """Löscht alle Datensätze aus einer Tabelle/Collection + def _truncate(self, collection): + """Löscht eine Tabelle/Collection Args: collection (str): Name der Collection, in die Werte eingefügt werden sollen. @@ -223,9 +223,8 @@ def truncate(self, collection): dict: - deleted, int: Anzahl der gelöschten Datensätze """ - table = self.connection.table(collection) - r = table.remove(lambda x: True) - return {'deleted': len(r)} + r = self.connection.drop_table(collection) + return {'deleted': 1} def get_metadata(self, uuid): collection = self.connection.table('metadata') diff --git a/tests/test_integ_basics.py b/tests/test_integ_basics.py index 580bfa5..1951a0c 100644 --- a/tests/test_integ_basics.py +++ b/tests/test_integ_basics.py @@ -24,7 +24,7 @@ def test_truncate(test_app): with test_app.app_context(): with test_app.test_client() as client: - result = client.delete("/api/truncateDatabase/DE89370400440532013000") + result = client.delete("/api/deleteDatabase/DE89370400440532013000") assert result.status_code == 200, "Fehler beim Leeren der Datenbank" From 36d2fe73894d60b27f5352fceab37e03600dd6bf Mon Sep 17 00:00:00 2001 From: Pitastic Date: Sun, 12 Oct 2025 21:57:04 +0200 Subject: [PATCH 11/12] add dry_run --- app/static/js/iban.js | 20 ++++++++++++++------ app/templates/iban.html | 12 ++++++++++++ app/ui.py | 4 ++-- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/app/static/js/iban.js b/app/static/js/iban.js index 0d4aac4..a22c8cb 100644 --- a/app/static/js/iban.js +++ b/app/static/js/iban.js @@ -118,13 +118,17 @@ function listTxElements() { * 'input_tagging_name' (more in the Future) */ function tagEntries() { + let payload = {}; const rule_name = document.getElementById('tag-select').value; - let rules = {} if (rule_name) { - rules['rule_name'] = rule_name + payload['rule_name'] = rule_name; + } + const dry_run = document.getElementById('tag-dry').checked; + if (dry_run) { + payload['dry_run'] = dry_run; } - apiSubmit('tag/'+IBAN, rules, function (responseText, error) { + apiSubmit('tag/'+IBAN, payload, function (responseText, error) { if (error) { printResult('Tagging failed: ' + '(' + error + ')' + responseText); @@ -141,13 +145,17 @@ function tagEntries() { * Optional Categorization commands are read from the input with ID */ function catEntries() { + let payload = {}; const rule_name = document.getElementById('cat-select').value; - let rules = {} if (rule_name) { - rules['rule_name'] = rule_name + payload['rule_name'] = rule_name + } + const dry_run = document.getElementById('cat-dry').checked; + if (dry_run) { + payload['dry_run'] = dry_run; } - apiSubmit('cat/'+IBAN, rules, function (responseText, error) { + apiSubmit('cat/'+IBAN, payload, function (responseText, error) { if (error) { printResult('Categorization failed: ' + '(' + error + ')' + responseText); diff --git a/app/templates/iban.html b/app/templates/iban.html index ce9e586..cd4d73e 100644 --- a/app/templates/iban.html +++ b/app/templates/iban.html @@ -93,6 +93,12 @@

Tag

+

+ +

@@ -105,6 +111,12 @@

Kategorie

+

+ +

diff --git a/app/ui.py b/app/ui.py index ce920e1..6893d76 100644 --- a/app/ui.py +++ b/app/ui.py @@ -298,11 +298,11 @@ def uploadRules(metadata): Args (uri, multipart/form-data): metadata (str): [regex|parser|config] Type of Metadata to save - input_file (binary): Dateiupload aus Formular-Submit + file-input (binary): Dateiupload aus Formular-Submit Returns: json: Informationen zur Datei und Ergebnis der Untersuchung. """ - input_file = request.files.get('input_file') + input_file = request.files.get('file-input') if not input_file: return {'error': 'No file provided'}, 400 From d4a38f145e3d70030e691d8c57fc7d31d605f5f5 Mon Sep 17 00:00:00 2001 From: Pitastic Date: Mon, 13 Oct 2025 22:42:43 +0200 Subject: [PATCH 12/12] Metadata Fileupload, Get & Set --- app/static/css/style.css | 2 +- app/static/js/iban.js | 5 -- app/static/js/index.js | 95 ++++++++++++++++++++++++++++++++++++++ app/templates/iban.html | 7 --- app/templates/index.html | 39 ++++++++++++++++ app/ui.py | 72 ++++++----------------------- tests/test_integ_basics.py | 4 +- 7 files changed, 150 insertions(+), 74 deletions(-) diff --git a/app/static/css/style.css b/app/static/css/style.css index 74b8c15..bff7dea 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -16,7 +16,7 @@ html, body { display: inline-block; } -#file-drop-area { +#file-drop-area, #settings-drop-area { border: 2px dashed #ccc; padding: 20px; text-align: center; diff --git a/app/static/js/iban.js b/app/static/js/iban.js index a22c8cb..6dabfe2 100644 --- a/app/static/js/iban.js +++ b/app/static/js/iban.js @@ -5,11 +5,6 @@ let IBAN = window.location.pathname.split('/').pop(); document.addEventListener('DOMContentLoaded', function () { - // PopUps - document.getElementById('settings-button').addEventListener('click', function () { - openPopup('settings-popup'); - }); - // enabling/disabling the edit button based on checkbox selection const selectAllCheckbox = document.getElementById('select-all'); rowCheckboxes = document.querySelectorAll('.row-checkbox'); diff --git a/app/static/js/index.js b/app/static/js/index.js index a1b5aca..a5afe2c 100644 --- a/app/static/js/index.js +++ b/app/static/js/index.js @@ -9,6 +9,9 @@ document.addEventListener('DOMContentLoaded', function () { document.getElementById('add-group-btn').addEventListener('click', function () { openPopup('add-group'); }); + document.getElementById('settings-button').addEventListener('click', function () { + openPopup('settings-popup'); + }); // Import Input const fileInput = document.getElementById('file-input'); @@ -40,18 +43,110 @@ document.addEventListener('DOMContentLoaded', function () { } }); + // Metadata-Select + document.getElementById('read-setting').addEventListener('change', function () { + document.getElementById('set-setting').value = ""; + }); }); // ---------------------------------------------------------------------------- // -- DOM Functions ----------------------------------------------------------- // ---------------------------------------------------------------------------- +/** + * Gets a value for a Metadate into a textarea. + * The key is selected via the select input element 'read-setting' + * and written to 'set-setting'. + */ +function loadSetting() { + const setting_uuid = document.getElementById('read-setting').value; + const result_text = document.getElementById('set-setting'); + if (!setting_uuid) { + alert('Kein Name einer Einstellung angegeben!'); + return; + } + + apiGet('getMeta/' + setting_uuid, {}, function (responseText, error) { + if (error) { + alert('Settings not loaded: ' + '(' + error + ')' + responseText); + + } else { + result_text.value = responseText; + + } + }); +} + +/** + * Sets a value for a Metadate. + * The key is selected via the select input element 'read-setting' + * and the value is taken from 'set-setting'. + */ +function saveSetting() { + const setting_uuid = document.getElementById('read-setting').value; + const result_text = document.getElementById('set-setting'); + if (!setting_uuid || !result_text.value) { + alert('Kein Name einer Einstellung oder Wert angegeben!'); + return; + } + + let payload; + let meta_type; + try { + payload = JSON.parse(result_text.value); + if (!payload['metatype']) { + throw new ValueError("No metatype provided!"); + } + meta_type = payload['metatype']; + + } catch (error) { + alert('Could not parse settingsvalue!' + error); + return; + } + + apiSubmit('saveMeta/' + meta_type, payload, function (responseText, error) { + if (error) { + alert('Settings not saved: ' + '(' + error + ')' + responseText); + + } else { + alert('Settings saved: ' + responseText) + result_text.value = ''; + + } + }, false); +} // ---------------------------------------------------------------------------- // -- API Functions ----------------------------------------------------------- // ---------------------------------------------------------------------------- +/** + * Sends a file to the server for upload. + * The file is selected via the file input element 'settings-input'. + */ +function uploadFile() { + const settings_type = document.getElementById('settings-type').value; + + const fileInput = document.getElementById('settings-input'); + if (fileInput.files.length === 0) { + alert('Please select a file to upload.'); + return; + } + + const params = { file: 'file-input' }; // The value of 'file' corresponds to the input element's ID + apiSubmit('upload/metadata/' + settings_type, params, function (responseText, error) { + if (error) { + alert('File upload failed: ' + '(' + error + ')' + responseText); + + } else { + alert('File uploaded successfully!' + responseText); + window.location.href = '/' + settings_type; + + } + }, true); +} + /** * Sends a file to the server for upload. * The file is selected via the file input element 'file-input'. diff --git a/app/templates/iban.html b/app/templates/iban.html index cd4d73e..3a8743e 100644 --- a/app/templates/iban.html +++ b/app/templates/iban.html @@ -23,7 +23,6 @@
  • - 🚪
  • @@ -121,12 +120,6 @@

    Kategorie

    - - + + + {% endblock %} diff --git a/app/ui.py b/app/ui.py index 6893d76..dc6e341 100644 --- a/app/ui.py +++ b/app/ui.py @@ -85,7 +85,8 @@ def iban(iban) -> str: # No IBAN selected, show Welcome page with IBANs ibans = self.db_handler.list_ibans() groups = self.db_handler.list_groups() - return render_template('index.html', ibans=ibans, groups=groups) + meta = self.db_handler.filter_metadata(condition=None) + return render_template('index.html', ibans=ibans, groups=groups, meta=meta) if not self.db_handler.get_group_ibans(iban, True): # It's not an IBAN or valid Groupname @@ -192,28 +193,20 @@ def getTx(iban, t_id): return tx_details[0], 200 @current_app.route('/api/saveMeta/', defaults={'rule_type':'rule'}, methods=['POST']) - @current_app.route('/api/saveMeta/', methods=['POST']) + @current_app.route('/api/saveMeta/', methods=['PUT']) def saveMeta(rule_type): """ Einfügen oder updaten von Metadaten in der Datenbank. Args (json / file): - rule_type, str: Typ der Regel (rule | parser) + rule_type, str: Typ der Regel (rule | parser | config) rule, dict: Regel-Objekt """ - input_file = request.files.get('file-input') - if not input_file and not request.json: + if not request.json: return {'error': 'No file or json provided'}, 400 - if input_file: - # Store Upload file to tmp - path = '/tmp/metadata.tmp' - _ = self._mv_fileupload(input_file, path) - r = self.db_handler.import_metadata(path=path, metatype=rule_type) - - else: - entry = request.json - entry['metatype'] = rule_type - r = self.db_handler.set_metadata(entry, overwrite=True) + entry = request.json + entry['metatype'] = rule_type + r = self.db_handler.set_metadata(entry, overwrite=True) if not r.get('inserted'): return {'error': 'No data inserted', 'reason': r.get('error')}, 400 @@ -309,7 +302,11 @@ def uploadRules(metadata): # Store Upload file to tmp path = f'/tmp/{metadata}.tmp' _ = self._mv_fileupload(input_file, path) - return self._read_settings(path, metatype=metadata) + + # Import and cleanup + result = self.db_handler.import_metadata(path, metatype=metadata) + os.remove(path) + return result, 201 if result.get('inserted') else 200 @current_app.route('/api/deleteDatabase/', methods=['DELETE']) def deleteDatabase(iban): @@ -728,46 +725,3 @@ def _read_input(self, uri, bank='Generic', data_format=None): return [] return self.tagger.parse(data) - - def _read_settings(self, uri, metatype): - """ - Liest eine Datei mit Metadaten ein, die entweder Konfigurationen, - Regeln für das Tagging oder Regeln für das Parsing enthalten kann. - - Args: - uri (str): Pfad zur JSON mit den Eingabedaten. - metatype (str): [rule|parser|config] Art der Metadaten. - Sie dürfen nicht gemischt vorliegen. - Returns: - list(dict): Geparste Objekte für das Einfügen in die Datenbank. - """ - with open(uri, 'r', encoding='utf-8') as infile: - try: - parsed_data = json.load(infile) - - except json.JSONDecodeError as e: - logging.warning(f"Failed to parse JSON file: {e}") - return {'error': 'Invalid file format (not json)'}, 400 - - if isinstance(parsed_data, list): - - for i, _ in enumerate(parsed_data): - parsed_data[i]['metatype'] = metatype - - else: - parsed_data['metatype'] = metatype - parsed_data = [parsed_data] - - # Verarbeitete Metadataen in die DB speichern - # und vom Objekt und Dateisystem löschen - inserted = 0 - for data in parsed_data: - inserted += self.db_handler.set_metadata(data).get('inserted') - - os.remove(uri) - - return_code = 201 if inserted else 200 - return { - 'metatype': metatype, - 'inserted': inserted, - }, return_code diff --git a/tests/test_integ_basics.py b/tests/test_integ_basics.py index 1951a0c..807728b 100644 --- a/tests/test_integ_basics.py +++ b/tests/test_integ_basics.py @@ -176,7 +176,7 @@ def test_save_meta(test_app): 'name': 'Test Parsing 4 Digits', 'regex': '[0-9]]{4}' } - result = client.post("/api/saveMeta/parser", json=parameters) + result = client.put("/api/saveMeta/parser", json=parameters) assert result.status_code == 201, \ "Der Statuscode war nicht wie erwartet" @@ -192,7 +192,7 @@ def test_save_meta(test_app): parameters = json.dumps(parameters).encode('utf-8') files = {'file-input': (io.BytesIO(parameters), 'commerzbank.csv')} result = client.post( - "/api/saveMeta/", + "/api/upload/metadata/parser", data=files, content_type='multipart/form-data' ) assert result.status_code == 201, \