Merge branch 'dev'

This commit is contained in:
Kumi 2024-06-30 19:18:53 +02:00
commit 3cd2c7e551
Signed by: kumi
GPG key ID: ECBCC9082395383F
10 changed files with 300 additions and 159 deletions

19
.vscode/launch.json vendored Normal file
View 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"
}
}
]
}

View file

@ -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,6 +181,11 @@ h5 {
padding: 0 1rem;
}
.navbar-btn {
margin: 0 auto !important;
}
@media (max-width: 768px) {
.btn.btn-primary {
margin: 1rem auto;
display: block;
@ -191,10 +200,6 @@ h5 {
font-size: 2rem;
}
.navbar-btn {
margin: 0 auto !important;
}
.that-br {
display: none;
}
@ -202,4 +207,59 @@ h5 {
.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;
}

View file

@ -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"
}
},
"Domain Names": {},
"Operating Expenses": {},
"Conversions": {}
"6": {
"Membership Fees": {
"EUR": 382.42
},
"Server Costs": {
"EUR": -317.62
},
"Bank Fees": {
"EUR": -49.05
}
}
}
}

View file

@ -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",

View file

@ -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,10 +51,16 @@ 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 == "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))
@ -62,16 +82,20 @@ def get_transparency_data(data, year=None, month=None):
# 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
View file

@ -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"):

View file

@ -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

View file

@ -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>

View 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 %}