diff --git a/js/tail_log.js b/js/tail_log.js index e4d4274d5e..52363aa5b3 100644 --- a/js/tail_log.js +++ b/js/tail_log.js @@ -4,6 +4,7 @@ Liberapay.stream_lines = function(url, data_cb, error_cb) { function fetch_lines(first_pos) { jQuery.ajax({ url: url, + dataType: 'text', headers: {Range: 'x-lines='+first_pos+'-'}, }).done(function(data, textStatus, xhr) { var file_is_partial = false; diff --git a/liberapay/utils/__init__.py b/liberapay/utils/__init__.py index bb54da4bc5..96c5c5470d 100644 --- a/liberapay/utils/__init__.py +++ b/liberapay/utils/__init__.py @@ -640,19 +640,26 @@ def check_address_v2(addr): return True -def render_postal_address(addr, single_line=False): +def render_postal_address(addr, single_line=False, format='local'): if not check_address_v2(addr): return - # FIXME The rendering below is simplistic, we should implement - # https://github.com/liberapay/liberapay.com/issues/1056 - elements = [addr['local_address'], addr['city'], addr['postal_code']] - if addr.get('region'): - elements.append(addr['region']) - elements.append(LOCALE_EN.countries[addr['country']]) - if single_line: - return ', '.join(elements) + if format == 'local': + # FIXME The rendering below is simplistic, we should implement + # https://github.com/liberapay/liberapay.com/issues/1056 + elements = [addr['local_address'], addr['city'], addr['postal_code']] + if addr.get('region'): + elements.append(addr['region']) + elements.append(LOCALE_EN.countries[addr['country']]) + sep = ', ' if single_line else '\n' + elif format == 'downward': + elements = [LOCALE_EN.countries[addr['country']]] + if addr.get('region'): + elements.append(addr['region']) + elements += [addr['city'], addr['postal_code'], addr['local_address']] + sep = ' / ' if single_line else '\n' else: - return '\n'.join(elements) + raise ValueError(f"unknown `format` value {format!r}") + return sep.join(elements) def mkdir_p(path): diff --git a/liberapay/wireup.py b/liberapay/wireup.py index d283e30cf1..8938aa6d69 100644 --- a/liberapay/wireup.py +++ b/liberapay/wireup.py @@ -48,7 +48,7 @@ from liberapay.utils import find_files, markdown, resolve from liberapay.utils.emails import compile_email_spt from liberapay.utils.http_caching import asset_etag -from liberapay.utils.types import Object +from liberapay.utils.types import LocalizedString, Object from liberapay.version import get_version from liberapay.website import Website @@ -172,6 +172,20 @@ def cast_currency_basket(v, cursor): except (psycopg2.ProgrammingError, NeedDatabase): pass + def cast_localized_string(v, cursor): + if v in (None, '(,)'): + return None + else: + text, lang = v[1:-1].split(',') + if text.startswith('"') and text.endswith('"'): + text = text[1:-1].replace('""', '"') + return LocalizedString(text, lang) + try: + oid = db.one("SELECT 'localized_string'::regtype::oid") + register_type(new_type((oid,), 'localized_string', cast_localized_string)) + except (psycopg2.ProgrammingError, NeedDatabase): + pass + if db and env.override_query_cache: db.cache.max_size = 0 diff --git a/sql/branch.sql b/sql/branch.sql new file mode 100644 index 0000000000..cd4060a71b --- /dev/null +++ b/sql/branch.sql @@ -0,0 +1 @@ +CREATE TYPE localized_string AS (string text, lang text); diff --git a/style/base/utils.scss b/style/base/utils.scss index 5ef82676ee..32325ca144 100644 --- a/style/base/utils.scss +++ b/style/base/utils.scss @@ -31,6 +31,10 @@ margin-top: 0 !important; } +.m-0 { + margin: 0; +} + .mb-3 { margin-bottom: ($line-height-computed / 2); } @@ -47,6 +51,14 @@ margin-bottom: $line-height-computed; } +.row-gap-3 { + row-gap: 0.5em; +} + +.column-gap-2 { + column-gap: 0.3em; +} + .pre-wrap { white-space: pre-wrap; } diff --git a/templates/macros/pagination.html b/templates/macros/pagination.html index fa21522801..3b74cd1535 100644 --- a/templates/macros/pagination.html +++ b/templates/macros/pagination.html @@ -27,12 +27,13 @@ % if current_page > 1 or has_more
{{ summary or '' }}
+{{ summary or '' }}
+ % if numbers{{ _("This page lists Liberapay users who are hoping to receive their first donations.") }}
+{{ glyphicon('warning-sign') }} {{ _( + "Despite our efforts, some of the listed profiles may be spam or fraud." + ) }}
+ % if last_shown is None +{{ glyphicon('info-sign') }} {{ _( + "Profiles only start appearing in the list 72 hours after they're created." + ) }}
+ % endif + +{{ _("Nothing to show.") }}
+% endif + +% if user.ANON +{{ _("Create your account") }}
+% endif + +% endblock diff --git a/www/explore/individuals.spt b/www/explore/individuals.spt index aca389fc8d..3d5d36f60d 100644 --- a/www/explore/individuals.spt +++ b/www/explore/individuals.spt @@ -1,10 +1,16 @@ [---] +sort_by = request.qs.get_choice('sort_by', ('receiving', 'join_time'), default='receiving') +order = request.qs.get_choice('order', ('asc', 'desc'), default='desc') +if sort_by == 'receiving': + sql_order = f"convert(p.receiving, 'EUR') {order}, p.join_time {order}" +else: + sql_order = f"p.{sort_by} {order}" per_page = 18 current_page = request.qs.get_int('page', default=1, minimum=1, maximum=100) individuals = website.db.all(""" SELECT p - , ( SELECT s.content + , ( SELECT (s.content, s.lang)::localized_string FROM statements s WHERE s.participant = p.id AND s.type = 'summary' @@ -18,10 +24,10 @@ individuals = website.db.all(""" AND p.hide_receiving IS NOT TRUE AND p.hide_from_lists = 0 AND p.receiving > 0 - ORDER BY convert(p.receiving, 'EUR') DESC, p.join_time DESC + ORDER BY {} LIMIT %s OFFSET %s -""", (locale.language, per_page + 1, (current_page - 1) * per_page)) +""".format(sql_order), (locale.language, per_page + 1, (current_page - 1) * per_page), max_age=0) has_more = len(individuals) > per_page individuals = individuals[:per_page] @@ -37,20 +43,34 @@ subhead = _("Individuals") % block content % if individuals - % if current_page == 1 + % if current_page == 1 and sort_by == 'receiving' and order == 'desc'{{ _("The top {0} individuals on Liberapay are:", len(individuals)) }}
- % else -{{ _( - "List of individuals on Liberapay, page {number}:", number=current_page - ) }}
% endif +{{ _("Nothing to show.") }}
% endif diff --git a/www/explore/organizations.spt b/www/explore/organizations.spt index 6199390d5d..645d77e2dc 100644 --- a/www/explore/organizations.spt +++ b/www/explore/organizations.spt @@ -1,10 +1,16 @@ [---] +sort_by = request.qs.get_choice('sort_by', ('receiving', 'join_time'), default='receiving') +order = request.qs.get_choice('order', ('asc', 'desc'), default='desc') +if sort_by == 'receiving': + sql_order = f"convert(p.receiving, 'EUR') {order}, p.join_time {order}" +else: + sql_order = f"p.{sort_by} {order}" per_page = 18 current_page = request.qs.get_int('page', default=1, minimum=1, maximum=100) organizations = website.db.all(""" SELECT p - , ( SELECT s.content + , ( SELECT (s.content, s.lang)::localized_string FROM statements s WHERE s.participant = p.id AND s.type = 'summary' @@ -18,10 +24,10 @@ organizations = website.db.all(""" AND p.hide_receiving IS NOT TRUE AND p.hide_from_lists = 0 AND p.receiving > 0 - ORDER BY convert(p.receiving, 'EUR') DESC, p.join_time DESC + ORDER BY {} LIMIT %s OFFSET %s -""", (locale.language, per_page + 1, (current_page - 1) * per_page)) +""".format(sql_order), (locale.language, per_page + 1, (current_page - 1) * per_page), max_age=0) has_more = len(organizations) > per_page organizations = organizations[:per_page] @@ -37,21 +43,34 @@ subhead = _("Organizations") % block content % if organizations - % if current_page == 1 + % if current_page == 1 and sort_by == 'receiving' and order == 'desc'{{ _("The top {0} organizations on Liberapay are:", len(organizations)) }}
- % else -{{ _( - "List of organizations on Liberapay, page {number}:", - number=current_page - ) }}
% endif +{{ _("Nothing to show.") }}
% endif diff --git a/www/explore/pledges.spt b/www/explore/pledges.spt index 69982ced43..5a474fd8e5 100644 --- a/www/explore/pledges.spt +++ b/www/explore/pledges.spt @@ -13,7 +13,7 @@ pledgees = website.db.all(""" ORDER BY p.npatrons DESC, convert(p.receiving, 'EUR') DESC, e.id DESC LIMIT %s OFFSET %s -""", (per_page + 1, (current_page - 1) * per_page)) +""", (per_page + 1, (current_page - 1) * per_page), max_age=0) has_more = len(pledgees) > per_page pledgees = pledgees[:per_page] @@ -35,7 +35,6 @@ subhead = _("Unclaimed Donations") "the recipients join. Of course we notify the donors when that happens." ) }} -{{ _( "Please don't spam the people and projects listed below with messages " diff --git a/www/explore/recipients.spt b/www/explore/recipients.spt new file mode 100644 index 0000000000..6ed9d39619 --- /dev/null +++ b/www/explore/recipients.spt @@ -0,0 +1,108 @@ +[---] + +kind = request.qs.get_choice('kind', ('all', 'individual', 'organization', 'group'), default='all') +if kind == 'all': + sql_filter = "p.kind IN ('individual', 'organization', 'group')" +else: + sql_filter = f"p.kind = '{kind}'" +sort_by = request.qs.get_choice('sort_by', ('receiving', 'join_time'), default='receiving') +order = request.qs.get_choice('order', ('asc', 'desc'), default='desc') +if sort_by == 'receiving': + sql_order = f"convert(p.receiving, 'EUR') {order}, p.join_time {order}" +else: + sql_order = f"p.{sort_by} {order}" +per_page = 18 +current_page = request.qs.get_int('page', default=1, minimum=1, maximum=100) +participants = website.db.all(""" + SELECT p AS participant + , ( SELECT (s.content, s.lang)::localized_string + FROM statements s + WHERE s.participant = p.id + AND s.type = 'summary' + ORDER BY s.lang = %(lang)s DESC, s.id + LIMIT 1 + ) AS summary + FROM participants p + WHERE {} + AND p.status = 'active' + AND (p.goal > 0 OR p.goal IS NULL) + AND p.hide_from_lists = 0 + AND p.receiving > 0 + AND p.hide_receiving IS NOT TRUE + ORDER BY {} + LIMIT %(limit)s + OFFSET %(offset)s +""".format(sql_filter, sql_order), dict( + lang=locale.language, + limit=per_page + 1, + offset=(current_page - 1) * per_page, +), max_age=0) +has_more = len(participants) > per_page +participants = participants[:per_page] + +title = _("Explore") +subhead = _("Recipients") + +[---] text/html +% from 'templates/macros/nav.html' import querystring_nav with context +% from 'templates/macros/pagination.html' import simple_pager with context +% from 'templates/macros/profile-box.html' import profile_box_embedded with context + +% extends "templates/layouts/explore.html" + +% block content + +% if participants + % if current_page == 1 and sort_by == 'receiving' and order == 'desc' +
{{ ngettext( + "The individual receiving the most money through Liberapay is:", + "The {n} individuals receiving the most money through Liberapay are:", + len(participants) + ) if kind == 'individual' else ngettext( + "The organization receiving the most money through Liberapay is:", + "The {n} organizations receiving the most money through Liberapay are:", + len(participants) + ) if kind == 'organization' else ngettext( + "The team receiving the most money through Liberapay is:", + "The {n} teams receiving the most money through Liberapay are:", + len(teams) + ) if kind == 'group' else ngettext( + "The Liberapay account receiving the most money is:", + "The {n} Liberapay accounts receiving the most money are:", + len(participants) + ) }}
+ % endif + +{{ _("Nothing to show.") }}
+% endif + +% if user.ANON +{{ _("Create your account") }}
+% endif + +% endblock diff --git a/www/explore/repositories.spt b/www/explore/repositories.spt index 7de219fb84..bda808e753 100644 --- a/www/explore/repositories.spt +++ b/www/explore/repositories.spt @@ -1,5 +1,11 @@ [---] +sort_by = request.qs.get_choice('sort_by', ('stars_count', 'id'), default='stars_count') +order = request.qs.get_choice('order', ('asc', 'desc'), default='desc') +if sort_by != 'id': + sql_order = f"r.{sort_by} {order}, r.id {order}" +else: + sql_order = f"r.{sort_by} {order}" per_page = 20 current_page = request.qs.get_int('page', default=1, minimum=1, maximum=100) repos = website.db.all(""" @@ -11,10 +17,10 @@ repos = website.db.all(""" AND r.show_on_profile AND e.missing_since IS NULL AND p.status = 'active' - ORDER BY r.stars_count DESC, r.id DESC + ORDER BY {} LIMIT %s OFFSET %s -""", (per_page + 1, (current_page - 1) * per_page)) +""".format(sql_order), (per_page + 1, (current_page - 1) * per_page), max_age=0) has_more = len(repos) > per_page repos = repos[:per_page] @@ -28,17 +34,12 @@ subhead = _("Repositories") % block content -% if current_page == 1 +% if current_page == 1 and sort_by == 'stars_count' and order == 'desc'{{ ngettext( "The most popular repository currently linked to a Liberapay account is:", "The {n} most popular repositories currently linked to a Liberapay account are:", len(repos) ) }}
-% else -{{ _( - "List of repositories currently linked to a Liberapay account, page {number}:", - number=current_page -) }}
% endif{{ _( - "A team allows members of a project to receive money and share it, without " - "having to set up a legal entity. {0}Learn more…{1}", - ''|safe, ''|safe -) }}
-{{ ngettext( - "The top team on Liberapay is:", - "The top {n} teams on Liberapay are:", - len(teams) -) }}
-% else -{{ _("List of teams on Liberapay, page {number}:", number=current_page) }}
-% endif -{{ _( + "A team allows members of a project to receive money and share it, without " + "having to set up a legal entity. {0}Learn more…{1}", + ''|safe, ''|safe + ) }}
+{{ ngettext( + "The top team on Liberapay is:", + "The top {n} teams on Liberapay are:", + len(teams) + ) }}
+ % endif + +{{ _("Nothing to show.") }}
+% endif -