monopyly 1.5.1__py3-none-any.whl → 1.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. monopyly/CHANGELOG.md +27 -0
  2. monopyly/README.md +3 -3
  3. monopyly/__init__.py +22 -27
  4. monopyly/_version.py +2 -2
  5. monopyly/auth/blueprint.py +2 -0
  6. monopyly/auth/routes.py +2 -3
  7. monopyly/banking/accounts.py +7 -7
  8. monopyly/banking/actions.py +20 -17
  9. monopyly/banking/banks.py +1 -1
  10. monopyly/banking/blueprint.py +2 -0
  11. monopyly/banking/filters.py +6 -6
  12. monopyly/banking/forms.py +3 -5
  13. monopyly/banking/routes.py +72 -10
  14. monopyly/banking/transactions.py +15 -7
  15. monopyly/common/forms/__init__.py +8 -0
  16. monopyly/common/forms/_forms.py +1 -2
  17. monopyly/common/forms/fields.py +0 -2
  18. monopyly/common/forms/utils.py +1 -1
  19. monopyly/common/transactions.py +89 -14
  20. monopyly/core/actions.py +2 -8
  21. monopyly/core/blueprint.py +2 -0
  22. monopyly/core/filters.py +0 -2
  23. monopyly/core/routes.py +1 -1
  24. monopyly/credit/accounts.py +1 -1
  25. monopyly/credit/actions.py +4 -5
  26. monopyly/credit/blueprint.py +2 -0
  27. monopyly/credit/cards.py +7 -3
  28. monopyly/credit/forms.py +3 -5
  29. monopyly/credit/routes.py +65 -87
  30. monopyly/credit/statements.py +1 -1
  31. monopyly/credit/transactions/__init__.py +2 -0
  32. monopyly/credit/transactions/_transactions.py +18 -8
  33. monopyly/credit/transactions/activity/__init__.py +6 -0
  34. monopyly/credit/transactions/activity/parser.py +0 -1
  35. monopyly/credit/transactions/activity/reconciliation.py +25 -4
  36. monopyly/database/__init__.py +1 -59
  37. monopyly/database/models.py +198 -276
  38. monopyly/database/preloads.sql +6 -1
  39. monopyly/scripts/screenshot_application.py +100 -0
  40. monopyly/static/chartist-1.5.0.min.js +8 -0
  41. monopyly/static/css/style.css +35 -14
  42. monopyly/static/img/about/bank-account-details.png +0 -0
  43. monopyly/static/img/about/bank-account-summaries.png +0 -0
  44. monopyly/static/img/about/bank-accounts.png +0 -0
  45. monopyly/static/img/about/credit-account-details.png +0 -0
  46. monopyly/static/img/about/credit-statement-details.png +0 -0
  47. monopyly/static/img/about/credit-transactions.png +0 -0
  48. monopyly/static/img/about/homepage-user.png +0 -0
  49. monopyly/static/img/about/homepage.png +0 -0
  50. monopyly/static/jquery-3.7.1.min.js +2 -0
  51. monopyly/static/js/add-transfer.js +8 -9
  52. monopyly/static/js/bind-tag-actions.js +6 -0
  53. monopyly/static/js/create-balance-chart.js +2 -2
  54. monopyly/static/js/create-category-chart.js +1 -1
  55. monopyly/static/js/load-more-transactions.js +27 -0
  56. monopyly/static/js/modules/expand-transaction.js +7 -6
  57. monopyly/static/js/modules/update-display-ajax.js +20 -1
  58. monopyly/static/js/update-transactions-display.js +8 -2
  59. monopyly/templates/banking/account_page.html +15 -16
  60. monopyly/templates/banking/account_summaries.html +2 -2
  61. monopyly/templates/banking/account_summary.html +1 -1
  62. monopyly/templates/banking/accounts_page.html +2 -2
  63. monopyly/templates/banking/transactions_table/table.html +3 -0
  64. monopyly/templates/banking/transactions_table/transactions.html +0 -1
  65. monopyly/templates/common/tag_tree.html +25 -0
  66. monopyly/templates/{credit → common}/tags_page.html +7 -3
  67. monopyly/templates/common/transactions_table/linked_bank_transaction.html +2 -2
  68. monopyly/templates/common/transactions_table/table.html +6 -0
  69. monopyly/templates/common/transactions_table/transactions.html +9 -15
  70. monopyly/templates/core/index.html +112 -101
  71. monopyly/templates/core/profile.html +1 -1
  72. monopyly/templates/credit/statement_page.html +2 -2
  73. monopyly/templates/credit/statement_reconciliation/statement_reconciliation_inquiry.html +1 -1
  74. monopyly/templates/credit/statement_reconciliation/statement_reconciliation_page.html +3 -3
  75. monopyly/templates/credit/statement_summary.html +2 -2
  76. monopyly/templates/credit/transaction_submission_page.html +3 -3
  77. monopyly/templates/credit/transactions_page.html +19 -3
  78. monopyly/templates/credit/transactions_table/condensed_row_content.html +2 -3
  79. monopyly/templates/credit/transactions_table/expanded_row_content.html +5 -5
  80. monopyly/templates/credit/transactions_table/table.html +3 -0
  81. monopyly/templates/credit/transactions_table/transactions.html +0 -1
  82. monopyly/templates/layout.html +9 -4
  83. {monopyly-1.5.1.dist-info → monopyly-1.6.0.dist-info}/METADATA +12 -13
  84. {monopyly-1.5.1.dist-info → monopyly-1.6.0.dist-info}/RECORD +88 -87
  85. monopyly-1.6.0.dist-info/entry_points.txt +3 -0
  86. monopyly/cli/apps.py +0 -108
  87. monopyly/cli/launch.py +0 -135
  88. monopyly/config/__init__.py +0 -1
  89. monopyly/config/default_settings.py +0 -56
  90. monopyly/config/settings.py +0 -59
  91. monopyly/static/jquery-3.7.0.min.js +0 -2
  92. monopyly/templates/credit/tag_tree/subtag_tree.html +0 -22
  93. monopyly/templates/credit/tag_tree/tag_tree.html +0 -13
  94. monopyly-1.5.1.dist-info/entry_points.txt +0 -2
  95. {monopyly-1.5.1.dist-info → monopyly-1.6.0.dist-info}/WHEEL +0 -0
  96. {monopyly-1.5.1.dist-info → monopyly-1.6.0.dist-info}/licenses/COPYING +0 -0
  97. {monopyly-1.5.1.dist-info → monopyly-1.6.0.dist-info}/licenses/LICENSE +0 -0
monopyly/CHANGELOG.md CHANGED
@@ -207,7 +207,34 @@
207
207
  - Return to statement details page after deleting a transaction (rather than returning to the general transactions page)
208
208
  - Allow users to change their password
209
209
  - Warn users before form submission if the configuration currently disallows registration
210
+ - Fix issues with the application launcher not launching the browser; couple application launch process more tightly with click
211
+ - Use type annotations for SQLAlchemy ORM declarative mappings
210
212
  - Increase the flexibility of the credit activity parser
211
213
 
212
214
 
215
+ ### 1.5.2
216
+
217
+ - Use smoothing on charts for up to 100 transactions
218
+ - Improve tokenization normalization for credit reconciliation
219
+ - Remove statement requiring activity files be located in the `Downloads` directory
220
+ - Do not clear the reconciliation info when adding subtransaction fields via POST request
221
+ - Incorporate support for enhanced database handler selection subsets
222
+ - Bump dependencies (including support for recent SQLAlchemy versions)
223
+
224
+
225
+ ### 1.6.0
226
+
227
+ - Enable additional transactions to be loaded on bank/credit transactions tables
228
+ - Create a script to take application screenshots (e.g., for the 'About' page)
229
+ - Remove Python version requirement (allow Python versions after 3.10)
230
+ - Use Jinja recursive loops for transaction tag tree structures in templates
231
+ - Refactor to include ruff-based linting checks
232
+ - Set 'Credit payments' to be a default tag for all users
233
+ - Protect globally defined tags from user deletion
234
+ - Hide transaction tags when the Escape key is pressed
235
+ - Ensure that the 'Record Transfer' functionality only ever adds one input box
236
+ - Update dependencies (including using ruff in place of _Black_ and _isort_)
237
+ - Update JavaScript libraries (jQuery, Chartist)
238
+
239
+
213
240
  <a name="bottom" id="bottom"></a>
monopyly/README.md CHANGED
@@ -27,10 +27,10 @@ The package requires a recent version of Python (3.10+).
27
27
 
28
28
  ## Getting started
29
29
 
30
- Once the package is properly installed, run the app in local mode from the command line (the default options should be sensible, but you may customize the host and port if necessary):
30
+ Once the package is properly installed, launch the app in local mode from the command line (the default options should be sensible, but you may customize the host and port if necessary):
31
31
 
32
32
  ```
33
- $ monopyly local --browser [--host HOST] [--port PORT]
33
+ $ monopyly launch local --browser [--host HOST] [--port PORT]
34
34
  ```
35
35
 
36
36
  Local mode indicates that the app is just going to be run using a locally hosted server, accessible to just your machine.
@@ -123,7 +123,7 @@ Card balances are also visible by visiting the pages for individual statements.
123
123
  A full history of statements for each card is available off the homepage.
124
124
  Each statement's page gives the statement's balance, transactions, and due date.
125
125
 
126
- <img class="screenshot" src="monopyly/static/img/about/statement-details.png" alt="statement details" width="800px" />
126
+ <img class="screenshot" src="monopyly/static/img/about/credit-statement-details.png" alt="statement details" width="800px" />
127
127
 
128
128
  Payments can be made directly from a statement's page and can be linked to a bank account in the _Monopyly_ system for simplified tracking.
129
129
  (Note that even linked transactions must be edited independently, as there are times when a user may wish to have separate values for linked transactions. For example, a credit card payment may be processed on a given date while it is only registered as a bank account transaction several days later.)
monopyly/__init__.py CHANGED
@@ -2,39 +2,29 @@
2
2
  Run the Monopyly app.
3
3
  """
4
4
 
5
- from flask import Flask
5
+ from dry_foundation import DryFlask, Factory, interact
6
6
 
7
- from monopyly.config import DevelopmentConfig, ProductionConfig
8
- from monopyly.core.errors import render_error_template
9
- from monopyly.database import SQLAlchemy, register_db_cli_commands
7
+ from .core.errors import render_error_template
8
+ from .database import SQLAlchemy
10
9
 
11
10
 
12
- def create_app(test_config=None, debug=None):
13
- # Create and configure the app
14
- app = Flask(__name__, instance_relative_config=True)
15
-
16
- # Prepare the app configuration
17
- if test_config:
18
- config = test_config
19
- else:
20
- # Load the development/production config when not testing
21
- if app.debug or debug:
22
- config = DevelopmentConfig.configure_for_instance(app.instance_path)
23
- else:
24
- config = ProductionConfig.configure_for_instance(app.instance_path)
25
- app.config.from_object(config)
26
-
27
- # Initialize the app, including CLI commands and blueprints
28
- init_app(app)
29
- return app
30
-
11
+ @Factory(db_interface=SQLAlchemy, echo_engine=False)
12
+ def create_app(config=None):
13
+ """
14
+ Create the Flask application.
31
15
 
32
- @SQLAlchemy.interface_selector
33
- def init_app(app):
34
- """Initialize the app."""
16
+ Create the Flask app, including configurations as specified. This
17
+ will configure the app using the configuration objects made
18
+ available by the Monopyly application and initialize the app
19
+ by registering app blueprints, routes, and commands.
20
+ """
21
+ # Create and configure the app
22
+ app = DryFlask(__name__, app_name="Monopyly")
23
+ app.configure(config)
24
+ # Register blueprints and error handlers specific to this app
35
25
  register_blueprints(app)
36
26
  register_errorhandlers(app)
37
- register_db_cli_commands(app)
27
+ return app
38
28
 
39
29
 
40
30
  def register_blueprints(app):
@@ -76,3 +66,8 @@ def register_errorhandlers(app):
76
66
  ]
77
67
  for code in handled_error_codes:
78
68
  app.register_error_handler(code, render_error_template)
69
+
70
+
71
+ def main():
72
+ """The entry point to the Monopyly application."""
73
+ interact(__name__)
monopyly/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.5.1'
16
- __version_tuple__ = version_tuple = (1, 5, 1)
15
+ __version__ = version = '1.6.0'
16
+ __version_tuple__ = version_tuple = (1, 6, 0)
@@ -9,3 +9,5 @@ bp = Blueprint("auth", __name__, url_prefix="/auth")
9
9
 
10
10
  # Import routes after defining blueprint to avoid circular imports
11
11
  from . import routes
12
+
13
+ __all__ = ["routes"]
monopyly/auth/routes.py CHANGED
@@ -2,6 +2,7 @@
2
2
  Routes for site authentication.
3
3
  """
4
4
 
5
+ from dry_foundation.database import db_transaction
5
6
  from flask import (
6
7
  current_app,
7
8
  flash,
@@ -12,8 +13,6 @@ from flask import (
12
13
  session,
13
14
  url_for,
14
15
  )
15
- from fuisce.database import db_transaction
16
- from sqlalchemy import select
17
16
  from werkzeug.security import check_password_hash, generate_password_hash
18
17
 
19
18
  from ..database.models import User
@@ -34,7 +33,7 @@ def register():
34
33
  error = "Username is required."
35
34
  elif not password:
36
35
  error = "Password is required."
37
- elif user := identify_user(username):
36
+ elif identify_user(username):
38
37
  error = f"User {username} is already registered."
39
38
  else:
40
39
  # Create a new user
@@ -3,8 +3,8 @@ Tools for interacting with bank accounts in the database.
3
3
  """
4
4
 
5
5
  import sqlalchemy.sql.functions as sql_func
6
- from authanor.database.handler import DatabaseViewHandler
7
- from werkzeug.exceptions import abort
6
+ from dry_foundation.database.handler import DatabaseViewHandler
7
+ from flask import abort
8
8
 
9
9
  from ..common.forms.utils import execute_on_form_validation
10
10
  from ..database.models import (
@@ -114,15 +114,15 @@ class BankAccountTypeHandler(
114
114
  super().delete_entry(entry_id)
115
115
 
116
116
  @classmethod
117
- def _validate_authorization(cls, entry_id):
118
- account_type = super()._validate_authorization(entry_id)
119
- # Limit manipulation to only the user (excluding common entries)
117
+ def _retrieve_authorized_manipulable_entry(cls, entry_id):
118
+ account_type = super()._retrieve_authorized_manipulable_entry(entry_id)
120
119
  if account_type.user_id != cls.user_id:
121
120
  abort_msg = (
122
121
  "The current user is not authorized to manipulate "
123
122
  "this account type entry."
124
123
  )
125
124
  abort(403, abort_msg)
125
+ return account_type
126
126
 
127
127
 
128
128
  class BankAccountHandler(
@@ -229,10 +229,10 @@ class BankAccountHandler(
229
229
  return account
230
230
 
231
231
  @classmethod
232
- def _filter_entries(cls, query, criteria):
232
+ def _filter_entries(cls, query, criteria, offset, limit):
233
233
  # Add a join to enable filtering by bank account type
234
234
  query = query.join(BankAccountTypeView)
235
- return super()._filter_entries(query, criteria)
235
+ return super()._filter_entries(query, criteria, offset, limit)
236
236
 
237
237
  @classmethod
238
238
  def delete_entry(cls, entry_id):
@@ -1,6 +1,6 @@
1
1
  """Module describing logical banking actions (to be used in routes)."""
2
2
 
3
- from collections import UserList, namedtuple
3
+ from collections import UserDict, namedtuple
4
4
 
5
5
  from ..common.utils import convert_date_to_midnight_timestamp
6
6
  from .accounts import BankAccountHandler, BankAccountTypeHandler
@@ -30,25 +30,26 @@ def get_balance_chart_data(transactions):
30
30
 
31
31
  Returns
32
32
  -------
33
- chart_data : list
34
- A list containing (x, y) pairs, each consisting of the Unix
33
+ chart_data : dict
34
+ A dictionary containing a Chartist compatible data structure,
35
+ including (x, y) pairs that each represent the Unix
35
36
  timestamp (in milliseconds) and the bank account balance.
36
37
  """
37
- return list(_BalanceChartData(transactions))
38
+ return _BalanceChartData(transactions).data
38
39
 
39
40
 
40
- class _BalanceChartData(UserList):
41
+ class _BalanceChartData(UserDict):
41
42
  """
42
- A list of balances to be passed to a `chartist.js` chart constructor.
43
+ A mapping of balances to be passed to a `chartist.js` chart constructor.
43
44
 
44
- A special list-like object containing transaction data formatted for
45
- use in a balance chart created by the `chartist.js` library. This
46
- converts each transaction into an (x, y) pair consisting of a Unix
47
- timestamp (in milleseconds) and a corresponding bank account
48
- balance. For transactions occurring on the same day (the finest
49
- granularity recorded by the Monopyly app), a slight offset is
50
- added to each timestamp to guarantee a smooth representation in the
51
- rendered chart.
45
+ A special dictionary-like object containing transaction data
46
+ formatted for use in a balance chart created by the `chartist.js`
47
+ library. This converts each transaction into an (x, y) pair
48
+ consisting of a Unix timestamp (in milleseconds) and a corresponding
49
+ bank account balance. For transactions occurring on the same day
50
+ (the finest granularity recorded by the Monopyly app), a slight
51
+ offset is added to each timestamp to guarantee a smooth
52
+ representation in the rendered chart.
52
53
 
53
54
  Parameters
54
55
  ----------
@@ -61,9 +62,9 @@ class _BalanceChartData(UserList):
61
62
  point = namedtuple("DataPoint", ["timestamp", "balance"])
62
63
 
63
64
  def __init__(self, transactions):
64
- super().__init__()
65
65
  transaction_groups = self._group_transactions_by_date(transactions)
66
- self._prepare_chart_data(transaction_groups)
66
+ chart_data = self._prepare_chart_data(transaction_groups)
67
+ super().__init__({"series": [{"name": "balances", "data": chart_data}]})
67
68
 
68
69
  @staticmethod
69
70
  def _group_transactions_by_date(transactions):
@@ -75,6 +76,7 @@ class _BalanceChartData(UserList):
75
76
 
76
77
  def _prepare_chart_data(self, transaction_groups):
77
78
  # Assign chart data to the list as tuples, adding offsets for duplicated dates
79
+ chart_data = []
78
80
  for transaction_date, transaction_group in transaction_groups.items():
79
81
  base_timestamp = convert_date_to_midnight_timestamp(
80
82
  transaction_date, milliseconds=True
@@ -82,4 +84,5 @@ class _BalanceChartData(UserList):
82
84
  offset = self._DAILY_MILLISECONDS / len(transaction_group)
83
85
  for i, transaction in enumerate(transaction_group):
84
86
  adjusted_timestamp = base_timestamp + (i * offset)
85
- self.data.append((adjusted_timestamp, transaction.balance))
87
+ chart_data.append({"x": adjusted_timestamp, "y": transaction.balance})
88
+ return chart_data
monopyly/banking/banks.py CHANGED
@@ -2,7 +2,7 @@
2
2
  Tools for interacting with banks in the database.
3
3
  """
4
4
 
5
- from authanor.database.handler import DatabaseHandler
5
+ from dry_foundation.database.handler import DatabaseHandler
6
6
 
7
7
  from ..database.models import Bank
8
8
 
@@ -9,3 +9,5 @@ bp = Blueprint("banking", __name__, url_prefix="/banking")
9
9
 
10
10
  # Import routes after defining blueprint to avoid circular imports
11
11
  from . import filters, routes
12
+
13
+ __all__ = ["filters", "routes"]
@@ -6,14 +6,14 @@ from .blueprint import bp
6
6
 
7
7
 
8
8
  @bp.app_template_filter("is_single_bank_transfer")
9
- def check_transfer_is_within_bank(transaction):
10
- """Check if the transfer is linked ot another transaction at the same bank."""
11
- if transaction.internal_transaction:
12
- linked_bank_transactions = transaction.internal_transaction.bank_transactions
9
+ def check_transfer_is_within_bank(transaction_view):
10
+ """Check if the transfer is linked to another transaction at the same bank."""
11
+ if internal_transaction := transaction_view.internal_transaction:
12
+ linked_bank_transactions = internal_transaction.bank_transaction_views
13
13
  if len(linked_bank_transactions) > 1:
14
- common_bank_id = linked_bank_transactions[0].account.bank_id
14
+ common_bank_id = linked_bank_transactions[0].account_view.bank_id
15
15
  return all(
16
- transaction.account.bank_id == common_bank_id
16
+ transaction.account_view.bank_id == common_bank_id
17
17
  for transaction in linked_bank_transactions
18
18
  )
19
19
  return False
monopyly/banking/forms.py CHANGED
@@ -2,18 +2,16 @@
2
2
  Generate banking forms for the user to complete.
3
3
  """
4
4
 
5
- from wtforms.fields import BooleanField, FieldList, FormField, StringField, SubmitField
5
+ from wtforms.fields import BooleanField, FieldList, FormField, SubmitField
6
6
  from wtforms.validators import DataRequired, Optional
7
7
 
8
8
  from ..common.forms import AcquisitionSubform, EntryForm, EntrySubform, TransactionForm
9
9
  from ..common.forms.fields import (
10
10
  CustomChoiceSelectField,
11
- DateField,
12
11
  LastFourDigitsField,
13
12
  StringField,
14
13
  )
15
14
  from ..common.forms.utils import Autocompleter
16
- from ..common.utils import parse_date
17
15
  from ..database.models import (
18
16
  Bank,
19
17
  BankAccountTypeView,
@@ -179,7 +177,7 @@ class BankTransactionForm(TransactionForm):
179
177
  data = {
180
178
  "bank_name": entry.bank.bank_name,
181
179
  "last_four_digits": entry.last_four_digits,
182
- "type_name": entry.account_type.type_name,
180
+ "type_name": entry.account_type_view.type_name,
183
181
  }
184
182
  elif isinstance(entry, Bank):
185
183
  data = {"bank_name": entry.bank_name}
@@ -278,7 +276,7 @@ class BankTransactionForm(TransactionForm):
278
276
  """Gather data for the form from the given database entry."""
279
277
  if isinstance(entry, BankTransactionView):
280
278
  data = self._gather_transaction_data(entry)
281
- account_info = entry.account
279
+ account_info = entry.account_view
282
280
  # Do not prepopulate any transfer information
283
281
  elif isinstance(entry, (BankAccountView, Bank)):
284
282
  data = {}
@@ -2,8 +2,8 @@
2
2
  Routes for banking financials.
3
3
  """
4
4
 
5
- from flask import jsonify, redirect, render_template, request, url_for
6
- from fuisce.database import db_transaction
5
+ from dry_foundation.database import db_transaction
6
+ from flask import g, jsonify, redirect, render_template, request, url_for
7
7
 
8
8
  from ..auth.tools import login_required
9
9
  from ..common.forms.utils import extend_field_list_for_ajax
@@ -13,7 +13,10 @@ from .actions import get_balance_chart_data, get_bank_account_type_grouping
13
13
  from .banks import BankHandler
14
14
  from .blueprint import bp
15
15
  from .forms import BankAccountForm, BankTransactionForm
16
- from .transactions import BankTransactionHandler, save_transaction
16
+ from .transactions import BankTagHandler, BankTransactionHandler, save_transaction
17
+
18
+ # Set a limit on the number of transactions loaded at one time for certain routes
19
+ TRANSACTION_LIMIT = 100
17
20
 
18
21
 
19
22
  @bp.route("/accounts")
@@ -34,7 +37,7 @@ def add_account(bank_id):
34
37
  form = BankAccountForm()
35
38
  # Check if an account was submitted and add it to the database
36
39
  if request.method == "POST":
37
- account = save_account(form)
40
+ save_account(form)
38
41
  return redirect(url_for("banking.load_accounts"))
39
42
  else:
40
43
  if bank_id:
@@ -69,21 +72,39 @@ def load_account_summaries(bank_id):
69
72
  @login_required
70
73
  def load_account_details(account_id):
71
74
  account = BankAccountHandler.get_entry(account_id)
72
- transactions = list(
73
- BankTransactionHandler.get_transactions(
74
- account_ids=(account_id,), sort_order="DESC"
75
- )
76
- )
75
+ transactions = BankTransactionHandler.get_transactions(
76
+ account_ids=(account_id,), sort_order="DESC"
77
+ ).all()
77
78
  # Only display the first 100 transactions
78
79
  return render_template(
79
80
  "banking/account_page.html",
80
81
  account=account,
81
- account_transactions=transactions[:100],
82
+ transactions=transactions[:100],
83
+ total_transactions=len(transactions),
82
84
  # Reverse the chart transactions to be chronologically ascending
83
85
  chart_data=get_balance_chart_data(reversed(transactions)),
84
86
  )
85
87
 
86
88
 
89
+ @bp.route("/_extra_transactions", methods=("POST",))
90
+ @login_required
91
+ def load_more_transactions():
92
+ # Get info about the transactions being displayed from the AJAX request
93
+ post_args = request.get_json()
94
+ account_id = post_args["account_id"]
95
+ block_index = post_args["block_count"] - 1
96
+ # Get a subset of the remaining transactions to load
97
+ more_transactions = BankTransactionHandler.get_transactions(
98
+ account_ids=(account_id,),
99
+ offset=block_index * TRANSACTION_LIMIT,
100
+ limit=TRANSACTION_LIMIT,
101
+ )
102
+ return render_template(
103
+ "banking/transactions_table/transactions.html",
104
+ transactions=more_transactions,
105
+ )
106
+
107
+
87
108
  @bp.route("/_expand_transaction", methods=("POST",))
88
109
  @login_required
89
110
  def expand_transaction():
@@ -216,6 +237,47 @@ def delete_transaction(transaction_id):
216
237
  return redirect(url_for("banking.load_account_details", account_id=account_id))
217
238
 
218
239
 
240
+ @bp.route("/tags")
241
+ @login_required
242
+ def load_tags():
243
+ # Get the tag hierarchy from the database
244
+ hierarchy = BankTagHandler.get_hierarchy()
245
+ return render_template("common/tags_page.html", tags_hierarchy=hierarchy)
246
+
247
+
248
+ @bp.route("/_add_tag", methods=("POST",))
249
+ @login_required
250
+ @db_transaction
251
+ def add_tag():
252
+ # Get the new tag (and potentially parent category) from the AJAX request
253
+ post_args = request.get_json()
254
+ tag_name = post_args["tag_name"]
255
+ parent_name = post_args.get("parent")
256
+ # Check that the tag name does not already exist
257
+ if BankTagHandler.get_tags(tag_names=(tag_name,)):
258
+ raise ValueError("The given tag name already exists. Tag names must be unique.")
259
+ parent_id = BankTagHandler.find_tag(parent_name).id if parent_name else None
260
+ tag = BankTagHandler.add_entry(
261
+ parent_id=parent_id,
262
+ user_id=g.user.id,
263
+ tag_name=tag_name,
264
+ )
265
+ return render_template("common/tag_tree.html", tags_hierarchy={tag: []})
266
+
267
+
268
+ @bp.route("/_delete_tag", methods=("POST",))
269
+ @login_required
270
+ @db_transaction
271
+ def delete_tag():
272
+ # Get the tag to be deleted from the AJAX request
273
+ post_args = request.get_json()
274
+ tag_name = post_args["tag_name"]
275
+ tag = BankTagHandler.find_tag(tag_name)
276
+ # Remove the tag from the database
277
+ BankTagHandler.delete_entry(tag.id)
278
+ return ""
279
+
280
+
219
281
  @bp.route("/_suggest_transaction_autocomplete", methods=("POST",))
220
282
  @login_required
221
283
  def suggest_transaction_autocomplete():
@@ -2,13 +2,12 @@
2
2
  Tools for interacting with the bank transactions in the database.
3
3
  """
4
4
 
5
- from authanor.database.handler import DatabaseViewHandler
5
+ from dry_foundation.database.handler import DatabaseViewHandler
6
6
 
7
7
  from ..common.forms.utils import execute_on_form_validation
8
8
  from ..common.transactions import TransactionHandler, TransactionTagHandler
9
9
  from ..core.internal_transactions import add_internal_transaction
10
10
  from ..database.models import (
11
- Bank,
12
11
  BankAccountView,
13
12
  BankSubtransaction,
14
13
  BankTransaction,
@@ -36,7 +35,9 @@ class BankTransactionHandler(
36
35
 
37
36
  @classmethod
38
37
  @DatabaseViewHandler.view_query
39
- def get_transactions(cls, account_ids=None, active=None, sort_order="DESC"):
38
+ def get_transactions(
39
+ cls, account_ids=None, active=None, sort_order="DESC", offset=None, limit=None
40
+ ):
40
41
  """
41
42
  Get bank transactions from the database.
42
43
 
@@ -61,6 +62,13 @@ class BankTransactionHandler(
61
62
  An indicator of whether the transactions should be ordered
62
63
  in ascending (oldest at top) or descending (newest at top)
63
64
  order. The default is descending order.
65
+ offset : int, optional
66
+ The number of transactions by which to offset the results
67
+ returned by this query. The default is `None`, in which case
68
+ no offset will be added.
69
+ limit : int, optional
70
+ A limit on the number of transactions retrieved from the
71
+ database.
64
72
 
65
73
  Returns
66
74
  -------
@@ -71,7 +79,7 @@ class BankTransactionHandler(
71
79
  criteria.add_match_filter(cls.model, "account_id", account_ids)
72
80
  criteria.add_match_filter(BankAccountView, "active", active)
73
81
  transactions = super()._get_transactions(
74
- criteria=criteria, sort_order=sort_order
82
+ criteria=criteria, sort_order=sort_order, offset=offset, limit=limit
75
83
  )
76
84
  return transactions
77
85
 
@@ -150,7 +158,7 @@ class BankTagHandler(TransactionTagHandler, model=TransactionTagHandler.model):
150
158
  return tags
151
159
 
152
160
  @classmethod
153
- def _filter_entries(cls, query, criteria):
161
+ def _filter_entries(cls, query, criteria, offset, limit):
154
162
  # Add a join to enable filtering by transaction ID or subtransaction ID
155
163
  join_transaction = BankTransactionView in criteria.discriminators
156
164
  join_subtransaction = (
@@ -160,7 +168,7 @@ class BankTagHandler(TransactionTagHandler, model=TransactionTagHandler.model):
160
168
  query = query.join(bank_tag_link_table).join(BankSubtransaction)
161
169
  if join_transaction:
162
170
  query = query.join(BankTransactionView)
163
- return super()._filter_entries(query, criteria)
171
+ return super()._filter_entries(query, criteria, offset, limit)
164
172
 
165
173
 
166
174
  @execute_on_form_validation
@@ -205,7 +213,7 @@ def save_transaction(form, transaction_id=None):
205
213
  transfer = record_new_transfer(transfer_data)
206
214
  transaction_data.update(
207
215
  internal_transaction_id=transfer.internal_transaction_id,
208
- merchant=transfer.account.bank.bank_name,
216
+ merchant=transfer.account_view.bank.bank_name,
209
217
  )
210
218
  transaction = BankTransactionHandler.add_entry(**transaction_data)
211
219
  return transaction
@@ -5,3 +5,11 @@ from ._forms import (
5
5
  TransactionForm,
6
6
  form_err_msg,
7
7
  )
8
+
9
+ __all__ = [
10
+ "AcquisitionSubform",
11
+ "EntryForm",
12
+ "EntrySubform",
13
+ "TransactionForm",
14
+ "form_err_msg",
15
+ ]
@@ -6,11 +6,10 @@ from abc import ABC, abstractmethod
6
6
  from datetime import date
7
7
 
8
8
  from flask_wtf import FlaskForm
9
- from wtforms.fields import FieldList, FormField, SelectField, StringField, SubmitField
9
+ from wtforms.fields import StringField, SubmitField
10
10
  from wtforms.validators import DataRequired
11
11
 
12
12
  from .fields import CurrencyField, DateField
13
- from .validators import SelectionNotBlank
14
13
 
15
14
  # Define a custom form error messaage
16
15
  form_err_msg = "There was an improper value in your form. Please try again."
@@ -4,12 +4,10 @@ General form constructions.
4
4
 
5
5
  from abc import ABC, abstractmethod
6
6
 
7
- from flask_wtf import FlaskForm
8
7
  from wtforms import fields as wtforms_fields
9
8
  from wtforms.validators import Length
10
9
  from wtforms.widgets import NumberInput
11
10
 
12
- from ...banking.banks import BankHandler
13
11
  from ..utils import parse_date
14
12
  from .validators import NumeralsOnly, SelectionNotBlank
15
13
 
@@ -75,7 +75,7 @@ class Autocompleter:
75
75
  # Get information from the database to use for autocompletion
76
76
  query = model.select_for_user(getattr(model, field))
77
77
  values = current_app.db.session.scalars(query)
78
- suggestions = sort_by_frequency([value for value in values])
78
+ suggestions = sort_by_frequency(list(values))
79
79
  return suggestions
80
80
 
81
81
  def _sort_suggestions_by_field(