Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
abe90d3483 | |||
65e7dcdf6d | |||
74e414badf | |||
69175979ff | |||
2f69e0f215 | |||
961385c389 | |||
a691cfd2da | |||
482a0faa23 | |||
0ecf7b6617 | |||
4408bbfc82 | |||
433110f486 | |||
0b1dd4f4fc | |||
46bd27e126 | |||
b718d19450 | |||
2969e83afe | |||
a732656746 | |||
1daed940b6 |
@ -2,6 +2,39 @@ Change Log
|
|||||||
==========
|
==========
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.4
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/5/18
|
||||||
|
|
||||||
|
Security fixes.
|
||||||
|
|
||||||
|
* Added safeguard to the next URI utilities, to prevent Cross-Site
|
||||||
|
Scripting (XSS) attacks.
|
||||||
|
* Applied the safe next URI utilities to the test site.
|
||||||
|
* Added the ``SameSite`` and ``Secure`` flags to the session cookie
|
||||||
|
of the test site.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.3
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/30
|
||||||
|
|
||||||
|
* Fixed the error of the net balance in the unmatched offset list.
|
||||||
|
* Revised the original line item editor not to override the existing
|
||||||
|
amount when the existing amount is less or equal to the net
|
||||||
|
balance.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.2
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/30
|
||||||
|
|
||||||
|
* Fixed the error of the net balance in the unmatched offset list.
|
||||||
|
|
||||||
|
|
||||||
Version 1.5.1
|
Version 1.5.1
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
@ -50,9 +50,9 @@ The following front-end JavaScript libraries must be loaded. You may
|
|||||||
download it locally or use CDN_.
|
download it locally or use CDN_.
|
||||||
|
|
||||||
* Bootstrap_ 5.2.3 or above
|
* Bootstrap_ 5.2.3 or above
|
||||||
* FontAwesome_ 6.2.1 or above
|
* FontAwesome_ 6.4.0 or above
|
||||||
* `Decimal.js`_ 6.4.3 or above
|
* `decimal.js`_ 10.4.3 or above, or `decimal.js-light`_ 2.5.1 or above.
|
||||||
* `Tempus-Dominus`_ 6.4.3 or above
|
* `Tempus-Dominus`_ 6.7.7 or above
|
||||||
|
|
||||||
|
|
||||||
Configuration
|
Configuration
|
||||||
@ -114,6 +114,7 @@ Check your Flask application and see how it works.
|
|||||||
.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network
|
.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network
|
||||||
.. _Bootstrap: https://getbootstrap.com
|
.. _Bootstrap: https://getbootstrap.com
|
||||||
.. _FontAwesome: https://fontawesome.com
|
.. _FontAwesome: https://fontawesome.com
|
||||||
.. _Decimal.js: https://mikemcl.github.io/decimal.js
|
.. _decimal.js: https://mikemcl.github.io/decimal.js
|
||||||
|
.. _decimal.js-light: https://mikemcl.github.io/decimal.js-light
|
||||||
.. _Tempus-Dominus: https://getdatepicker.com
|
.. _Tempus-Dominus: https://getdatepicker.com
|
||||||
.. _Bootstrap navigation bar: https://getbootstrap.com/docs/5.3/components/navbar/
|
.. _Bootstrap navigation bar: https://getbootstrap.com/docs/5.3/components/navbar/
|
||||||
|
@ -24,7 +24,7 @@ from flask_sqlalchemy import SQLAlchemy
|
|||||||
|
|
||||||
from accounting.utils.user import UserUtilityInterface
|
from accounting.utils.user import UserUtilityInterface
|
||||||
|
|
||||||
VERSION: str = "1.5.1"
|
VERSION: str = "1.5.4"
|
||||||
"""The package version."""
|
"""The package version."""
|
||||||
db: SQLAlchemy = SQLAlchemy()
|
db: SQLAlchemy = SQLAlchemy()
|
||||||
"""The database instance."""
|
"""The database instance."""
|
||||||
|
@ -182,6 +182,8 @@ class Account(db.Model):
|
|||||||
:param value: The new title.
|
:param value: The new title.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
|
if self.title == value:
|
||||||
|
return
|
||||||
if self.title_l10n is None:
|
if self.title_l10n is None:
|
||||||
self.title_l10n = value
|
self.title_l10n = value
|
||||||
return
|
return
|
||||||
@ -424,6 +426,8 @@ class Currency(db.Model):
|
|||||||
:param value: The new name.
|
:param value: The new name.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
|
if self.name == value:
|
||||||
|
return
|
||||||
if self.name_l10n is None:
|
if self.name_l10n is None:
|
||||||
self.name_l10n = value
|
self.name_l10n = value
|
||||||
return
|
return
|
||||||
|
@ -123,15 +123,13 @@ class OffsetMatcher:
|
|||||||
.options(selectinload(JournalEntryLineItem.currency),
|
.options(selectinload(JournalEntryLineItem.currency),
|
||||||
selectinload(JournalEntryLineItem.journal_entry)).all()
|
selectinload(JournalEntryLineItem.journal_entry)).all()
|
||||||
for line_item in self.line_items:
|
for line_item in self.line_items:
|
||||||
line_item.is_offset = line_item.id in net_balances
|
line_item.is_offset = line_item.id not in net_balances
|
||||||
self.unapplied = [x for x in self.line_items
|
self.unapplied = [x for x in self.line_items if not x.is_offset]
|
||||||
if x.is_offset]
|
|
||||||
for line_item in self.unapplied:
|
for line_item in self.unapplied:
|
||||||
line_item.net_balance = line_item.amount \
|
line_item.net_balance = line_item.amount \
|
||||||
if net_balances[line_item.id] is None \
|
if net_balances[line_item.id] is None \
|
||||||
else net_balances[line_item.id]
|
else net_balances[line_item.id]
|
||||||
self.unmatched = [x for x in self.line_items
|
self.unmatched = [x for x in self.line_items if x.is_offset]
|
||||||
if not x.is_offset]
|
|
||||||
self.__populate_accumulated_balances()
|
self.__populate_accumulated_balances()
|
||||||
|
|
||||||
def __populate_accumulated_balances(self) -> None:
|
def __populate_accumulated_balances(self) -> None:
|
||||||
|
@ -290,7 +290,9 @@ class JournalEntryLineItemEditor {
|
|||||||
this.account = originalLineItem.account.copy();
|
this.account = originalLineItem.account.copy();
|
||||||
this.isAccountConfirmed = false;
|
this.isAccountConfirmed = false;
|
||||||
this.#accountText.innerText = this.account.text;
|
this.#accountText.innerText = this.account.text;
|
||||||
this.#amountInput.value = String(originalLineItem.netBalance);
|
if (this.#amountInput.value === "" || new Decimal(this.#amountInput.value).greaterThan(originalLineItem.netBalance)) {
|
||||||
|
this.#amountInput.value = String(originalLineItem.netBalance);
|
||||||
|
}
|
||||||
this.#amountInput.max = String(originalLineItem.netBalance);
|
this.#amountInput.max = String(originalLineItem.netBalance);
|
||||||
this.#amountInput.min = "0";
|
this.#amountInput.min = "0";
|
||||||
this.#validate();
|
this.#validate();
|
||||||
|
@ -32,7 +32,7 @@ First written: 2023/1/30
|
|||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
<form class="btn btn-primary d-flex input-group" name="accounting-search-form" action="{{ url_for("accounting.account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
||||||
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||||
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
|
@ -26,7 +26,7 @@ First written: 2023/1/26
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="mb-2 accounting-toolbar">
|
<div class="mb-2 accounting-toolbar">
|
||||||
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.base-account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
<form class="btn btn-primary d-flex input-group" name="accounting-search-form" action="{{ url_for("accounting.base-account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
||||||
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||||
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
|
@ -32,7 +32,7 @@ First written: 2023/2/6
|
|||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.currency.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
<form class="btn btn-primary d-flex input-group" name="accounting-search-form" action="{{ url_for("accounting.currency.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
||||||
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||||
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
|
@ -19,7 +19,7 @@ description-editor-modal.html: The modal of the description editor
|
|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
First written: 2023/2/28
|
First written: 2023/2/28
|
||||||
#}
|
#}
|
||||||
<form id="accounting-description-editor-{{ description_editor.debit_credit }}" class="accounting-description-editor" data-debit-credit="{{ description_editor.debit_credit }}">
|
<form id="accounting-description-editor-{{ description_editor.debit_credit }}" class="accounting-description-editor" name="accounting-dummy-form" data-debit-credit="{{ description_editor.debit_credit }}">
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-modal-label" aria-hidden="true">
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-modal-label" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
@ -19,7 +19,7 @@ journal-entry-line-item-editor-modal: The modal of the journal entry line item e
|
|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
First written: 2023/2/25
|
First written: 2023/2/25
|
||||||
#}
|
#}
|
||||||
<form id="accounting-line-item-editor">
|
<form id="accounting-line-item-editor" name="accounting-dummy-form">
|
||||||
<div id="accounting-line-item-editor-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-line-item-editor-modal-label" aria-hidden="true">
|
<div id="accounting-line-item-editor-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-line-item-editor-modal-label" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
@ -19,7 +19,7 @@ recurring-item-editor-modal.html: The modal of the recurring item editor
|
|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
First written: 2023/3/22
|
First written: 2023/3/22
|
||||||
#}
|
#}
|
||||||
<form id="accounting-recurring-item-editor-{{ expense_income }}">
|
<form id="accounting-recurring-item-editor-{{ expense_income }}" name="accounting-dummy-form">
|
||||||
<div id="accounting-recurring-item-editor-{{ expense_income }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-recurring-item-editor-{{ expense_income }}-modal-label" aria-hidden="true">
|
<div id="accounting-recurring-item-editor-{{ expense_income }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-recurring-item-editor-{{ expense_income }}-modal-label" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
@ -19,7 +19,7 @@ search-modal.html: The search modal
|
|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
First written: 2023/3/8
|
First written: 2023/3/8
|
||||||
#}
|
#}
|
||||||
<form action="{{ url_for("accounting-report.search") }}" method="get" role="search" aria-labelledby="accounting-search-modal-label">
|
<form action="{{ url_for("accounting-report.search") }}" name="accounting-search-form" method="get" role="search" aria-labelledby="accounting-search-modal-label">
|
||||||
<div class="modal fade" id="accounting-search-modal" tabindex="-1" aria-labelledby="accounting-search-modal-label" aria-hidden="true">
|
<div class="modal fade" id="accounting-search-modal" tabindex="-1" aria-labelledby="accounting-search-modal-label" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
@ -118,7 +118,7 @@ First written: 2023/3/8
|
|||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if use_search %}
|
{% if use_search %}
|
||||||
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting-report.search") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
<form class="btn btn-primary d-flex input-group" name="accounting-search-form" action="{{ url_for("accounting-report.search") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
||||||
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||||
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
|
@ -14,8 +14,7 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
"""The utility to cast a SQLAlchemy column into the column type, to avoid
|
"""The utilities to cast values into desired types, to avoid IDE warnings.
|
||||||
warnings from the IDE.
|
|
||||||
|
|
||||||
This module should not import any other module from the application.
|
This module should not import any other module from the application.
|
||||||
|
|
||||||
|
@ -21,7 +21,6 @@ from typing import Self
|
|||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from accounting import db
|
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
from accounting.models import Account
|
from accounting.models import Account
|
||||||
|
|
||||||
@ -75,7 +74,7 @@ class CurrentAccount:
|
|||||||
"""
|
"""
|
||||||
accounts: list[cls] = [cls.current_assets_and_liabilities()]
|
accounts: list[cls] = [cls.current_assets_and_liabilities()]
|
||||||
accounts.extend([CurrentAccount(x)
|
accounts.extend([CurrentAccount(x)
|
||||||
for x in db.session.query(Account)
|
for x in Account.query
|
||||||
.filter(cls.sql_condition())
|
.filter(cls.sql_condition())
|
||||||
.order_by(Account.base_code, Account.no)])
|
.order_by(Account.base_code, Account.no)])
|
||||||
return accounts
|
return accounts
|
||||||
|
@ -41,11 +41,8 @@ def inherit_next(uri: str) -> str:
|
|||||||
:param uri: The URI.
|
:param uri: The URI.
|
||||||
:return: The URI with the current next URI added at the query argument.
|
:return: The URI with the current next URI added at the query argument.
|
||||||
"""
|
"""
|
||||||
next_uri: str | None = request.form.get("next") \
|
next_uri: str | None = __get_next_uri()
|
||||||
if request.method == "POST" else request.args.get("next")
|
return uri if next_uri is None else __set_next(uri, next_uri)
|
||||||
if next_uri is None:
|
|
||||||
return uri
|
|
||||||
return __set_next(uri, next_uri)
|
|
||||||
|
|
||||||
|
|
||||||
def or_next(uri: str) -> str:
|
def or_next(uri: str) -> str:
|
||||||
@ -54,9 +51,22 @@ def or_next(uri: str) -> str:
|
|||||||
:param uri: The URI.
|
:param uri: The URI.
|
||||||
:return: The next URI or the supplied URI.
|
:return: The next URI or the supplied URI.
|
||||||
"""
|
"""
|
||||||
|
next_uri: str | None = __get_next_uri()
|
||||||
|
return uri if next_uri is None else next_uri
|
||||||
|
|
||||||
|
|
||||||
|
def __get_next_uri() -> str | None:
|
||||||
|
"""Returns the valid next URI.
|
||||||
|
|
||||||
|
:return: The valid next URI.
|
||||||
|
"""
|
||||||
next_uri: str | None = request.form.get("next") \
|
next_uri: str | None = request.form.get("next") \
|
||||||
if request.method == "POST" else request.args.get("next")
|
if request.method == "POST" else request.args.get("next")
|
||||||
return uri if next_uri is None else next_uri
|
if next_uri is None or not next_uri.startswith("/"):
|
||||||
|
return None
|
||||||
|
if len(next_uri) > 512:
|
||||||
|
return next_uri[:512]
|
||||||
|
return next_uri
|
||||||
|
|
||||||
|
|
||||||
def __set_next(uri: str, next_uri: str) -> str:
|
def __set_next(uri: str, next_uri: str) -> str:
|
||||||
|
@ -2153,7 +2153,7 @@ class JournalEntryReorderTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(db.session.get(JournalEntry, id_1).no, 1)
|
self.assertEqual(db.session.get(JournalEntry, id_1).no, 1)
|
||||||
self.assertEqual(db.session.get(JournalEntry, id_2).no, 3)
|
self.assertEqual(db.session.get(JournalEntry, id_2).no, 3)
|
||||||
self.assertEqual(db.session.get(JournalEntry, id_3).no, 2)
|
self.assertEqual(db.session.get(JournalEntry, id_3).no, 2)
|
||||||
self.assertEqual(db.session.get(JournalEntry, id_4).no, 1)
|
self.assertEqual( db.session.get(JournalEntry, id_4).no, 1)
|
||||||
self.assertEqual(db.session.get(JournalEntry, id_5).no, 2)
|
self.assertEqual(db.session.get(JournalEntry, id_5).no, 2)
|
||||||
|
|
||||||
def test_reorder(self) -> None:
|
def test_reorder(self) -> None:
|
||||||
@ -2181,14 +2181,14 @@ class JournalEntryReorderTestCase(unittest.TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
f"{PREFIX}/dates/{date.isoformat()}",
|
f"{PREFIX}/dates/{date.isoformat()}",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.csrf_token,
|
||||||
"next": "/next",
|
"next": NEXT_URI,
|
||||||
f"{id_1}-no": "4",
|
f"{id_1}-no": "4",
|
||||||
f"{id_2}-no": "1",
|
f"{id_2}-no": "1",
|
||||||
f"{id_3}-no": "5",
|
f"{id_3}-no": "5",
|
||||||
f"{id_4}-no": "2",
|
f"{id_4}-no": "2",
|
||||||
f"{id_5}-no": "3"})
|
f"{id_5}-no": "3"})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], f"/next")
|
self.assertEqual(response.headers["Location"], NEXT_URI)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.app.app_context():
|
||||||
self.assertEqual(db.session.get(JournalEntry, id_1).no, 4)
|
self.assertEqual(db.session.get(JournalEntry, id_1).no, 4)
|
||||||
@ -2209,12 +2209,12 @@ class JournalEntryReorderTestCase(unittest.TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
f"{PREFIX}/dates/{date.isoformat()}",
|
f"{PREFIX}/dates/{date.isoformat()}",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.csrf_token,
|
||||||
"next": "/next",
|
"next": NEXT_URI,
|
||||||
f"{id_2}-no": "3a",
|
f"{id_2}-no": "3a",
|
||||||
f"{id_3}-no": "5",
|
f"{id_3}-no": "5",
|
||||||
f"{id_4}-no": "2"})
|
f"{id_4}-no": "2"})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], f"/next")
|
self.assertEqual(response.headers["Location"], NEXT_URI)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.app.app_context():
|
||||||
self.assertEqual(db.session.get(JournalEntry, id_1).no, 3)
|
self.assertEqual(db.session.get(JournalEntry, id_1).no, 3)
|
||||||
|
@ -52,6 +52,8 @@ def create_app(is_testing: bool = False) -> Flask:
|
|||||||
db_uri: str = "sqlite:///" if is_testing else "sqlite:///local.sqlite"
|
db_uri: str = "sqlite:///" if is_testing else "sqlite:///local.sqlite"
|
||||||
app.config.from_mapping({
|
app.config.from_mapping({
|
||||||
"SECRET_KEY": os.environ.get("SECRET_KEY", token_urlsafe(32)),
|
"SECRET_KEY": os.environ.get("SECRET_KEY", token_urlsafe(32)),
|
||||||
|
"SESSION_COOKIE_SAMESITE": "Lax",
|
||||||
|
"SESSION_COOKIE_SECURE": True,
|
||||||
"SQLALCHEMY_DATABASE_URI": db_uri,
|
"SQLALCHEMY_DATABASE_URI": db_uri,
|
||||||
"BABEL_DEFAULT_LOCALE": "en",
|
"BABEL_DEFAULT_LOCALE": "en",
|
||||||
"ALL_LINGUAS": "zh_Hant|正體中文,en|English,zh_Hans|简体中文",
|
"ALL_LINGUAS": "zh_Hant|正體中文,en|English,zh_Hans|简体中文",
|
||||||
|
@ -19,10 +19,12 @@
|
|||||||
"""
|
"""
|
||||||
from babel import Locale
|
from babel import Locale
|
||||||
from flask import request, session, current_app, Blueprint, Response, \
|
from flask import request, session, current_app, Blueprint, Response, \
|
||||||
redirect, url_for, Flask
|
redirect, Flask
|
||||||
from flask_babel import Babel
|
from flask_babel import Babel
|
||||||
from werkzeug.datastructures import LanguageAccept
|
from werkzeug.datastructures import LanguageAccept
|
||||||
|
|
||||||
|
from accounting.utils.next_uri import or_next
|
||||||
|
|
||||||
bp: Blueprint = Blueprint("locale", __name__, url_prefix="/")
|
bp: Blueprint = Blueprint("locale", __name__, url_prefix="/")
|
||||||
|
|
||||||
|
|
||||||
@ -68,9 +70,7 @@ def set_locale() -> Response:
|
|||||||
all_linguas: dict[str, str] = get_all_linguas()
|
all_linguas: dict[str, str] = get_all_linguas()
|
||||||
if "locale" in request.form and request.form["locale"] in all_linguas:
|
if "locale" in request.form and request.form["locale"] in all_linguas:
|
||||||
session["locale"] = request.form["locale"]
|
session["locale"] = request.form["locale"]
|
||||||
if "next" in request.form:
|
return redirect(or_next("/"))
|
||||||
return redirect(request.form["next"])
|
|
||||||
return redirect(url_for("home.home"))
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_linguas() -> dict[str, str]:
|
def get_all_linguas() -> dict[str, str]:
|
||||||
|
@ -26,13 +26,13 @@ First written: 2023/1/27
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="author" content="{{ "imacat" }}" />
|
<meta name="author" content="{{ "imacat" }}" />
|
||||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" crossorigin="anonymous">
|
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" crossorigin="anonymous">
|
||||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.2.1/css/all.min.css" crossorigin="anonymous">
|
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css" crossorigin="anonymous">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@eonasdan/tempus-dominus@6.4.3/dist/css/tempus-dominus.min.css" crossorigin="anonymous">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@eonasdan/tempus-dominus@6.7.7/dist/css/tempus-dominus.min.css" crossorigin="anonymous">
|
||||||
{% block styles %}{% endblock %}
|
{% block styles %}{% endblock %}
|
||||||
<script src="{{ url_for("babel_catalog") }}"></script>
|
<script src="{{ url_for("babel_catalog") }}"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/decimal.js-light@2.5.1/decimal.min.js" integrity="sha384-QdsxGEq4Y0erX8WUIsZJDtfoSSyBF6dmNCnzRNYCa2AOM/xzNsyhHu0RbdFBAm+l" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/decimal.js-light@2.5.1/decimal.min.js" integrity="sha384-QdsxGEq4Y0erX8WUIsZJDtfoSSyBF6dmNCnzRNYCa2AOM/xzNsyhHu0RbdFBAm+l" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@eonasdan/tempus-dominus@6.4.3/dist/js/tempus-dominus.min.js" integrity="sha384-2MkID2vkc9sxBCqs2us3mB8fV+c0o7uPtOvAPjaC8gKv9Bk21UHT0r2Q7Kv70+zO" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@eonasdan/tempus-dominus@6.7.7/dist/js/tempus-dominus.min.js" integrity="sha384-MxHp+/TqTjbku1jSTIe1e/4l6CZTLhACLDbWyxYaFRgD3AM4oh99AY8bxsGhIoRc" crossorigin="anonymous"></script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
<link rel="shortcut icon" href="{{ url_for("static", filename="favicon.svg") }}">
|
<link rel="shortcut icon" href="{{ url_for("static", filename="favicon.svg") }}">
|
||||||
<title>{% block title %}{% endblock %}</title>
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
@ -101,6 +101,60 @@ class NextUriTestCase(unittest.TestCase):
|
|||||||
"name": "viewer"})
|
"name": "viewer"})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_invalid(self) -> None:
|
||||||
|
"""Tests the next URI utilities without an invalid next URI.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
def test_invalid_next_uri_view() -> str:
|
||||||
|
"""The test view without the next URI."""
|
||||||
|
self.assertEqual(inherit_next(self.TARGET),
|
||||||
|
request.args.get("inherit-expected"))
|
||||||
|
self.assertEqual(or_next(self.TARGET),
|
||||||
|
request.args.get("or-expected"))
|
||||||
|
return ""
|
||||||
|
|
||||||
|
self.app.add_url_rule("/test-invalid-next",
|
||||||
|
view_func=test_invalid_next_uri_view,
|
||||||
|
methods=["GET", "POST"])
|
||||||
|
client: httpx.Client = httpx.Client(app=self.app, base_url=TEST_SERVER)
|
||||||
|
client.headers["Referer"] = TEST_SERVER
|
||||||
|
csrf_token: str = get_csrf_token(client)
|
||||||
|
next_uri: str
|
||||||
|
expected1: str
|
||||||
|
expected2: str
|
||||||
|
response: httpx.Response
|
||||||
|
|
||||||
|
# A foreign URI
|
||||||
|
next_uri = "https://example.com"
|
||||||
|
expected1 = self.TARGET
|
||||||
|
expected2 = self.TARGET
|
||||||
|
response = client.get(f"/test-invalid-next?next={quote_plus(next_uri)}"
|
||||||
|
f"&inherit-expected={quote_plus(expected1)}"
|
||||||
|
f"&or-expected={quote_plus(expected2)}")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = client.post("/test-invalid-next"
|
||||||
|
f"?inherit-expected={quote_plus(expected1)}"
|
||||||
|
f"&or-expected={quote_plus(expected2)}",
|
||||||
|
data={"csrf_token": csrf_token,
|
||||||
|
"next": next_uri})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# An extremely-long URI to trigger the error
|
||||||
|
next_uri = "/" + "x" * 1024
|
||||||
|
expected2 = next_uri[:512]
|
||||||
|
expected1 = f"{self.TARGET}?next={quote_plus(expected2)}"
|
||||||
|
response = client.get(f"/test-invalid-next?next={quote_plus(next_uri)}"
|
||||||
|
f"&inherit-expected={quote_plus(expected1)}"
|
||||||
|
f"&or-expected={quote_plus(expected2)}")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = client.post("/test-invalid-next"
|
||||||
|
f"?inherit-expected={quote_plus(expected1)}"
|
||||||
|
f"&or-expected={quote_plus(expected2)}",
|
||||||
|
data={"csrf_token": csrf_token,
|
||||||
|
"next": next_uri})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
class QueryKeywordParserTestCase(unittest.TestCase):
|
class QueryKeywordParserTestCase(unittest.TestCase):
|
||||||
"""The test case for the query keyword parser."""
|
"""The test case for the query keyword parser."""
|
||||||
|
Reference in New Issue
Block a user