Browse Source

Show donation meter on financials page

merge-requests/102/head
Timo 5 years ago
committed by Deimos
parent
commit
f0305bc0cd
  1. 3
      tildes/scss/modules/_donation.scss
  2. 4
      tildes/scss/modules/_sidebar.scss
  3. 6
      tildes/tildes/templates/financials.jinja2
  4. 33
      tildes/tildes/templates/home.jinja2
  5. 34
      tildes/tildes/templates/macros/donation_goal.jinja2
  6. 36
      tildes/tildes/views/financials.py
  7. 33
      tildes/tildes/views/topic.py

3
tildes/scss/modules/_donation.scss

@ -5,8 +5,9 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
max-width: $paragraph-max-width;
padding: 0.4rem 0.4rem 0;
padding: 0.4rem;
margin: 1rem 0; margin: 1rem 0;
border: 1px solid; border: 1px solid;
border-color: inherit; border-color: inherit;

4
tildes/scss/modules/_sidebar.scss

@ -2,10 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
#sidebar { #sidebar {
p {
margin-bottom: 0.4rem;
}
.btn { .btn {
width: 100%; width: 100%;
} }

6
tildes/tildes/templates/financials.jinja2

@ -4,6 +4,7 @@
{% extends 'base_no_sidebar.jinja2' %} {% extends 'base_no_sidebar.jinja2' %}
{% from "macros/utils.jinja2" import format_money %} {% from "macros/utils.jinja2" import format_money %}
{% from "macros/donation_goal.jinja2" import donation_goal %}
{% block title %}Tildes financials{% endblock %} {% block title %}Tildes financials{% endblock %}
@ -12,6 +13,7 @@
{% block main_heading %}Tildes financials{% endblock %} {% block main_heading %}Tildes financials{% endblock %}
{% block content %} {% block content %}
<p>This page is a view into Tildes's financials: operating expenses, income from the various donation methods, and the overall goal for monthly donations. Currently, it only contains data for {{ current_time.strftime("%B %Y") }}, but more historical data will be available eventually.</p> <p>This page is a view into Tildes's financials: operating expenses, income from the various donation methods, and the overall goal for monthly donations. Currently, it only contains data for {{ current_time.strftime("%B %Y") }}, but more historical data will be available eventually.</p>
<p>Amounts on this page are in USD unless otherwise noted. Even though Tildes is a Canadian non-profit, many of its costs and donations are in USD. People from other parts of the world are also generally most familiar with the relative value of USD, so using it makes this info more understandable to everyone.</p> <p>Amounts on this page are in USD unless otherwise noted. Even though Tildes is a Canadian non-profit, many of its costs and donations are in USD. People from other parts of the world are also generally most familiar with the relative value of USD, so using it makes this info more understandable to everyone.</p>
@ -22,6 +24,10 @@
<p><strong>The current donation goal is {{ format_money(entries["goal"]|sum(attribute="amount")) }} per month.</strong></p> <p><strong>The current donation goal is {{ format_money(entries["goal"]|sum(attribute="amount")) }} per month.</strong></p>
{% if financial_data %}
{{ donation_goal(financial_data, current_time) }}
{% endif %}
<p>The actual <em>costs</em> solely to keep Tildes running are much lower than this (see table below), but this represents the amount that I believe will make Tildes truly independently sustainable. It will cover all of the operating costs and also allow me (<a href="/user/Deimos">Deimos</a>) to pay myself a somewhat respectable (but low) salary of about $35,000/year. This goal may not be achievable in the near term, but it is the point where I will be comfortable focusing on Tildes without still needing to find additional outside income.</p> <p>The actual <em>costs</em> solely to keep Tildes running are much lower than this (see table below), but this represents the amount that I believe will make Tildes truly independently sustainable. It will cover all of the operating costs and also allow me (<a href="/user/Deimos">Deimos</a>) to pay myself a somewhat respectable (but low) salary of about $35,000/year. This goal may not be achievable in the near term, but it is the point where I will be comfortable focusing on Tildes without still needing to find additional outside income.</p>
<p><a href="https://docs.tildes.net/donate">Please donate&mdash;any amount will help get us closer to the goal!</a></p> <p><a href="https://docs.tildes.net/donate">Please donate&mdash;any amount will help get us closer to the goal!</a></p>

33
tildes/tildes/templates/home.jinja2

@ -4,6 +4,7 @@
{% extends 'topic_listing.jinja2' %} {% extends 'topic_listing.jinja2' %}
{% from 'macros/forms.jinja2' import search_form %} {% from 'macros/forms.jinja2' import search_form %}
{% from 'macros/donation_goal.jinja2' import donation_goal %}
{% block title_full %}Tildes{% endblock %} {% block title_full %}Tildes{% endblock %}
@ -109,35 +110,3 @@
</ul> </ul>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% macro donation_goal(financial_data, current_time) %}
<div class="donation-goal">
<header>Tildes's progress to sustainability</header>
<div class="donation-goal-progress">
<meter
{% if financial_data["goal_percentage"] <= 100 %}
class="donation-goal-meter"
max="{{ financial_data["goal"] }}"
low="{{ financial_data["expense"] }}"
high="{{ financial_data["goal"] // 4 * 3 }}" {# 3/4 of the goal #}
optimum="{{ financial_data["goal"] - 1 }}" {# just needs to be between high and max #}
value="{{ financial_data["income"] }}"
{% else %}
{# Above 100%, this creates an effect of "extra" bar being added to the end #}
class="donation-goal-meter donation-goal-meter-over-goal"
max="{{ financial_data["income"] }}"
value="{{ financial_data["goal"] }}"
{% endif %}
title="${{ financial_data["income"] }} of ${{ financial_data["goal"] }} goal (USD)"
></meter>
<span class="donation-goal-percentage">{{ financial_data["goal_percentage"] }}%</span>
</div>
<p class="text-secondary">{{ current_time.strftime("%B %Y") }} donations</p>
<p>Tildes is a non-profit site with no ads or investors, funded entirely by donations.</p>
<p><a href="https://docs.tildes.net/donate">Please donate</a> to support its continued development! (<a href="/financials">more details</a>)</p>
</div>
{% endmacro %}

34
tildes/tildes/templates/macros/donation_goal.jinja2

@ -0,0 +1,34 @@
{# Copyright (c) 2020 Tildes contributors <code@tildes.net> #}
{# SPDX-License-Identifier: AGPL-3.0-or-later #}
{% macro donation_goal(financial_data, current_time) %}
<div class="donation-goal">
<header>Tildes's progress to sustainability</header>
<div class="donation-goal-progress">
<meter
{% if financial_data["goal_percentage"] <= 100 %}
class="donation-goal-meter"
max="{{ financial_data["goal"] }}"
low="{{ financial_data["expense"] }}"
high="{{ financial_data["goal"] // 4 * 3 }}" {# 3/4 of the goal #}
optimum="{{ financial_data["goal"] - 1 }}" {# just needs to be between high and max #}
value="{{ financial_data["income"] }}"
{% else %}
{# Above 100%, this creates an effect of "extra" bar being added to the end #}
class="donation-goal-meter donation-goal-meter-over-goal"
max="{{ financial_data["income"] }}"
value="{{ financial_data["goal"] }}"
{% endif %}
title="${{ financial_data["income"] }} of ${{ financial_data["goal"] }} goal (USD)"
></meter>
<span class="donation-goal-percentage">{{ financial_data["goal_percentage"] }}%</span>
</div>
<p class="text-secondary">{{ current_time.strftime("%B %Y") }} donations</p>
<p>Tildes is a non-profit site with no ads or investors, funded entirely by donations.</p>
<p><a href="https://docs.tildes.net/donate">Please donate</a> to support its continued development! (<a href="/financials">more details</a>)</p>
</div>
{% endmacro %}

36
tildes/tildes/views/financials.py

@ -4,10 +4,13 @@
"""The view for displaying entries in the financials table.""" """The view for displaying entries in the financials table."""
from collections import defaultdict from collections import defaultdict
from typing import Dict, List
from decimal import Decimal
from typing import Dict, List, Optional
from pyramid.request import Request from pyramid.request import Request
from pyramid.view import view_config from pyramid.view import view_config
from sqlalchemy import func
from sqlalchemy.orm.session import Session
from sqlalchemy.sql.expression import text from sqlalchemy.sql.expression import text
from tildes.lib.datetime import utc_now from tildes.lib.datetime import utc_now
@ -31,4 +34,33 @@ def get_financials(request: Request) -> dict:
for entry in financial_entries: for entry in financial_entries:
entries[entry.entry_type.name.lower()].append(entry) entries[entry.entry_type.name.lower()].append(entry)
return {"entries": entries, "current_time": utc_now()}
financial_data = get_financial_data(request.db_session)
return {
"entries": entries,
"current_time": utc_now(),
"financial_data": financial_data,
}
def get_financial_data(db_session: Session) -> Optional[Dict[str, Decimal]]:
"""Return financial data used to render the donation goal box."""
# get the total sum for each entry type in the financials table relevant to today
financial_totals = (
db_session.query(Financials.entry_type, func.sum(Financials.amount))
.filter(Financials.date_range.op("@>")(text("CURRENT_DATE")))
.group_by(Financials.entry_type)
.all()
)
financial_data = {entry[0].name.lower(): entry[1] for entry in financial_totals}
# if any of the entry types were missing, the data won't be usable
if any(key not in financial_data for key in ("expense", "goal", "income")):
return None
financial_data["goal_percentage"] = round(
financial_data["income"] / financial_data["goal"] * 100
)
return financial_data

33
tildes/tildes/views/topic.py

@ -4,9 +4,8 @@
"""Views related to posting/viewing topics and comments on them.""" """Views related to posting/viewing topics and comments on them."""
from collections import namedtuple from collections import namedtuple
from decimal import Decimal
from difflib import SequenceMatcher from difflib import SequenceMatcher
from typing import Any, Dict, Optional, Union
from typing import Any, Optional, Union
from marshmallow import missing, ValidationError from marshmallow import missing, ValidationError
from marshmallow.fields import Boolean, String from marshmallow.fields import Boolean, String
@ -15,8 +14,7 @@ from pyramid.renderers import render_to_response
from pyramid.request import Request from pyramid.request import Request
from pyramid.response import Response from pyramid.response import Response
from pyramid.view import view_config from pyramid.view import view_config
from sqlalchemy import cast, func, or_
from sqlalchemy.orm.session import Session
from sqlalchemy import cast, or_
from sqlalchemy.sql.expression import any_, desc, text from sqlalchemy.sql.expression import any_, desc, text
from sqlalchemy_utils import Ltree from sqlalchemy_utils import Ltree
from webargs.pyramidparser import use_kwargs from webargs.pyramidparser import use_kwargs
@ -31,7 +29,6 @@ from tildes.enums import (
from tildes.lib.database import TagList from tildes.lib.database import TagList
from tildes.lib.datetime import SimpleHoursPeriod, utc_now from tildes.lib.datetime import SimpleHoursPeriod, utc_now
from tildes.models.comment import Comment, CommentNotification, CommentTree from tildes.models.comment import Comment, CommentNotification, CommentTree
from tildes.models.financials import Financials
from tildes.models.group import Group, GroupWikiPage from tildes.models.group import Group, GroupWikiPage
from tildes.models.log import LogComment, LogTopic from tildes.models.log import LogComment, LogTopic
from tildes.models.topic import Topic, TopicSchedule, TopicVisit from tildes.models.topic import Topic, TopicSchedule, TopicVisit
@ -41,6 +38,7 @@ from tildes.schemas.fields import Enum, ShortTimePeriod
from tildes.schemas.listing import TopicListingSchema from tildes.schemas.listing import TopicListingSchema
from tildes.schemas.topic import TopicSchema from tildes.schemas.topic import TopicSchema
from tildes.views.decorators import rate_limit_view from tildes.views.decorators import rate_limit_view
from tildes.views.financials import get_financial_data
DefaultSettings = namedtuple("DefaultSettings", ["order", "period"]) DefaultSettings = namedtuple("DefaultSettings", ["order", "period"])
@ -307,7 +305,7 @@ def get_group_topics( # noqa
most_recent_scheduled_topics = None most_recent_scheduled_topics = None
if is_home_page: if is_home_page:
financial_data = _get_financial_data(request.db_session)
financial_data = get_financial_data(request.db_session)
else: else:
financial_data = None financial_data = None
@ -560,26 +558,3 @@ def _get_default_settings(
default_period = None default_period = None
return DefaultSettings(order=default_order, period=default_period) return DefaultSettings(order=default_order, period=default_period)
def _get_financial_data(db_session: Session) -> Optional[Dict[str, Decimal]]:
"""Return financial data used to render the donation goal box."""
# get the total sum for each entry type in the financials table relevant to today
financial_totals = (
db_session.query(Financials.entry_type, func.sum(Financials.amount))
.filter(Financials.date_range.op("@>")(text("CURRENT_DATE")))
.group_by(Financials.entry_type)
.all()
)
financial_data = {entry[0].name.lower(): entry[1] for entry in financial_totals}
# if any of the entry types were missing, the data won't be usable
if any(key not in financial_data for key in ("expense", "goal", "income")):
return None
financial_data["goal_percentage"] = round(
financial_data["income"] / financial_data["goal"] * 100
)
return financial_data
Loading…
Cancel
Save