monopyly 1.4.6__py3-none-any.whl → 1.4.8__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 (51) hide show
  1. monopyly/CHANGELOG.md +195 -0
  2. monopyly/README.md +14 -3
  3. monopyly/__init__.py +20 -1
  4. monopyly/_version.py +2 -2
  5. monopyly/auth/actions.py +7 -2
  6. monopyly/banking/accounts.py +1 -1
  7. monopyly/banking/actions.py +51 -10
  8. monopyly/banking/routes.py +2 -1
  9. monopyly/cli/apps.py +51 -28
  10. monopyly/cli/{run.py → launch.py} +18 -10
  11. monopyly/common/forms/_forms.py +4 -1
  12. monopyly/core/actions.py +108 -21
  13. monopyly/core/blueprint.py +1 -1
  14. monopyly/core/context_processors.py +10 -0
  15. monopyly/core/errors.py +9 -0
  16. monopyly/core/filters.py +4 -2
  17. monopyly/core/routes.py +22 -9
  18. monopyly/credit/routes.py +9 -1
  19. monopyly/credit/transactions/_transactions.py +1 -1
  20. monopyly/static/css/style.css +166 -73
  21. monopyly/static/js/make-payment.js +4 -2
  22. monopyly/templates/banking/account_summaries.html +26 -12
  23. monopyly/templates/banking/account_summaries_page.html +2 -1
  24. monopyly/templates/banking/account_summary.html +7 -2
  25. monopyly/templates/banking/accounts_page.html +2 -2
  26. monopyly/templates/core/credits.html +32 -0
  27. monopyly/templates/core/errors/400.html +8 -0
  28. monopyly/templates/core/errors/401.html +8 -0
  29. monopyly/templates/core/errors/403.html +8 -0
  30. monopyly/templates/core/errors/404.html +8 -0
  31. monopyly/templates/core/errors/405.html +8 -0
  32. monopyly/templates/core/errors/408.html +8 -0
  33. monopyly/templates/core/errors/418.html +8 -0
  34. monopyly/templates/core/errors/425.html +8 -0
  35. monopyly/templates/core/errors/500.html +8 -0
  36. monopyly/templates/core/errors/error.html +31 -0
  37. monopyly/templates/{profile.html → core/profile.html} +3 -1
  38. monopyly/templates/core/story.html +62 -0
  39. monopyly/templates/credit/statement_summary.html +7 -2
  40. monopyly/templates/credit/statements.html +7 -6
  41. monopyly/templates/layout.html +11 -2
  42. {monopyly-1.4.6.dist-info → monopyly-1.4.8.dist-info}/METADATA +15 -4
  43. {monopyly-1.4.6.dist-info → monopyly-1.4.8.dist-info}/RECORD +48 -36
  44. monopyly-1.4.8.dist-info/entry_points.txt +2 -0
  45. monopyly/templates/credits.html +0 -20
  46. monopyly/templates/story.html +0 -47
  47. monopyly-1.4.6.dist-info/entry_points.txt +0 -2
  48. /monopyly/templates/{index.html → core/index.html} +0 -0
  49. {monopyly-1.4.6.dist-info → monopyly-1.4.8.dist-info}/WHEEL +0 -0
  50. {monopyly-1.4.6.dist-info → monopyly-1.4.8.dist-info}/licenses/COPYING +0 -0
  51. {monopyly-1.4.6.dist-info → monopyly-1.4.8.dist-info}/licenses/LICENSE +0 -0
monopyly/core/actions.py CHANGED
@@ -10,25 +10,112 @@ def get_timestamp():
10
10
  return datetime.now().strftime("%Y%m%d_%H%M%S")
11
11
 
12
12
 
13
- def format_readme_as_html_template(readme_text):
14
- """Given the README text in Markdown, convert it to a renderable HTML template."""
15
- # Convert Markdown to HTML
16
- raw_html_readme = markdown.markdown(readme_text, extensions=["fenced_code"])
17
- # Replace README relative links with app relevant links
18
- html_readme = raw_html_readme.replace('src="monopyly/static', 'src="/static')
19
- # Format the HTML as a valid Jinja template
20
- html_readme_template = (
21
- '{% extends "layout.html" %}'
22
- "{% block title %}About{% endblock %}"
23
- "{% block content %}"
24
- ' <div id="readme" class="about">'
25
- f" {html_readme}"
26
- ' <div class="resource-links">'
27
- " <h2>Links</h2>"
28
- ' <p><a href="{{ url_for("core.story") }}">Story</a></p>'
29
- ' <p><a href="{{ url_for("core.credits") }}">Credits</a></p>'
30
- " </div>"
31
- " </div>"
32
- "{% endblock %}"
13
+ class MarkdownConverter:
14
+
15
+ replacements = {
16
+ "src": [
17
+ ["monopyly/static", "/static"],
18
+ ],
19
+ "href": [
20
+ ["README.md", '{{ url_for("core.about") }}'],
21
+ ["CHANGELOG.md", '{{ url_for("core.changelog") }}'],
22
+ ],
23
+ }
24
+
25
+ @classmethod
26
+ def convert(cls, markdown_path, title, id_="", class_="", extra_content=""):
27
+ """Given a Markdown file, convert it to a renderable HTML template."""
28
+ raw_markdown = cls._read_markdown(markdown_path)
29
+ html_content = cls._convert_markdown_to_html(raw_markdown)
30
+ return cls._generate_html_template(
31
+ html_content, title, id_=id_, class_=class_, extra_content=extra_content
32
+ )
33
+
34
+ @staticmethod
35
+ def _read_markdown(markdown_path):
36
+ with markdown_path.open(encoding="utf-8") as markdown_file:
37
+ raw_markdown = markdown_file.read()
38
+ return raw_markdown
39
+
40
+ @classmethod
41
+ def _convert_markdown_to_html(cls, raw_markdown):
42
+ raw_html = markdown.markdown(raw_markdown, extensions=["fenced_code"])
43
+ return cls._replace_links(raw_html)
44
+
45
+ @classmethod
46
+ def _replace_links(cls, raw_html):
47
+ html = raw_html
48
+ for tag, pairs in cls.replacements.items():
49
+ for original, replacement in pairs:
50
+ html = html.replace(f'{tag}="{original}', f'{tag}="{replacement}')
51
+ return html
52
+
53
+ @staticmethod
54
+ def _generate_html_template(content, title, id_="", class_="", extra_content=""):
55
+ # Format the HTML as a valid Jinja template
56
+ html_template = (
57
+ '{% extends "layout.html" %}'
58
+ "{% block title %}"
59
+ f" {title}"
60
+ "{% endblock %}"
61
+ "{% block content %}"
62
+ f' <div id="{id_}" class="{class_}">'
63
+ f" {content}"
64
+ f" {extra_content}"
65
+ "{% endblock %}"
66
+ )
67
+ return html_template
68
+
69
+
70
+ def convert_readme_to_html_template(readme_path):
71
+ """Given a README file in Markdown, convert it to a renderable HTML template."""
72
+ return MarkdownConverter.convert(
73
+ readme_path,
74
+ title="About",
75
+ id_="readme",
76
+ class_="about",
77
+ extra_content=(
78
+ '<div class="resource-links">'
79
+ " <h2>Links</h2>"
80
+ ' <p><a href="{{ url_for("core.story") }}">Story</a></p>'
81
+ ' <p><a href="{{ url_for("core.credits") }}">Credits</a></p>'
82
+ "</div>"
83
+ ),
84
+ )
85
+
86
+
87
+ def convert_changelog_to_html_template(changelog_path):
88
+ """Given a CHANGELOG file in Markdown, convert it to a renderable HTML template."""
89
+ return MarkdownConverter.convert(
90
+ changelog_path,
91
+ title="Changes",
92
+ id_="changelog",
33
93
  )
34
- return html_readme_template
94
+
95
+
96
+ def determine_summary_balance_svg_viewbox_width(currency_value):
97
+ """
98
+ Determine the width of the SVG viewBox attribute displayed in summary boxes.
99
+
100
+ Parameters
101
+ ----------
102
+ currency_value : str
103
+ A currency value, displayed in the format output by the
104
+ `core.filters.make_currency` filter function.
105
+ """
106
+ # Set the per-character width contributions
107
+ digit_width = 55
108
+ punctuation_width = 25
109
+ spacing_width = 25
110
+ # Count the number of commas and non-comma characters in the non-decimal portion
111
+ nondecimal_value = currency_value.rsplit(".", maxsplit=1)[0]
112
+ comma_count = nondecimal_value.count(",")
113
+ digit_count = len(nondecimal_value) - comma_count
114
+ # Width is the total of the following subcomponents
115
+ svg_currency_width_subcomponents = [
116
+ digit_width * digit_count, # ------------- total width of digits/sign
117
+ punctuation_width * comma_count, # ------- total width of commas
118
+ punctuation_width + (2 * digit_width), # - width of the decimal section
119
+ digit_width + spacing_width, # ----------- width of the dollar sign and spacing
120
+ ]
121
+ return max(400, sum(svg_currency_width_subcomponents))
@@ -8,4 +8,4 @@ from flask import Blueprint
8
8
  bp = Blueprint("core", __name__)
9
9
 
10
10
  # Import routes after defining blueprint to avoid circular imports
11
- from . import context_processors, filters, routes
11
+ from . import context_processors, errors, filters, routes
@@ -5,6 +5,7 @@ Filters defined for the application.
5
5
  from datetime import date
6
6
  from importlib import import_module
7
7
 
8
+ from .actions import determine_summary_balance_svg_viewbox_width
8
9
  from .blueprint import bp
9
10
 
10
11
 
@@ -19,6 +20,15 @@ def inject_global_template_variables():
19
20
  return template_globals
20
21
 
21
22
 
23
+ @bp.app_context_processor
24
+ def inject_utility_functions():
25
+ """Inject utility functions globally into the template context."""
26
+ utility_functions = {
27
+ "calculate_summary_balance_width": determine_summary_balance_svg_viewbox_width,
28
+ }
29
+ return utility_functions
30
+
31
+
22
32
  def _display_version():
23
33
  """Show the version (without commit information)."""
24
34
  try:
@@ -0,0 +1,9 @@
1
+ """
2
+ Tools for handling app errors.
3
+ """
4
+
5
+ from flask import render_template
6
+
7
+
8
+ def render_error_template(exception):
9
+ return render_template(f"core/errors/{exception.code}.html", exception=exception)
monopyly/core/filters.py CHANGED
@@ -2,6 +2,8 @@
2
2
  Filters defined for the application.
3
3
  """
4
4
 
5
+ from math import floor, log10
6
+
5
7
  from .blueprint import bp
6
8
 
7
9
 
@@ -32,8 +34,8 @@ def make_ordinal(integer):
32
34
 
33
35
  Notes
34
36
  -----
35
- This function is an adaptation of the one proposed by Stack Overflow user
36
- Florian Brucker (https://stackoverflow.com/a/50992575/8754471).
37
+ This function is an adaptation of the one proposed by Stack Overflow
38
+ user Florian Brucker (https://stackoverflow.com/a/50992575/8754471).
37
39
 
38
40
  Parameters
39
41
  ----------
monopyly/core/routes.py CHANGED
@@ -5,15 +5,18 @@ Routes for core functionality.
5
5
  from pathlib import Path
6
6
 
7
7
  from flask import g, render_template, render_template_string, session
8
+ from werkzeug.exceptions import abort
8
9
 
9
10
  from ..auth.tools import login_required
10
11
  from ..banking.accounts import BankAccountHandler
11
12
  from ..banking.banks import BankHandler
12
13
  from ..credit.cards import CreditCardHandler
13
14
  from ..credit.statements import CreditStatementHandler
14
- from .actions import format_readme_as_html_template
15
+ from .actions import convert_changelog_to_html_template, convert_readme_to_html_template
15
16
  from .blueprint import bp
16
17
 
18
+ APP_ROOT_DIR = Path(__file__).parents[1]
19
+
17
20
 
18
21
  @bp.route("/")
19
22
  def index():
@@ -40,7 +43,7 @@ def index():
40
43
  session["show_homepage_block"] = True
41
44
  bank_accounts, active_cards = None, None
42
45
  return render_template(
43
- "index.html", bank_accounts=bank_accounts, cards=active_cards
46
+ "core/index.html", bank_accounts=bank_accounts, cards=active_cards
44
47
  )
45
48
 
46
49
 
@@ -53,22 +56,27 @@ def hide_homepage_block():
53
56
 
54
57
  @bp.route("/about")
55
58
  def about():
56
- readme_path = Path(__file__).parents[1] / "README.md"
57
- with readme_path.open(encoding="utf-8") as readme_file:
58
- raw_readme_text = readme_file.read()
59
- about_page_template = format_readme_as_html_template(raw_readme_text)
59
+ readme_path = APP_ROOT_DIR / "README.md"
60
+ about_page_template = convert_readme_to_html_template(readme_path)
60
61
  return render_template_string(about_page_template)
61
62
 
62
63
 
64
+ @bp.route("/changelog")
65
+ def changelog():
66
+ changelog_path = APP_ROOT_DIR / "CHANGELOG.md"
67
+ changelog_page_template = convert_changelog_to_html_template(changelog_path)
68
+ return render_template_string(changelog_page_template)
69
+
70
+
63
71
  @bp.route("/story")
64
72
  @login_required
65
73
  def story():
66
- return render_template("story.html")
74
+ return render_template("core/story.html")
67
75
 
68
76
 
69
77
  @bp.route("/credits")
70
78
  def credits():
71
- return render_template("credits.html")
79
+ return render_template("core/credits.html")
72
80
 
73
81
 
74
82
  @bp.route("/profile")
@@ -76,4 +84,9 @@ def credits():
76
84
  def load_profile():
77
85
  banks = BankHandler.get_banks()
78
86
  # Return banks as a list to allow multiple reuse
79
- return render_template("profile.html", banks=list(banks))
87
+ return render_template("core/profile.html", banks=list(banks))
88
+
89
+
90
+ @bp.route("/change_password")
91
+ def change_password():
92
+ abort(418)
monopyly/credit/routes.py CHANGED
@@ -216,12 +216,20 @@ def pay_credit_card(card_id, statement_id):
216
216
  make_payment(card_id, payment_account_id, payment_date, payment_amount)
217
217
  # Get the current statement information from the database
218
218
  statement = CreditStatementHandler.get_entry(statement_id)
219
+ transactions = CreditTransactionHandler.get_transactions(
220
+ statement_ids=(statement_id,)
221
+ )
219
222
  bank_accounts = BankAccountHandler.get_accounts()
220
- return render_template(
223
+ summary_template = render_template(
221
224
  "credit/statement_summary.html",
222
225
  statement=statement,
223
226
  bank_accounts=bank_accounts,
224
227
  )
228
+ transactions_table_template = render_template(
229
+ "credit/transactions_table/transactions.html",
230
+ transactions=transactions,
231
+ )
232
+ return jsonify((summary_template, transactions_table_template))
225
233
 
226
234
 
227
235
  @bp.route("/transactions", defaults={"card_id": None})
@@ -52,7 +52,7 @@ class CreditTransactionHandler(
52
52
 
53
53
  Parameters
54
54
  ----------
55
- statement_ids : tuple of str, optional
55
+ statement_ids : tuple of int, optional
56
56
  A sequence of statement IDs with which to filter
57
57
  transactions (if `None`, all statement IDs will be shown).
58
58
  card_ids : tuple of int, optional