diff --git a/README.md b/README.md index 867904bd3..fbba66092 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Make sure you have the following dependencies installed first: - python ≥ 3.11 - including the C headers of python and libffi, which are packaged separately in many Linux distributions -- postgresql 13 (see [the official download & install docs](https://www.postgresql.org/download/)) +- postgresql 16 (see [the official download & install docs](https://www.postgresql.org/download/)) - make Then run: diff --git a/liberapay/cron.py b/liberapay/cron.py index f14171e85..b6ab3b048 100644 --- a/liberapay/cron.py +++ b/liberapay/cron.py @@ -172,30 +172,43 @@ def f(): self.running = False self.cron.website.tell_sentry(e) if self.exclusive: - try: - self.cron.website.db.run(""" - INSERT INTO cron_jobs - (name, last_error_time, last_error) - VALUES (%s, current_timestamp, %s) - ON CONFLICT (name) DO UPDATE - SET last_error_time = excluded.last_error_time - , last_error = excluded.last_error - """, (func_name, traceback.format_exc())) - except psycopg2.OperationalError: - pass + while True: + try: + self.cron.website.db.run(""" + INSERT INTO cron_jobs + (name, last_error_time, last_error) + VALUES (%s, current_timestamp, %s) + ON CONFLICT (name) DO UPDATE + SET last_error_time = excluded.last_error_time + , last_error = excluded.last_error + """, (func_name, traceback.format_exc())) + except psycopg2.OperationalError as e: + self.cron.website.tell_sentry(e) + # retry in a minute + sleep(60) + else: + break # retry in a minute sleep(60) continue else: self.running = False if self.exclusive: - self.cron.website.db.run(""" - INSERT INTO cron_jobs - (name, last_success_time) - VALUES (%s, current_timestamp) - ON CONFLICT (name) DO UPDATE - SET last_success_time = excluded.last_success_time - """, (func_name,)) + while True: + try: + self.cron.website.db.run(""" + INSERT INTO cron_jobs + (name, last_success_time) + VALUES (%s, current_timestamp) + ON CONFLICT (name) DO UPDATE + SET last_success_time = excluded.last_success_time + """, (func_name,)) + except psycopg2.OperationalError: + self.cron.website.tell_sentry(e) + # retry in a minute + sleep(60) + else: + break if period == 'irregular': if r is None: return diff --git a/liberapay/exceptions.py b/liberapay/exceptions.py index c78e2ede5..6b09ab965 100644 --- a/liberapay/exceptions.py +++ b/liberapay/exceptions.py @@ -126,6 +126,11 @@ def msg(self, _): return _("The username '{0}' contains invalid characters.", self.username) +class UsernameIsPurelyNumerical(UsernameError): + def msg(self, _): + return _("The username '{0}' is purely numerical. This isn't allowed.") + + class UsernameIsRestricted(UsernameError): def msg(self, _): return _("The username '{0}' is restricted.", self.username) diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py index fc645de72..0812ec803 100644 --- a/liberapay/models/participant.py +++ b/liberapay/models/participant.py @@ -65,6 +65,7 @@ UsernameContainsInvalidCharacters, UsernameEndsWithForbiddenSuffix, UsernameIsEmpty, + UsernameIsPurelyNumerical, UsernameIsRestricted, UsernameTooLong, ValueTooLong, @@ -2048,6 +2049,9 @@ def check_username(suggested): if set(suggested) - ASCII_ALLOWED_IN_USERNAME: raise UsernameContainsInvalidCharacters(suggested) + if suggested.isdigit(): + raise UsernameIsPurelyNumerical(suggested) + if suggested[0] == '.': raise UsernameBeginsWithRestrictedCharacter(suggested) @@ -2059,7 +2063,8 @@ def check_username(suggested): raise UsernameIsRestricted(suggested) def change_username(self, suggested, cursor=None, recorder=None): - self.check_username(suggested) + if suggested != f'~{self.id}': + self.check_username(suggested) recorder_id = getattr(recorder, 'id', None) if suggested != self.username: diff --git a/style/base/icons.scss b/style/base/icons.scss index b2ad68837..dcc9e64ec 100644 --- a/style/base/icons.scss +++ b/style/base/icons.scss @@ -12,6 +12,6 @@ width: 17px; } .icon-32 { - height: 32px; - width: 32px; + height: 34px; + width: 34px; } diff --git a/templates/macros/icons.html b/templates/macros/icons.html index aac072f32..9a1501651 100644 --- a/templates/macros/icons.html +++ b/templates/macros/icons.html @@ -48,8 +48,9 @@ % macro icon(name, sr='', size=16) % set name = icon_aliases.get(name, name) % if soft_assert(name in website.icon_names, "unknown icon name %r" % name) - + % else
+ + % if row.payment_instruments is defined + % endif diff --git a/www/admin/users.spt b/www/admin/users.spt index 34c3fb432..e366430e7 100644 --- a/www/admin/users.spt +++ b/www/admin/users.spt @@ -9,6 +9,11 @@ PT_STATUS_MAP = { } ACCOUNT_MARKS = set(website.db.one("SELECT array_to_json(enum_range(NULL::account_mark))")) ACCOUNT_MARKS.add('') +PAYMENT_INSTRUMENT_TYPES = { + 'paypal': "PayPal", + 'stripe-card': "card", + 'stripe-sdd': "SEPA mandate", +} [---] @@ -101,6 +106,14 @@ if mode == 'all': WHERE tip.tipper = p.id ) tip ) AS outgoing_donations + , ( SELECT json_objectagg(r.network:r.count) + FROM ( SELECT r.network, count(*) AS count + FROM exchange_routes r + WHERE r.participant = p.id + GROUP BY r.network + ORDER BY r.network + ) r + ) AS payment_instruments , ( SELECT json_agg(json_build_object( 'provider', a.provider, 'id', a.id, @@ -346,6 +359,19 @@ title = "Users Admin" none. % endifPayment instruments: + % if row.payment_instruments + % for network, count in row.payment_instruments.items() + {{ count }} {{ PAYMENT_INSTRUMENT_TYPES.get(network, network) }} + {{- '' if count == 1 else 's' }} + {{- '.' if loop.last else ',' }} + % endfor + % else + none. + % endif + % endif % endif
Payment accounts: