Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions app/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,8 +498,42 @@ def uploadRules(metadata):
# Import and cleanup
result = parent.db_handler.import_metadata(path, metatype=metadata)
os.remove(path)
if result.get('error'):
return {'error': f"Die Datei konnte nicht importiert werden: {result.get('error')}"}, 400

return result, 201 if result.get('inserted') else 200

@current_app.route('/api/export/metadata/<metatype>', methods=['GET'])
def exportMetadata(metatype):
"""
Endpunkt für das Exportieren von Metadaten.

Args (uri):
metatype (str): Type of Metadata to export
Returns:
json: Informationen zur Datei und Ergebnis der Untersuchung.
"""
if metatype not in ['rule', 'parser', 'config']:
return {'error': 'Ungültiger Metadatentyp (rule, parser, config)'}, 400

meta = parent.db_handler.filter_metadata({
'key': 'metatype',
'value': metatype
})

# Strip uuids for export
for m in meta:
m.pop('uuid', None)

# Create file response
response = make_response(json.dumps(meta, indent=4))
response.headers['Content-Type'] = 'application/json'
response.headers['Content-Disposition'] = (
f'attachment; filename={metatype}_export.json'
)
return response


@current_app.route('/api/deleteDatabase/<iban>', methods=['DELETE'])
def deleteDatabase(iban):
"""
Expand Down
19 changes: 11 additions & 8 deletions app/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,15 @@ def create_app(config_path: str) -> Flask:

return app

config = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'config.py'
)
application = create_app(config)

if __name__ == "__main__":
# Run the application directly if executed as a standalone script
application.run(host='0.0.0.0', port=8000, debug=True)
# Only create the application if not in a test environment
if os.getenv('PYTEST_MODE') is None: # Or another test-detection method
config = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'config.py'
)
application = create_app(config)

if __name__ == "__main__":
# Run the application directly if executed as a standalone script
application.run(host='0.0.0.0', port=8000, debug=True)
72 changes: 49 additions & 23 deletions app/static/css/grid.css
Original file line number Diff line number Diff line change
Expand Up @@ -78,45 +78,71 @@
display: grid;
grid-template-columns: 1fr;
}
.transactions thead {
display: none;
}
.transactions tr {
display: grid;
grid-template-columns: 1fr 5fr 5fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 0.25em;
grid-template-areas:
"checkbox dates category amount"
"button betreff betreff betreff";
padding: 1em 0;
}
.transactions td {
display: block;
border: none;
padding-top: 0em;
padding-bottom: 0em;
}
.transactions thead {
padding: 1em 0;
}
.transactions thead tr {
display: grid;
grid-template-columns: 1fr 3fr 6fr 1fr;
grid-template-rows: 1fr;
gap: 0.25em;
grid-template-areas:
"checkbox dates betreff amount";
padding: 1em 0;
}
.transactions thead th {
border: none;
}
.transactions thead th:first-child {
padding-left: 0;
}
.transactions tbody tr {
display: grid;
grid-template-columns: 1fr 5fr 5fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 0.25em;
grid-template-areas:
"checkbox dates category amount"
"button betreff betreff betreff";
padding: 1em 0;
}
.transactions td {
display: block;
border: none;
padding-top: 0em;
padding-bottom: 0em;
}

/* Naming and Styling Cells */
.transactions tr td:nth-child(1){
.transactions tr td:nth-child(1),
.transactions tr th:nth-child(1){
padding-left: 0;
grid-area: checkbox;
}
.transactions tr td:nth-child(2){grid-area: dates;}
.transactions tr td:nth-child(3){
.transactions tr td:nth-child(2),
.transactions tr th:nth-child(2) {
grid-area: dates;
}
.transactions tr td:nth-child(3),
.transactions tr th:nth-child(3){
padding-right: 0;
grid-area: betreff;
}
.transactions tr td:nth-child(4){grid-area: category;}
.transactions tr td:nth-child(6){
.transactions tr td:nth-child(6),
.transactions tr th:nth-child(6){
padding-right: 0;
grid-area: amount;
}
.transactions tr td:nth-child(7){
padding-left: 0;
grid-area: button;
}
.transactions tr th:nth-child(4),
.transactions tr th:nth-child(7) {
display: none;
}


/* TODO: #48 ,TX Details PopUp
#dynamic-results tr:last-child th,
Expand Down
29 changes: 29 additions & 0 deletions app/static/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,35 @@ function importSettings() {
}, true);
}

/**
* Sends a request to the server to export settings of the selected type.
* The type is selected via the select input element 'export-setting-type'.
*/
function exportSettings() {
const settings_type = document.getElementById('export-setting-type').value;
if (!settings_type) {
alert('Please select a settings type to export.');
return;
}

apiGet('export/metadata/' + settings_type, {}, function (response, error) {
if (error) {
showAjaxError(error, response);
} else {
const blob = new Blob([JSON.stringify(response, null, 4)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${settings_type}_export.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
});
}


/**
* Sends transactions in a file or a batch of files to the server for upload.
* The file is selected via the file input element 'file-input' (multiple)
Expand Down
22 changes: 22 additions & 0 deletions app/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,28 @@ <h2>Settings</h2>
<button class="delete" onclick="deleteSetting()">Löschen</button>
</footer>
</details>
<details>
<summary>
Export in eine Datei:
</summary>
<p>
Exportiere eine Art von Einstellungen in eine <kbd>.json</kbd> Datei, um sie über das
Serververzeichnis <code>/app/settings/*</code> oder in der Oberfläche wieder zu importieren.
</p>
<p>
Namensgleiche Einstellungen werden überschrieben, neue hinzugefügt.
</p>
<p>
<select id="export-setting-type">
<option value="config">Einstellungen</option>
<option value="parser">Parsing-Anweisungen</option>
<option value="rule">Regeln</option>
</select>
</p>
<footer dir="rtl">
<button class="secondary" onclick="exportSettings()">Export</button>
</footer>
</details>
</article>
</dialog>

Expand Down
7 changes: 5 additions & 2 deletions handler/MongoDb.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,11 @@ def _form_condition(self, condition):
# Empty lists
stmt = {'$size': 0}
else:
# Lists with exact members
stmt = {'$all': condition.get('value')}
# Lists with exact members and exact length
stmt = {
'$all': condition.get('value'),
'$size': len(condition.get('value'))
}

# Nested or Plain Key
condition_key = condition.get('key')
Expand Down
4 changes: 4 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(parent_dir)

# Set Env before more imports
os.environ['PYTEST_MODE'] = '1'

from helper import MockDatabase
from app.server import create_app

Expand All @@ -22,6 +25,7 @@ def test_app():

# Config
root_path = os.path.dirname(os.path.realpath(__file__))
print(f"Root Path: {root_path}")
config_path = os.path.join(
root_path,
'config.py'
Expand Down
9 changes: 0 additions & 9 deletions tests/start_test.sh

This file was deleted.

54 changes: 0 additions & 54 deletions tests/test_integ_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -732,60 +732,6 @@ def test_iban_filtering(test_app):
f"Es wurden {len(rows)} Einträge gefunden, statt der erwarteten 1"


def test_iban_filtering_tags(test_app):
"""Eigene Funktion zum Testen der aufwendigeren Tag-Filter"""
with test_app.app_context():

with test_app.test_client() as client:

# in
result = client.get(r"/DE89370400440532013000?tags=Supermarkt%2CStadt&tag_mode=in")
soup = BeautifulSoup(result.text, features="html.parser")
rows = soup.css.select('table.transactions tr[name] td input.row-checkbox')
assert result.status_code == 200, \
"Die Ergebnisseite mit den Transaktionen ist nicht (richtig) erreichbar"
assert len(rows) == 2, \
f"Es wurden {len(rows)} Einträge gefunden, statt der erwarteten 2"

# notin
result = client.get(r"/DE89370400440532013000?tags=Supermarkt%2CStadt&tag_mode=notin")
soup = BeautifulSoup(result.text, features="html.parser")
rows = soup.css.select('table.transactions tr[name] td input.row-checkbox')
assert result.status_code == 200, \
"Die Ergebnisseite mit den Transaktionen ist nicht (richtig) erreichbar"
assert len(rows) == 3, \
f"Es wurden {len(rows)} Einträge gefunden, statt der erwarteten 3"

# all
result = client.get(
r"/DE89370400440532013000?tags=Test_SECONDARY_2%2CReplaced_TAG&tag_mode=all")
soup = BeautifulSoup(result.text, features="html.parser")
rows = soup.css.select('table.transactions tr[name] td input.row-checkbox')
assert result.status_code == 200, \
"Die Ergebnisseite mit den Transaktionen ist nicht (richtig) erreichbar"
assert len(rows) == 1, \
f"Es wurden {len(rows)} Einträge gefunden, statt der erwarteten 1"

# exact (==)
result = client.get(
r"/DE89370400440532013000?tags=Test_SECONDARY_2%2CReplaced_TAG&tag_mode=exact")
soup = BeautifulSoup(result.text, features="html.parser")
rows = soup.css.select('table.transactions tr[name] td input.row-checkbox')
assert result.status_code == 200, \
"Die Ergebnisseite mit den Transaktionen ist nicht (richtig) erreichbar"
assert len(rows) == 1, \
f"Es wurden {len(rows)} Einträge gefunden, statt der erwarteten 1"

# exact (== no tags)
result = client.get(
r"/DE89370400440532013000?tags=&tag_mode=exact")
soup = BeautifulSoup(result.text, features="html.parser")
rows = soup.css.select('table.transactions tr[name] td input.row-checkbox')
assert result.status_code == 200, \
"Die Ergebnisseite mit den Transaktionen ist nicht (richtig) erreichbar"
assert len(rows) == 0, \
f"Es wurden {len(rows)} Einträge gefunden, statt der erwarteten 0"

def test_statsapi(test_app):
"""Testet den API-Endpoint für die Statistiken"""
with test_app.app_context():
Expand Down
Loading
Loading