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 */
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 991px) {
|
||||
p.text-center.special-header {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.navbar .container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -177,29 +181,85 @@ h5 {
|
|||
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 {
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
.that-br {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.btn.btn-primary {
|
||||
margin: 1rem auto;
|
||||
display: block;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.slogan {
|
||||
display: none;
|
||||
h2 .special-header {
|
||||
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": {
|
||||
"EUR": 365
|
||||
},
|
||||
"Donations": {},
|
||||
"Server Costs": {
|
||||
"EUR": -216.57
|
||||
},
|
||||
"Domain Names": {},
|
||||
"Operating Expenses": {
|
||||
"EUR": -36.10
|
||||
},
|
||||
"Conversions": {}
|
||||
"Administrative Expenses": {
|
||||
"EUR": -36.10,
|
||||
"Notes": "Administrative fee for the formation of the association"
|
||||
}
|
||||
},
|
||||
"5": {
|
||||
"Membership Fees": {
|
||||
|
@ -23,11 +22,20 @@
|
|||
"XMR": 0.447661805527
|
||||
},
|
||||
"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": {},
|
||||
"Operating Expenses": {},
|
||||
"Conversions": {}
|
||||
"Server Costs": {
|
||||
"EUR": -317.62
|
||||
},
|
||||
"Bank Fees": {
|
||||
"EUR": -49.05
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,12 +4,18 @@
|
|||
"name": "Matrix",
|
||||
"url": "https://element.private.coffee",
|
||||
"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",
|
||||
"links": [
|
||||
{
|
||||
"name": "Go to Element (Web client)",
|
||||
"url": "https://element.private.coffee"
|
||||
"name": "Go to Element",
|
||||
"url": "https://element.private.coffee",
|
||||
"alternatives": [
|
||||
{
|
||||
"name": "Tor",
|
||||
"url": "http://element.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "matrix-logo",
|
||||
|
@ -33,15 +39,25 @@
|
|||
"exclude_from_simple": false
|
||||
},
|
||||
{
|
||||
"name": "Piped",
|
||||
"name": "Piped / Invidious",
|
||||
"url": "https://piped.private.coffee",
|
||||
"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",
|
||||
"links": [
|
||||
{
|
||||
"name": "Go to Piped",
|
||||
"url": "https://piped.private.coffee"
|
||||
},
|
||||
{
|
||||
"name": "Go to Invidious",
|
||||
"url": "https://invidious.private.coffee",
|
||||
"alternatives": [
|
||||
{
|
||||
"name": "Tor",
|
||||
"url": "http://invidious.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "video",
|
||||
|
@ -80,22 +96,6 @@
|
|||
"exclude_from_index": 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",
|
||||
"url": "https://cuddly.space",
|
||||
|
|
|
@ -1,14 +1,27 @@
|
|||
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())
|
||||
latest_year = years[-1]
|
||||
months = sorted(data[latest_year].keys())
|
||||
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)
|
||||
|
||||
|
||||
def get_transparency_data(data, year=None, month=None):
|
||||
if year is None:
|
||||
year = max(data.keys())
|
||||
|
@ -23,6 +36,7 @@ def get_transparency_data(data, year=None, month=None):
|
|||
balances = {}
|
||||
incomes = {}
|
||||
expenses = {}
|
||||
notes = {}
|
||||
|
||||
start_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 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 currency, amount in data[y][m][category].items():
|
||||
if currency not in balances:
|
||||
balances[currency] = Decimal(0)
|
||||
balances[currency] += Decimal(str(amount))
|
||||
if currency == "Notes":
|
||||
if int(y) == int(year) and int(m) == int(month):
|
||||
notes[category] = amount
|
||||
else:
|
||||
if currency not in balances:
|
||||
balances[currency] = Decimal(0)
|
||||
balances[currency] += Decimal(str(amount))
|
||||
|
||||
# Track incomes and expenses
|
||||
if int(y) == int(year) and int(m) == int(month):
|
||||
if Decimal(str(amount)) > 0:
|
||||
if category not in incomes:
|
||||
incomes[category] = {}
|
||||
if currency not in incomes[category]:
|
||||
incomes[category][currency] = Decimal(0)
|
||||
incomes[category][currency] += Decimal(str(amount))
|
||||
else:
|
||||
if category not in expenses:
|
||||
expenses[category] = {}
|
||||
if currency not in expenses[category]:
|
||||
expenses[category][currency] = Decimal(0)
|
||||
expenses[category][currency] += Decimal(str(amount))
|
||||
# Track incomes and expenses
|
||||
if int(y) == int(year) and int(m) == int(month):
|
||||
if Decimal(str(amount)) > 0:
|
||||
if category not in incomes:
|
||||
incomes[category] = {}
|
||||
if currency not in incomes[category]:
|
||||
incomes[category][currency] = Decimal(0)
|
||||
incomes[category][currency] += Decimal(str(amount))
|
||||
else:
|
||||
if category not in expenses:
|
||||
expenses[category] = {}
|
||||
if currency not in expenses[category]:
|
||||
expenses[category][currency] = Decimal(0)
|
||||
expenses[category][currency] += Decimal(str(amount))
|
||||
|
||||
# If the month is the one we are interested in, capture the end balance
|
||||
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
|
||||
accumulated_incomes = {
|
||||
currency: sum(incomes[cat].get(currency, Decimal(0)) for cat in incomes)
|
||||
for currency in balances
|
||||
if currency != "Notes"
|
||||
}
|
||||
accumulated_expenses = {
|
||||
currency: sum(expenses[cat].get(currency, Decimal(0)) for cat in expenses)
|
||||
for currency in balances
|
||||
if currency != "Notes"
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -81,6 +105,7 @@ def get_transparency_data(data, year=None, month=None):
|
|||
"expenses": expenses,
|
||||
"accumulated_incomes": accumulated_incomes,
|
||||
"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_expenses"].keys())
|
||||
)
|
||||
- {"EUR"}
|
||||
- {"EUR", "Notes"}
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -152,7 +177,8 @@ def generate_transparency_table(result, currencies=None):
|
|||
|
||||
# Add income rows
|
||||
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:
|
||||
value = transactions.get(currency, "")
|
||||
if value != "":
|
||||
|
@ -163,7 +189,8 @@ def generate_transparency_table(result, currencies=None):
|
|||
|
||||
# Add expense rows
|
||||
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:
|
||||
value = transactions.get(currency, "")
|
||||
if value != "":
|
||||
|
@ -198,4 +225,11 @@ def generate_transparency_table(result, currencies=None):
|
|||
</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
|
||||
|
|
45
main.py
45
main.py
|
@ -54,7 +54,9 @@ def catch_all(path):
|
|||
(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_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)
|
||||
|
||||
except TemplateNotFound:
|
||||
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
|
||||
|
||||
if os.environ.get("PRIVATECOFFEE_DEV"):
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
<body>
|
||||
<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"
|
||||
>
|
||||
<div class="container">
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -87,6 +87,11 @@
|
|||
donations are being used.
|
||||
</p>
|
||||
<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>
|
||||
|
||||
|
|
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