Merge branch 'dev'
This commit is contained in:
commit
3cd2c7e551
10 changed files with 300 additions and 159 deletions
19
.vscode/launch.json
vendored
Normal file
19
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Python Debugger",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "main.py",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"PRIVATECOFFEE_DEV": "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -136,7 +136,11 @@ h5 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive Styles */
|
/* Responsive Styles */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 991px) {
|
||||||
|
p.text-center.special-header {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar .container {
|
.navbar .container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -177,29 +181,85 @@ h5 {
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.btn-primary {
|
|
||||||
margin: 1rem auto;
|
|
||||||
display: block;
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 .special-header {
|
|
||||||
font-size: 6rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.special-header {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-btn {
|
.navbar-btn {
|
||||||
margin: 0 auto !important;
|
margin: 0 auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.that-br {
|
@media (max-width: 768px) {
|
||||||
display: none;
|
.btn.btn-primary {
|
||||||
}
|
margin: 1rem auto;
|
||||||
|
display: block;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
.slogan {
|
h2 .special-header {
|
||||||
display: none;
|
font-size: 6rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.special-header {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.that-br {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slogan {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown Styles */
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
min-width: 100%;
|
||||||
|
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content a {
|
||||||
|
color: black;
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown:hover .dropdown-content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-toggle-area {
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:has(.dropdown-toggle-area) {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary .main-link {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary .main-link:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
}
|
}
|
|
@ -4,15 +4,14 @@
|
||||||
"Membership Fees": {
|
"Membership Fees": {
|
||||||
"EUR": 365
|
"EUR": 365
|
||||||
},
|
},
|
||||||
"Donations": {},
|
|
||||||
"Server Costs": {
|
"Server Costs": {
|
||||||
"EUR": -216.57
|
"EUR": -216.57
|
||||||
},
|
},
|
||||||
"Domain Names": {},
|
"Domain Names": {},
|
||||||
"Operating Expenses": {
|
"Administrative Expenses": {
|
||||||
"EUR": -36.10
|
"EUR": -36.10,
|
||||||
},
|
"Notes": "Administrative fee for the formation of the association"
|
||||||
"Conversions": {}
|
}
|
||||||
},
|
},
|
||||||
"5": {
|
"5": {
|
||||||
"Membership Fees": {
|
"Membership Fees": {
|
||||||
|
@ -23,11 +22,20 @@
|
||||||
"XMR": 0.447661805527
|
"XMR": 0.447661805527
|
||||||
},
|
},
|
||||||
"Server Costs": {
|
"Server Costs": {
|
||||||
"EUR": -430.04
|
"EUR": -430.04,
|
||||||
|
"Notes": "Includes setup costs and two monthly payments for new server"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"Membership Fees": {
|
||||||
|
"EUR": 382.42
|
||||||
},
|
},
|
||||||
"Domain Names": {},
|
"Server Costs": {
|
||||||
"Operating Expenses": {},
|
"EUR": -317.62
|
||||||
"Conversions": {}
|
},
|
||||||
|
"Bank Fees": {
|
||||||
|
"EUR": -49.05
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,12 +4,18 @@
|
||||||
"name": "Matrix",
|
"name": "Matrix",
|
||||||
"url": "https://element.private.coffee",
|
"url": "https://element.private.coffee",
|
||||||
"short_description": "Matrix is an open network for secure, decentralized communication.",
|
"short_description": "Matrix is an open network for secure, decentralized communication.",
|
||||||
"long_description": "Private.coffee runs a Matrix server. You can use it to chat with other people at Private.coffee or around the world. Use it with a client of your choice, with https://matrix.private.coffee as the homeserver, or use our web client.",
|
"long_description": "Private.coffee runs a Matrix server. You can use it to chat with other people at Private.coffee or around the world. Use it with a client of your choice, or use our Element web client.",
|
||||||
"status": "OK",
|
"status": "OK",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"name": "Go to Element (Web client)",
|
"name": "Go to Element",
|
||||||
"url": "https://element.private.coffee"
|
"url": "https://element.private.coffee",
|
||||||
|
"alternatives": [
|
||||||
|
{
|
||||||
|
"name": "Tor",
|
||||||
|
"url": "http://element.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "matrix-logo",
|
"icon": "matrix-logo",
|
||||||
|
@ -33,15 +39,25 @@
|
||||||
"exclude_from_simple": false
|
"exclude_from_simple": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Piped",
|
"name": "Piped / Invidious",
|
||||||
"url": "https://piped.private.coffee",
|
"url": "https://piped.private.coffee",
|
||||||
"short_description": "Watch YouTube videos without Google tracking.",
|
"short_description": "Watch YouTube videos without Google tracking.",
|
||||||
"long_description": "Piped is another alternative front-end to YouTube. It allows you to watch YouTube videos without Google tracking you.",
|
"long_description": "Piped and Invidious are alternative front-ends to YouTube. They allow you to watch YouTube videos without Google tracking you.",
|
||||||
"status": "OK",
|
"status": "OK",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"name": "Go to Piped",
|
"name": "Go to Piped",
|
||||||
"url": "https://piped.private.coffee"
|
"url": "https://piped.private.coffee"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Go to Invidious",
|
||||||
|
"url": "https://invidious.private.coffee",
|
||||||
|
"alternatives": [
|
||||||
|
{
|
||||||
|
"name": "Tor",
|
||||||
|
"url": "http://invidious.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "video",
|
"icon": "video",
|
||||||
|
@ -80,22 +96,6 @@
|
||||||
"exclude_from_index": false,
|
"exclude_from_index": false,
|
||||||
"exclude_from_simple": false
|
"exclude_from_simple": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Invidious",
|
|
||||||
"url": "https://invidious.private.coffee",
|
|
||||||
"short_description": "Watch YouTube videos without Google tracking.",
|
|
||||||
"long_description": "Invidious is an alternative front-end to YouTube. It allows you to watch YouTube videos without Google tracking you.",
|
|
||||||
"status": "OK",
|
|
||||||
"links": [
|
|
||||||
{
|
|
||||||
"name": "Go to Invidious",
|
|
||||||
"url": "https://invidious.private.coffee"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"icon": "video",
|
|
||||||
"exclude_from_index": false,
|
|
||||||
"exclude_from_simple": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Mastodon",
|
"name": "Mastodon",
|
||||||
"url": "https://cuddly.space",
|
"url": "https://cuddly.space",
|
||||||
|
|
|
@ -1,14 +1,27 @@
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
def get_latest_month(data):
|
def get_latest_month(data, allow_current=False):
|
||||||
years = sorted(data.keys())
|
years = sorted(data.keys())
|
||||||
latest_year = years[-1]
|
latest_year = years[-1]
|
||||||
months = sorted(data[latest_year].keys())
|
months = sorted(data[latest_year].keys())
|
||||||
latest_month = months[-1]
|
latest_month = months[-1]
|
||||||
|
|
||||||
|
if (
|
||||||
|
not allow_current
|
||||||
|
and latest_year == str(datetime.now().year)
|
||||||
|
and latest_month == str(datetime.now().month)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
latest_month = months[-2]
|
||||||
|
except IndexError:
|
||||||
|
latest_year = years[-2]
|
||||||
|
latest_month = months[-1]
|
||||||
|
|
||||||
return int(latest_month), int(latest_year)
|
return int(latest_month), int(latest_year)
|
||||||
|
|
||||||
|
|
||||||
def get_transparency_data(data, year=None, month=None):
|
def get_transparency_data(data, year=None, month=None):
|
||||||
if year is None:
|
if year is None:
|
||||||
year = max(data.keys())
|
year = max(data.keys())
|
||||||
|
@ -23,6 +36,7 @@ def get_transparency_data(data, year=None, month=None):
|
||||||
balances = {}
|
balances = {}
|
||||||
incomes = {}
|
incomes = {}
|
||||||
expenses = {}
|
expenses = {}
|
||||||
|
notes = {}
|
||||||
|
|
||||||
start_balance = {}
|
start_balance = {}
|
||||||
end_balance = {}
|
end_balance = {}
|
||||||
|
@ -37,41 +51,51 @@ def get_transparency_data(data, year=None, month=None):
|
||||||
|
|
||||||
# If the month is the one we are interested in, capture the start balance
|
# If the month is the one we are interested in, capture the start balance
|
||||||
if int(y) == int(year) and int(m) == int(month):
|
if int(y) == int(year) and int(m) == int(month):
|
||||||
start_balance = {k: Decimal(v) for k, v in balances.items()}
|
start_balance = {
|
||||||
|
k: Decimal(v) for k, v in balances.items() if k != "Notes"
|
||||||
|
}
|
||||||
|
|
||||||
for category in data[y][m]:
|
for category in data[y][m]:
|
||||||
for currency, amount in data[y][m][category].items():
|
for currency, amount in data[y][m][category].items():
|
||||||
if currency not in balances:
|
if currency == "Notes":
|
||||||
balances[currency] = Decimal(0)
|
if int(y) == int(year) and int(m) == int(month):
|
||||||
balances[currency] += Decimal(str(amount))
|
notes[category] = amount
|
||||||
|
else:
|
||||||
|
if currency not in balances:
|
||||||
|
balances[currency] = Decimal(0)
|
||||||
|
balances[currency] += Decimal(str(amount))
|
||||||
|
|
||||||
# Track incomes and expenses
|
# Track incomes and expenses
|
||||||
if int(y) == int(year) and int(m) == int(month):
|
if int(y) == int(year) and int(m) == int(month):
|
||||||
if Decimal(str(amount)) > 0:
|
if Decimal(str(amount)) > 0:
|
||||||
if category not in incomes:
|
if category not in incomes:
|
||||||
incomes[category] = {}
|
incomes[category] = {}
|
||||||
if currency not in incomes[category]:
|
if currency not in incomes[category]:
|
||||||
incomes[category][currency] = Decimal(0)
|
incomes[category][currency] = Decimal(0)
|
||||||
incomes[category][currency] += Decimal(str(amount))
|
incomes[category][currency] += Decimal(str(amount))
|
||||||
else:
|
else:
|
||||||
if category not in expenses:
|
if category not in expenses:
|
||||||
expenses[category] = {}
|
expenses[category] = {}
|
||||||
if currency not in expenses[category]:
|
if currency not in expenses[category]:
|
||||||
expenses[category][currency] = Decimal(0)
|
expenses[category][currency] = Decimal(0)
|
||||||
expenses[category][currency] += Decimal(str(amount))
|
expenses[category][currency] += Decimal(str(amount))
|
||||||
|
|
||||||
# If the month is the one we are interested in, capture the end balance
|
# If the month is the one we are interested in, capture the end balance
|
||||||
if int(y) == int(year) and int(m) == int(month):
|
if int(y) == int(year) and int(m) == int(month):
|
||||||
end_balance = {k: Decimal(v) for k, v in balances.items()}
|
end_balance = {
|
||||||
|
k: Decimal(v) for k, v in balances.items() if k != "Notes"
|
||||||
|
}
|
||||||
|
|
||||||
# Calculate accumulated sums of incomes and expenses
|
# Calculate accumulated sums of incomes and expenses
|
||||||
accumulated_incomes = {
|
accumulated_incomes = {
|
||||||
currency: sum(incomes[cat].get(currency, Decimal(0)) for cat in incomes)
|
currency: sum(incomes[cat].get(currency, Decimal(0)) for cat in incomes)
|
||||||
for currency in balances
|
for currency in balances
|
||||||
|
if currency != "Notes"
|
||||||
}
|
}
|
||||||
accumulated_expenses = {
|
accumulated_expenses = {
|
||||||
currency: sum(expenses[cat].get(currency, Decimal(0)) for cat in expenses)
|
currency: sum(expenses[cat].get(currency, Decimal(0)) for cat in expenses)
|
||||||
for currency in balances
|
for currency in balances
|
||||||
|
if currency != "Notes"
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -81,6 +105,7 @@ def get_transparency_data(data, year=None, month=None):
|
||||||
"expenses": expenses,
|
"expenses": expenses,
|
||||||
"accumulated_incomes": accumulated_incomes,
|
"accumulated_incomes": accumulated_incomes,
|
||||||
"accumulated_expenses": accumulated_expenses,
|
"accumulated_expenses": accumulated_expenses,
|
||||||
|
"notes": notes,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -94,7 +119,7 @@ def generate_transparency_table(result, currencies=None):
|
||||||
+ list(data["accumulated_incomes"].keys())
|
+ list(data["accumulated_incomes"].keys())
|
||||||
+ list(data["accumulated_expenses"].keys())
|
+ list(data["accumulated_expenses"].keys())
|
||||||
)
|
)
|
||||||
- {"EUR"}
|
- {"EUR", "Notes"}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -152,7 +177,8 @@ def generate_transparency_table(result, currencies=None):
|
||||||
|
|
||||||
# Add income rows
|
# Add income rows
|
||||||
for category, transactions in result["incomes"].items():
|
for category, transactions in result["incomes"].items():
|
||||||
html += f"<tr><td>{category}</td>"
|
has_notes = result["notes"].get(category)
|
||||||
|
html += f"<tr><td>{category}{'*' if has_notes else ''}</td>"
|
||||||
for currency in currencies:
|
for currency in currencies:
|
||||||
value = transactions.get(currency, "")
|
value = transactions.get(currency, "")
|
||||||
if value != "":
|
if value != "":
|
||||||
|
@ -163,7 +189,8 @@ def generate_transparency_table(result, currencies=None):
|
||||||
|
|
||||||
# Add expense rows
|
# Add expense rows
|
||||||
for category, transactions in result["expenses"].items():
|
for category, transactions in result["expenses"].items():
|
||||||
html += f"<tr><td>{category}</td>"
|
has_notes = result["notes"].get(category)
|
||||||
|
html += f"<tr><td>{category}{'*' if has_notes else ''}</td>"
|
||||||
for currency in currencies:
|
for currency in currencies:
|
||||||
value = transactions.get(currency, "")
|
value = transactions.get(currency, "")
|
||||||
if value != "":
|
if value != "":
|
||||||
|
@ -198,4 +225,11 @@ def generate_transparency_table(result, currencies=None):
|
||||||
</table>
|
</table>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if result["notes"]:
|
||||||
|
html += "<p><b>Notes:</b></p>"
|
||||||
|
html += "<ul>"
|
||||||
|
for category, footnote in result["notes"].items():
|
||||||
|
html += f"<li>{category}: {footnote}</li>"
|
||||||
|
html += "</ul>"
|
||||||
|
|
||||||
return html
|
return html
|
||||||
|
|
45
main.py
45
main.py
|
@ -54,7 +54,9 @@ def catch_all(path):
|
||||||
(pathlib.Path(__file__).parent / "data" / "finances.json").read_text()
|
(pathlib.Path(__file__).parent / "data" / "finances.json").read_text()
|
||||||
)
|
)
|
||||||
|
|
||||||
finances_month, finances_year = get_latest_month(finances)
|
allow_current = app.development_mode
|
||||||
|
|
||||||
|
finances_month, finances_year = get_latest_month(finances, allow_current)
|
||||||
finances_period = datetime.date(finances_year, finances_month, 1)
|
finances_period = datetime.date(finances_year, finances_month, 1)
|
||||||
finances_period_str = finances_period.strftime("%B %Y")
|
finances_period_str = finances_period.strftime("%B %Y")
|
||||||
|
|
||||||
|
@ -69,12 +71,53 @@ def catch_all(path):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if path == "transparency":
|
||||||
|
finances = json.loads(
|
||||||
|
(pathlib.Path(__file__).parent / "data" / "finances.json").read_text()
|
||||||
|
)
|
||||||
|
|
||||||
|
finance_data = {}
|
||||||
|
|
||||||
|
for year in sorted(finances.keys(), reverse=True):
|
||||||
|
for month in sorted(finances[year].keys(), reverse=True):
|
||||||
|
if year not in finance_data:
|
||||||
|
finance_data[year] = {}
|
||||||
|
print(get_transparency_data(finances, year, month))
|
||||||
|
finance_data[year][month] = generate_transparency_table(
|
||||||
|
get_transparency_data(finances, year, month)
|
||||||
|
)
|
||||||
|
|
||||||
|
kwargs.update(
|
||||||
|
{
|
||||||
|
"finances": finance_data,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return render_template(f"{path}.html", **kwargs)
|
return render_template(f"{path}.html", **kwargs)
|
||||||
|
|
||||||
except TemplateNotFound:
|
except TemplateNotFound:
|
||||||
return "404 Not Found", 404
|
return "404 Not Found", 404
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/_metrics/")
|
||||||
|
def metrics():
|
||||||
|
finances = json.loads(
|
||||||
|
(pathlib.Path(__file__).parent / "data" / "finances.json").read_text()
|
||||||
|
)
|
||||||
|
|
||||||
|
balances = get_transparency_data(finances)["end_balance"]
|
||||||
|
|
||||||
|
response = (
|
||||||
|
"# HELP privatecoffee_balance The balance of the private.coffee account\n"
|
||||||
|
)
|
||||||
|
response += "# TYPE privatecoffee_balance gauge\n"
|
||||||
|
|
||||||
|
for currency, balance in balances.items():
|
||||||
|
response += f'privatecoffee_balance{{currency="{currency}"}} {balance}\n'
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
app.development_mode = False
|
app.development_mode = False
|
||||||
|
|
||||||
if os.environ.get("PRIVATECOFFEE_DEV"):
|
if os.environ.get("PRIVATECOFFEE_DEV"):
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<nav
|
<nav
|
||||||
class="navbar navbar-expand-md sticky-top navbar-shrink py-3 navbar-light"
|
class="navbar navbar-expand-md py-3 navbar-light"
|
||||||
id="mainNav"
|
id="mainNav"
|
||||||
>
|
>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -87,6 +87,11 @@
|
||||||
donations are being used.
|
donations are being used.
|
||||||
</p>
|
</p>
|
||||||
<div class="table-responsive">{{ finances|safe }}</div>
|
<div class="table-responsive">{{ finances|safe }}</div>
|
||||||
|
<p class="card-text">
|
||||||
|
Want to know how we got here? Check out all of our
|
||||||
|
<a href="/transparency.html">transparency reports</a> for more
|
||||||
|
information.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
25
templates/transparency.html
Normal file
25
templates/transparency.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{% extends "base.html" %} {% block title %}Membership / Donations{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container my-5">
|
||||||
|
<div class="text-center mb-5">
|
||||||
|
<h1 class="special-header fancy-text-primary">Transparency</h1>
|
||||||
|
<p class="lead">
|
||||||
|
Private.coffee is funded by its members and donations. We believe in
|
||||||
|
transparency and accountability. Below you can find financial reports for
|
||||||
|
each month since our inception.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for year, year_data in finances.items() %}
|
||||||
|
{% for month, month_data in year_data.items() %}
|
||||||
|
<div class="card shadow-sm mt-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Transparency Report for {{ month }}/{{ year }}</h5>
|
||||||
|
<div class="table-responsive">{{ month_data|safe }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Loading…
Reference in a new issue