diff --git a/liberapay/constants.py b/liberapay/constants.py index 306e389bee..1a53ff287e 100644 --- a/liberapay/constants.py +++ b/liberapay/constants.py @@ -383,8 +383,8 @@ def __missing__(self, currency): ) PRIVACY_FIELDS = OrderedDict([ - ('hide_giving', (_("Hide total giving from others."), False)), - ('hide_receiving', (_("Hide total receiving from others."), False)), + ('hide_giving', (_("Do not publish the amounts of money I send."), False)), + ('hide_receiving', (_("Do not publish the amounts of money I receive."), False)), ('hide_from_search', (_("Hide this profile from search results on Liberapay."), True)), ('profile_noindex', (_("Tell web search engines not to index this profile."), True)), ('hide_from_lists', (_("Prevent this profile from being listed on Liberapay."), True)), diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py index ec15eeab7c..d18647d9d4 100644 --- a/liberapay/models/participant.py +++ b/liberapay/models/participant.py @@ -982,9 +982,9 @@ def clear_tips_giving(self, cursor): tippees = cursor.all(""" INSERT INTO tips ( ctime, tipper, tippee, amount, period, periodic_amount - , paid_in_advance, is_funded, renewal_mode ) + , paid_in_advance, is_funded, renewal_mode, visibility ) SELECT ctime, tipper, tippee, amount, period, periodic_amount - , paid_in_advance, is_funded, 0 + , paid_in_advance, is_funded, 0, visibility FROM current_tips WHERE tipper = %s AND renewal_mode > 0 @@ -1915,6 +1915,38 @@ def send_newsletters(cls): sleep(1) + # Recipient settings + # ================== + + @cached_property + def recipient_settings(self): + return self.db.one(""" + SELECT * + FROM recipient_settings + WHERE participant = %s + """, (self.id,), default=Object( + participant=self.id, + patron_visibilities=(7 if self.status == 'stub' else 0), + )) + + def update_recipient_settings(self, **kw): + cols, vals = zip(*kw.items()) + updates = ','.join('{0}=excluded.{0}'.format(col) for col in cols) + cols = ', '.join(cols) + placeholders = ', '.join(['%s']*len(vals)) + with self.db.get_cursor() as cursor: + settings = cursor.one(""" + INSERT INTO recipient_settings + (participant, {0}) + VALUES (%s, {1}) + ON CONFLICT (participant) DO UPDATE + SET {2} + RETURNING * + """.format(cols, placeholders, updates), (self.id,) + vals) + self.add_event(cursor, 'recipient_settings', kw) + self.recipient_settings = settings + + # Random Stuff # ============ @@ -2551,7 +2583,7 @@ def update_receiving(self, cursor=None): def set_tip_to(self, tippee, periodic_amount, period='weekly', renewal_mode=None, - update_self=True, update_tippee=True): + visibility=None, update_self=True, update_tippee=True): """Given a Participant or username, and amount as str, returns a dict. We INSERT instead of UPDATE, so that we have history to explore. The @@ -2599,7 +2631,8 @@ def set_tip_to(self, tippee, periodic_amount, period='weekly', renewal_mode=None INSERT INTO tips ( ctime, tipper, tippee, amount, period, periodic_amount , paid_in_advance - , renewal_mode ) + , renewal_mode + , visibility ) VALUES ( COALESCE((SELECT ctime FROM current_tip), CURRENT_TIMESTAMP) , %(tipper)s, %(tippee)s, %(amount)s, %(period)s, %(periodic_amount)s , (SELECT convert(paid_in_advance, %(currency)s) FROM current_tip) @@ -2607,12 +2640,18 @@ def set_tip_to(self, tippee, periodic_amount, period='weekly', renewal_mode=None %(renewal_mode)s, (SELECT renewal_mode FROM current_tip WHERE renewal_mode > 0), 1 + ) + , coalesce( + %(visibility)s, + (SELECT visibility FROM current_tip), + 1 ) ) RETURNING tips """, dict( tipper=self.id, tippee=tippee.id, amount=amount, currency=amount.currency, period=period, periodic_amount=periodic_amount, renewal_mode=renewal_mode, + visibility=visibility, )) t.tipper_p = self t.tippee_p = tippee @@ -2641,6 +2680,7 @@ def _zero_tip(tippee, currency=None): return Tip( amount=zero, is_funded=False, tippee=tippee.id, tippee_p=tippee, period='weekly', periodic_amount=zero, renewal_mode=0, + visibility=0, ) @@ -2648,9 +2688,9 @@ def stop_tip_to(self, tippee, update_schedule=True): t = self.db.one(""" INSERT INTO tips ( ctime, tipper, tippee, amount, period, periodic_amount - , paid_in_advance, is_funded, renewal_mode ) + , paid_in_advance, is_funded, renewal_mode, visibility ) SELECT ctime, tipper, tippee, amount, period, periodic_amount - , paid_in_advance, is_funded, 0 + , paid_in_advance, is_funded, 0, visibility FROM current_tips WHERE tipper = %(tipper)s AND tippee = %(tippee)s @@ -2675,14 +2715,14 @@ def hide_tip_to(self, tippee_id, hide=True): return self.db.one(""" INSERT INTO tips ( ctime, tipper, tippee, amount, period, periodic_amount - , paid_in_advance, is_funded, renewal_mode, hidden ) + , paid_in_advance, is_funded, renewal_mode, visibility ) SELECT ctime, tipper, tippee, amount, period, periodic_amount - , paid_in_advance, is_funded, renewal_mode, %(hide)s + , paid_in_advance, is_funded, renewal_mode, -visibility FROM current_tips WHERE tipper = %(tipper)s AND tippee = %(tippee)s AND renewal_mode = 0 - AND hidden IS NOT %(hide)s + AND (visibility < 0) IS NOT %(hide)s RETURNING tips """, dict(tipper=self.id, tippee=tippee_id, hide=hide)) @@ -3261,6 +3301,7 @@ def get_giving_details(self): , t.is_funded , t.paid_in_advance , t.renewal_mode + , t.visibility , p.payment_providers , ( t.paid_in_advance IS NULL OR t.paid_in_advance < (t.amount * 3) @@ -3269,7 +3310,7 @@ def get_giving_details(self): JOIN participants p ON p.id = t.tippee WHERE t.tipper = %s AND p.status <> 'stub' - AND t.hidden IS NOT true + AND t.visibility > 0 ORDER BY tippee, t.mtime DESC """, (self.id,)) @@ -3283,13 +3324,14 @@ def get_giving_details(self): , t.ctime , t.mtime , t.renewal_mode + , t.visibility , (e, p)::elsewhere_with_participant AS e_account FROM current_tips t JOIN participants p ON p.id = t.tippee JOIN elsewhere e ON e.participant = t.tippee WHERE t.tipper = %s AND p.status = 'stub' - AND t.hidden IS NOT true + AND t.visibility > 0 ORDER BY tippee, t.mtime DESC """, (self.id,)) @@ -3603,11 +3645,11 @@ def take_over(self, account, have_confirmation=False): -- maximum allowed. INSERT INTO tips ( ctime, tipper, tippee, amount, period - , periodic_amount, is_funded, renewal_mode, hidden + , periodic_amount, is_funded, renewal_mode, visibility , paid_in_advance ) SELECT DISTINCT ON (tipper) ctime, tipper, %(live)s AS tippee, amount, period - , periodic_amount, is_funded, renewal_mode, hidden + , periodic_amount, is_funded, renewal_mode, visibility , ( SELECT sum(t2.paid_in_advance, t.amount::currency) FROM temp_tips t2 WHERE t2.tipper = t.tipper @@ -3624,9 +3666,9 @@ def take_over(self, account, have_confirmation=False): ZERO_OUT_OLD_TIPS_RECEIVING = """ INSERT INTO tips ( ctime, tipper, tippee, amount, period, periodic_amount - , paid_in_advance, is_funded, renewal_mode, hidden ) + , paid_in_advance, is_funded, renewal_mode, visibility ) SELECT ctime, tipper, tippee, amount, period, periodic_amount - , NULL, false, 0, true + , NULL, false, 0, -visibility FROM temp_tips WHERE tippee = %(dead)s AND ( coalesce_currency_amount(paid_in_advance, amount::currency) > 0 OR @@ -3788,7 +3830,7 @@ def to_dict(self, details=False): # Key: receiving # Values: - # null - user is receiving anonymously + # null - user does not publish how much they receive # 3.00 - user receives this amount in tips if not self.hide_receiving: receiving = self.receiving @@ -3798,7 +3840,7 @@ def to_dict(self, details=False): # Key: giving # Values: - # null - user is giving anonymously + # null - user does not publish how much they give # 3.00 - user gives this amount in tips if not self.hide_giving: giving = self.giving diff --git a/liberapay/payin/common.py b/liberapay/payin/common.py index 6147bbbf56..d2e3431a8c 100644 --- a/liberapay/payin/common.py +++ b/liberapay/payin/common.py @@ -17,7 +17,7 @@ ProtoTransfer = namedtuple( 'ProtoTransfer', - 'amount recipient destination context unit_amount period team', + 'amount recipient destination context unit_amount period team visibility', ) @@ -63,7 +63,7 @@ def prepare_payin(db, payer, amount, route, proto_transfers, off_session=False): for t in proto_transfers: payin_transfers.append(prepare_payin_transfer( cursor, payin, t.recipient, t.destination, t.context, t.amount, - t.unit_amount, t.period, t.team, + t.visibility, t.unit_amount, t.period, t.team, )) return payin, payin_transfers @@ -240,8 +240,8 @@ def adjust_payin_transfers(db, payin, net_amount): unit_amount = (d.amount / n_periods).round(allow_zero=False) prepare_payin_transfer( db, payin, d.recipient, d.destination, 'team-donation', - d.amount, unit_amount, tip.period, - team=team.id + d.amount, tip.visibility, unit_amount, tip.period, + team=team.id, ) else: pt = transfers[0] @@ -303,7 +303,7 @@ def resolve_tip( ) return [ProtoTransfer( payment_amount, tippee, destination, 'personal-donation', - tip.periodic_amount, tip.period, None, + tip.periodic_amount, tip.period, None, tip.visibility, )] @@ -450,6 +450,7 @@ def resolve_team_donation( (t.resolved_amount / n_periods).round(allow_zero=False), tip.period, team.id, + tip.visibility, ) for t in selected_takes if t.resolved_amount != 0 ] @@ -460,7 +461,7 @@ def resolve_team_donation( account = payment_accounts[member.id] return [ProtoTransfer( payment_amount, member, account, 'team-donation', - tip.periodic_amount, tip.period, team.id, + tip.periodic_amount, tip.period, team.id, tip.visibility, )] @@ -571,8 +572,8 @@ def compute_priority(item): def prepare_payin_transfer( - db, payin, recipient, destination, context, amount, - unit_amount=None, period=None, team=None + db, payin, recipient, destination, context, amount, visibility, + unit_amount=None, period=None, team=None, ): """Prepare the allocation of funds from a payin. @@ -581,6 +582,7 @@ def prepare_payin_transfer( recipient (Participant): the user who will receive the money destination (Record): a row from the `payment_accounts` table amount (Money): the amount of money that will be received + visibility (int): a copy of `tip.visibility` unit_amount (Money): the `periodic_amount` of a recurrent donation period (str): the period of a recurrent payment team (int): the ID of the project this payment is tied to @@ -601,14 +603,14 @@ def prepare_payin_transfer( return db.one(""" INSERT INTO payin_transfers (payin, payer, recipient, destination, context, amount, - unit_amount, n_units, period, team, + unit_amount, n_units, period, team, visibility, status, ctime) VALUES (%s, %s, %s, %s, %s, %s, - %s, %s, %s, %s, + %s, %s, %s, %s, %s, 'pre', clock_timestamp()) RETURNING * """, (payin.id, payin.payer, recipient.id, destination.pk, context, amount, - unit_amount, n_units, period, team)) + unit_amount, n_units, period, team, visibility)) def update_payin_transfer( diff --git a/liberapay/payin/stripe.py b/liberapay/payin/stripe.py index d63e8c2301..4ba798ca1e 100644 --- a/liberapay/payin/stripe.py +++ b/liberapay/payin/stripe.py @@ -708,9 +708,13 @@ def generate_transfer_description(pt): WHERE id = %s """, (pt.team or pt.recipient,)) if pt.team: - return f"anonymous donation for team {name} via Liberapay" + name = f"team {name}" + if pt.visibility == 3: + return f"public donation for {name} via Liberapay" + if pt.visibility == 2: + return f"private donation for {name} via Liberapay" else: - return f"anonymous donation for {name} via Liberapay" + return f"secret donation for {name} via Liberapay" def record_refunds(db, payin, charge): diff --git a/liberapay/utils/fake_data.py b/liberapay/utils/fake_data.py index d3d93ea7ef..ccb775590c 100644 --- a/liberapay/utils/fake_data.py +++ b/liberapay/utils/fake_data.py @@ -115,6 +115,7 @@ def fake_tip(db, tipper, tippee): amount=Money(amount, 'EUR'), period=period, periodic_amount=Money(periodic_amount, 'EUR'), + visibility=1, ) diff --git a/liberapay/utils/history.py b/liberapay/utils/history.py index cb4ba85e5c..c76dcb7a8c 100644 --- a/liberapay/utils/history.py +++ b/liberapay/utils/history.py @@ -471,7 +471,7 @@ def iter_payin_events(db, participant, period_start, period_end, minimize=False) outgoing_transfers = db.all(""" SELECT tr.id, tr.ctime, tr.payin, tr.recipient, tr.context, tr.status, tr.error , tr.amount, tr.fee, tr.unit_amount, tr.n_units, tr.period - , tr.reversed_amount + , tr.reversed_amount, tr.visibility , p.username AS recipient_username, p2.username AS team_name , r.network AS payin_method FROM payin_transfers tr @@ -487,7 +487,7 @@ def iter_payin_events(db, participant, period_start, period_end, minimize=False) incoming_transfers = db.all(""" SELECT tr.id, tr.ctime, tr.payin, tr.payer, tr.context, tr.status, tr.error , tr.amount, tr.fee, tr.unit_amount, tr.n_units, tr.period - , tr.reversed_amount + , tr.reversed_amount, tr.visibility , p.username AS payer_username, p2.username AS team_name , r.network AS payin_method FROM payin_transfers tr diff --git a/sql/branch.sql b/sql/branch.sql new file mode 100644 index 0000000000..0dcec600c0 --- /dev/null +++ b/sql/branch.sql @@ -0,0 +1,43 @@ +BEGIN; + ALTER TABLE tips ADD COLUMN visibility int CHECK (visibility >= -3 AND visibility <> 0 AND visibility <= 3); + -- 1 means secret, 2 means private, 3 means public, negative numbers mean hidden + ALTER TABLE payin_transfers ADD COLUMN visibility int DEFAULT 1 CHECK (visibility >= 1 AND visibility <= 3); + -- same meanings as for tips, but negative numbers aren't allowed + + UPDATE tips SET visibility = -1 WHERE hidden AND visibility IS NULL; + + CREATE OR REPLACE VIEW current_tips AS + SELECT DISTINCT ON (tipper, tippee) * + FROM tips + ORDER BY tipper, tippee, mtime DESC; + + CREATE TABLE recipient_settings + ( participant bigint PRIMARY KEY REFERENCES participants + , patron_visibilities int NOT NULL CHECK (patron_visibilities > 0) + -- Three bits: 1 is for "secret", 2 is for "private", 4 is for "public". + ); +END; + +SELECT 'after deployment'; + +BEGIN; + UPDATE tips SET visibility = -1 WHERE hidden AND visibility = 1; + DROP FUNCTION compute_arrears(current_tips); + DROP CAST (current_tips AS tips); + DROP VIEW current_tips; + ALTER TABLE tips + DROP COLUMN hidden, + ALTER COLUMN visibility DROP DEFAULT, + ALTER COLUMN visibility SET NOT NULL; + CREATE OR REPLACE VIEW current_tips AS + SELECT DISTINCT ON (tipper, tippee) * + FROM tips + ORDER BY tipper, tippee, mtime DESC; + CREATE CAST (current_tips AS tips) WITH INOUT; + CREATE FUNCTION compute_arrears(tip current_tips) RETURNS currency_amount AS $$ + SELECT compute_arrears(tip::tips); + $$ LANGUAGE sql; + ALTER TABLE payin_transfers + ALTER COLUMN visibility DROP DEFAULT, + ALTER COLUMN visibility SET NOT NULL; +END; diff --git a/style/base/base.scss b/style/base/base.scss index 7baa92c9e3..204b531735 100644 --- a/style/base/base.scss +++ b/style/base/base.scss @@ -436,6 +436,7 @@ input.search { .mini-user { display: inline-block; margin: 0 20px 25px; + text-align: center; width: 104px; .avatar-cutter-98 { border: 3px solid $gray-lighter; @@ -448,7 +449,6 @@ input.search { margin-top: 5px; max-height: 2 * $line-height-computed; overflow: hidden; - text-align: center; text-overflow: ellipsis; white-space: nowrap; } diff --git a/templates/layouts/components/navbar-logged-in.html b/templates/layouts/components/navbar-logged-in.html index 33f573074f..7abe3ccb5d 100644 --- a/templates/layouts/components/navbar-logged-in.html +++ b/templates/layouts/components/navbar-logged-in.html @@ -13,6 +13,7 @@
  • {{ _("Payment Instruments") }}
  • {{ _("Payment Schedule") }}
  • {{ _("Receiving") }}
  • +
  • {{ _("Patrons") }}
  • {{ _("Payment Processors") }}
  • {{ _("Ledger") }}
  • % if user.mangopay_user_id diff --git a/templates/macros/nav.html b/templates/macros/nav.html index 98b1139cba..eddf0407fc 100644 --- a/templates/macros/nav.html +++ b/templates/macros/nav.html @@ -118,6 +118,7 @@ }), ('/receiving/', _("Receiving"), { 'subnav': nav([ + ('/patrons/', _("Patrons")), ('/payment/', _("Payment Processors")), ], base=base) }), @@ -142,7 +143,11 @@ ], base=participant.path('edit')), 'togglable': True, }), - ('/receiving/', _("Receiving")), + ('/receiving/', _("Receiving"), { + 'subnav': nav([ + ('/patrons/', _("Patrons")), + ], base=base) + }), ('/emails/', _("Emails")), ('/widgets/', _("Widgets")), ]), base=base) }} diff --git a/templates/macros/your-tip.html b/templates/macros/your-tip.html index 50d7e72ef4..dd59c5c9ce 100644 --- a/templates/macros/your-tip.html +++ b/templates/macros/your-tip.html @@ -229,6 +229,82 @@
    {{ _("Manual renewal") }}
    + % set patron_visibilities = tippee_p.recipient_settings.patron_visibilities + % set paypal_only = tippee_p.payment_providers == 2 + % if paypal_only and patron_visibilities.__and__(1) + % set patron_visibilities = patron_visibilities.__xor__(1).__or__(2) + % endif + % if patron_visibilities == 1 +

    {{ glyphicon('info-sign') }} {{ _( + "{username} has chosen not to see who their patrons are, so your donation will be secret.", + username=tippee_name + ) }}

    + % elif patron_visibilities == 2 +

    {{ glyphicon('info-sign') }} {{ _( + "This donation won't be secret, you will appear in {username}'s private list of patrons.", + username=tippee_name + ) }}

    + % elif patron_visibilities == 4 +

    {{ glyphicon('info-sign') }} {{ _( + "{username} discloses who their patrons are, your donation will be public.", + username=tippee_name + ) }}

    + % elif patron_visibilities +

    {{ _("Please select a privacy level for this donation:") }}

    + + % else +

    {{ glyphicon('info-sign') }} {{ _( + "{username} hasn't yet specified whether they want to see who their patrons are, " + "so your donation will be secret.", + username=tippee_name + ) }}

    + % endif
    + % if show_visibility_hint +
    + % endif +% endif + +% if show_visibility_hint +
    + {{ glyphicon('info-sign') }} {{ _( + "Liberapay now supports non-anonymous donations, do you want to " + "modify the visibility of all your donations at once?" + ) }} +   +
    + + + + +
    +
    % if show_missing_route_warning
    % endif @@ -268,17 +315,25 @@ next_payday = compute_next_payday_date() ) }}

    % endif

    + % if tip.visibility == 3 + {{ fontawesome('eye') }} {{ _("Public donation") }} + % elif tip.visibility == 2 + {{ fontawesome('eye-slash') }} {{ _("Private donation") }} + % else + {{ fontawesome('user-secret') }} {{ _("Secret donation") }} + % endif +    % if tip.renewal_mode == 2 % if tip.tippee_p.payment_providers == 2 - {{ glyphicon('repeat') }} {{ _( +

    {{ fontawesome('repeat') }} {{ _( "Automatic renewals are enabled for this donation, but are " "currently impossible due to payment processor limitations." - ) }} + ) }}

    % else - {{ glyphicon('repeat') }} {{ _("Automatic renewal") }} + {{ fontawesome('repeat') }} {{ _("Automatic renewal") }} % endif % else - {{ glyphicon('user') }} {{ _("Manual renewal") }} + {{ fontawesome('hand-pointer-o') }} {{ _("Manual renewal") }} % endif

    % if tippee.is_suspended @@ -320,9 +375,9 @@ next_payday = compute_next_payday_date()

    {{ glyphicon('exclamation-sign') }} {{ _("Awaiting payment.") }}

    - - {{ glyphicon('repeat') }} {{ _("Renew") }} + {{ fontawesome('repeat') }} {{ _("Renew") }}   {{ payment_methods_icons(tippee) }} % else diff --git a/www/%username/giving/pay/%payment_id.spt b/www/%username/giving/pay/%payment_id.spt index 6bfb29e7c2..6ecff71a1d 100644 --- a/www/%username/giving/pay/%payment_id.spt +++ b/www/%username/giving/pay/%payment_id.spt @@ -268,7 +268,7 @@ title = _("Funding your donations") % elif payment.auto_renewal {{ _("Doesn't support automatic renewal") }} % else - {{ _("Not anonymous") }} + {{ _("Reveals your name and email address to the recipient") }} % endif % if possible diff --git a/www/%username/giving/pay/paypal/%payin_id.spt b/www/%username/giving/pay/paypal/%payin_id.spt index 881716267f..daa801de99 100644 --- a/www/%username/giving/pay/paypal/%payin_id.spt +++ b/www/%username/giving/pay/paypal/%payin_id.spt @@ -268,8 +268,7 @@ title = _("Funding your donations") ) }}

    {{ glyphicon('warning-sign') }} {{ _( - "PayPal payments are currently not anonymous, the recipient will " - "see your name and email address." + "PayPal reveals your name and email address to the recipient." ) }}

    diff --git a/www/%username/giving/pay/stripe/%payin_id.spt b/www/%username/giving/pay/stripe/%payin_id.spt index 6f9d8baae9..e9839ae713 100644 --- a/www/%username/giving/pay/stripe/%payin_id.spt +++ b/www/%username/giving/pay/stripe/%payin_id.spt @@ -160,6 +160,16 @@ elif payin_id: else: raise NotImplementedError() route = ExchangeRoute.from_id(payer, payin.route) + tell_payer_to_fill_profile = ( + payin.status in ('pending', 'succeeded') and + website.db.one(""" + SELECT count(*) + FROM payin_transfers + WHERE payin = %s + AND visibility = 3 + """, (payin.id,)) > 0 and + not (participant.public_name or participant.username) + ) else: routes = website.db.all(""" @@ -272,6 +282,17 @@ title = _("Funding your donations") % endif % endif + % if tell_payer_to_fill_profile +
    +

    {{ _( + "Since you've chosen to make a public donation, we recommend that you " + "complete your public profile." + ) }}

    + {{ + _("Edit your profile") + }} + % endif + % if status != 'failed'
    % set n_fundable = payer.get_tips_awaiting_payment()[1] diff --git a/www/%username/giving/public.spt b/www/%username/giving/public.spt new file mode 100644 index 0000000000..310b2336fb --- /dev/null +++ b/www/%username/giving/public.spt @@ -0,0 +1,34 @@ +from pando.utils import utcnow + +from liberapay.utils import get_participant + +[---] + +participant = get_participant(state, restrict=False) + +public_donees = website.db.all(""" + SELECT tip.ctime::date AS pledge_date + , tippee_p.id AS donee_id + , tippee_p.username AS donee_username + , coalesce(tippee_p.public_name, '') AS donee_public_name + , (tip.amount).currency AS donation_currency + , ( CASE WHEN tippee_p.hide_receiving THEN 'private' + ELSE (tip.amount).amount::text + END ) AS weekly_amount + , coalesce(tippee_p.avatar_url, '') AS donee_avatar_url + FROM current_tips tip + JOIN participants tippee_p ON tippee_p.id = tip.tippee + WHERE tip.tipper = %s + AND tip.visibility = 3 + AND tip.paid_in_advance > 0 + AND tip.renewal_mode > 0 + ORDER BY tip.ctime, tip.id +""", (participant.id,)) + +response.headers[b'Content-Disposition'] = ( + "attachment; filename*=UTF-8''liberapay-public-donees-of-%s-%s.csv" % + (participant.username, utcnow().date()) +).encode('utf8') + +[---] text/csv via csv_dump +public_donees diff --git a/www/%username/index.html.spt b/www/%username/index.html.spt index 62c5edf501..6c25c72468 100644 --- a/www/%username/index.html.spt +++ b/www/%username/index.html.spt @@ -38,6 +38,57 @@ communities = participant.get_communities() langs = participant.get_statement_langs() +patron_visibilities = participant.recipient_settings.patron_visibilities +if patron_visibilities > 1: + n_public_patrons = website.db.one(""" + SELECT count(*) + FROM current_tips tip + WHERE tip.tippee = %s + AND tip.visibility = 3 + AND tip.paid_in_advance > 0 + AND tip.renewal_mode > 0 + """, (participant.id,), max_age=5) + public_patrons = website.db.all(""" + SELECT tipper_p.id + , tipper_p.avatar_url + , coalesce(tipper_p.public_name, tipper_p.username) AS name + , tipper_p.hide_giving + , tip.amount AS weekly_amount + FROM current_tips tip + JOIN participants tipper_p ON tipper_p.id = tip.tipper + WHERE tip.tippee = %s + AND tip.visibility = 3 + AND tip.paid_in_advance > 0 + AND tip.renewal_mode > 0 + AND tipper_p.hide_from_lists = 0 + ORDER BY convert(tip.amount, 'EUR') * random()::numeric DESC + LIMIT 12 + """, (participant.id,), max_age=5) if n_public_patrons else () + +n_public_donees = website.db.one(""" + SELECT count(*) + FROM current_tips tip + WHERE tip.tipper = %s + AND tip.visibility = 3 + AND tip.paid_in_advance > 0 + AND tip.renewal_mode > 0 +""", (participant.id,), max_age=5) +public_donees = website.db.all(""" + SELECT tippee_p.id + , tippee_p.avatar_url + , coalesce(tippee_p.username, tippee_p.public_name) AS name + , tippee_p.hide_receiving + , tip.amount AS weekly_amount + FROM current_tips tip + JOIN participants tippee_p ON tippee_p.id = tip.tippee + WHERE tip.tipper = %s + AND tip.visibility = 3 + AND tip.paid_in_advance > 0 + AND tip.renewal_mode > 0 + ORDER BY convert(tip.amount, 'EUR') * random()::numeric DESC + LIMIT 12 +""", (participant.id,), max_age=5) if n_public_donees else () + show_income = not participant.hide_receiving and participant.accepts_tips [-----------------------------------------------------------------------------] @@ -169,6 +220,70 @@ show_income = not participant.hide_receiving and participant.accepts_tips % endif % endif + % if patron_visibilities > 1 and n_public_patrons +

    {{ _("Patrons") }} +   + {{ fontawesome('download', _("Export as CSV")) }} +

    + +

    {{ ngettext( + "{username} has {n} public patron.", + "{username} has {n} public patrons.", + username=participant.username, + n=n_public_patrons, + ) }}

    + +
    + % for p in public_patrons +
    + + {{ avatar_img(p) }} +
    {{ p.name }}
    + % if not p.hide_giving and not participant.hide_receiving +
    {{ _( + "{money_amount}{small}/week{end_small}", + money_amount=p.weekly_amount, + small=''|safe, end_small=''|safe + ) }}
    + % endif +
    +
    + % endfor +
    + % endif + + % if n_public_donees +

    {{ _("Donees") }} +   + {{ fontawesome('download', _("Export as CSV")) }} +

    + +

    {{ ngettext( + "{username} donates publicly to {n} creator.", + "{username} donates publicly to {n} creators.", + username=participant.username, + n=n_public_donees, + ) }}

    + +
    + % for p in public_donees +
    + + {{ avatar_img(p) }} +
    {{ p.name }}
    + % if not p.hide_receiving and not participant.hide_giving +
    {{ _( + "{money_amount}{small}/week{end_small}", + money_amount=p.weekly_amount, + small=''|safe, end_small=''|safe + ) }}
    + % endif +
    +
    + % endfor +
    + % endif +

    {{ _("History") }}

    {{ _( diff --git a/www/%username/ledger/index.spt b/www/%username/ledger/index.spt index d10134026a..0fdaab620c 100644 --- a/www/%username/ledger/index.spt +++ b/www/%username/ledger/index.spt @@ -16,8 +16,8 @@ participant = get_participant(state, restrict=True) title = participant.username subhead = _("Ledger") user_is_admin = user.is_acting_as('admin') -subpath = 'ledger/' if user_is_admin else '' admin_override = user_is_admin and (participant != user or 'override' in request.qs) +subpath = 'ledger/' if admin_override else '' translated_status = { None: '', 'pre': _('preparing'), @@ -179,25 +179,59 @@ if participant.join_time: % set context = event['context'] + % set visibility = event['visibility'] + % if visibility > 1 or admin_override + % set payer_link = ('{0}'|safe).format(event['payer_username'], subpath) + % else + % set payer_link = '' + % endif % if 'payer' in event % if context == 'personal-donation' - {{ _("anonymous donation") }} + {{ _( + "public donation from {donor_name}", donor_name=payer_link + ) if visibility == 3 else _( + "private donation from {donor_name}", donor_name=payer_link + ) if visibility == 2 else _( + "secret donation" + ) }} % elif context == 'team-donation' - {{ _("anonymous donation for your role in the {0} team", - ('{0}'|safe).format(event['team_name'])) }} + % set team_link = ('{0}'|safe).format(event['team_name']) + {{ _( + "public donation from {donor_name} for your role in the {team_name} team", + donor_name=payer_link, team_name=team_link + ) if visibility == 3 else _( + "private donation from {donor_name} for your role in the {team_name} team", + donor_name=payer_link, team_name=team_link + ) if visibility == 2 else _( + "secret donation for your role in the {team_name} team", + team_name=team_link + ) }} % endif - % if admin_override - - (from {{ event['payer_username'] }}) - + % if admin_override and visibility < 2 + (from {{ payer_link }}) % endif % else - % set to = ('{0}'|safe).format(event['recipient_username']) + % set to = ('{0}'|safe).format(event['recipient_username'], subpath) % if context == 'personal-donation' - {{ _("donation to {0}", to) }} + {{ _( + "public donation to {recipient_name}", recipient_name=to + ) if visibility == 3 else _( + "private donation to {recipient_name}", recipient_name=to + ) if visibility == 2 else _( + "secret donation to {recipient_name}", recipient_name=to + ) }} % elif context == 'team-donation' - {{ _("donation to {0} for their role in the {1} team", to, - ('{0}'|safe).format(event['team_name'])) }} + % set team_link = ('{0}'|safe).format(event['team_name']) + {{ _( + "public donation to {recipient_name} for their role in the {team_name} team", + recipient_name=to, team_name=team_link + ) if visibility == 3 else _( + "private donation to {recipient_name} for their role in the {team_name} team", + recipient_name=to, team_name=team_link + ) if visibility == 2 else _( + "secret donation to {recipient_name} for their role in the {team_name} team", + recipient_name=to, team_name=team_link + ) }} % endif % endif % if event['unit_amount'] diff --git a/www/%username/patrons/export.spt b/www/%username/patrons/export.spt new file mode 100644 index 0000000000..badd7d0c1d --- /dev/null +++ b/www/%username/patrons/export.spt @@ -0,0 +1,103 @@ +from pando.utils import utcnow + +from liberapay.utils import get_participant + +[---] + +participant = get_participant(state, restrict=True, allow_member=True) +if user != participant and user.recipient_settings.patron_visibilities < 2: + raise response.error(403, "You haven't opted-in to see who your patrons are.") + +today = utcnow().date() +scope = request.qs.get_choice('scope', ('active', 'all')) +if scope == 'active': + patrons = website.db.all(""" + SELECT (CASE WHEN tip.visibility > 1 THEN tip.tipper::text ELSE '' END) AS patron_id + , (CASE WHEN tip.visibility > 1 THEN coalesce(tipper_p.username, '') ELSE '' END) AS patron_username + , (CASE WHEN tip.visibility > 1 THEN coalesce(tipper_p.public_name, '') ELSE '' END) AS patron_public_name + , ( CASE WHEN tip.visibility = 3 THEN 'public' + WHEN tip.visibility = 2 THEN 'private' + ELSE 'secret' + END + ) AS visibility + , (tip.amount).currency AS donation_currency + , (tip.amount).amount AS weekly_amount + , tip.ctime::date AS pledge_date + , tip.mtime::date AS last_modification_date + , ( SELECT pt.ctime::date + FROM payin_transfers pt + WHERE pt.payer = tip.tipper + AND coalesce(pt.team, pt.recipient) = tip.tippee + AND pt.status = 'succeeded' + ORDER BY pt.ctime + LIMIT 1 + ) AS first_payment_date + , tipper_p.avatar_url AS patron_avatar_url + FROM current_tips tip + JOIN participants tipper_p ON tipper_p.id = tip.tipper + WHERE tip.tippee = %s + AND tip.paid_in_advance > 0 + AND tipper_p.is_suspended IS NOT true + ORDER BY tip.ctime, tip.id + """, (participant.id,)) + response.headers[b'Content-Disposition'] = ( + "attachment; filename*=UTF-8''liberapay-active-patrons-%s-%s.csv" % + (participant.username, today) + ).encode('utf8') +elif scope == 'all': + patrons = website.db.all(""" + SELECT (CASE WHEN tip.visibility > 1 THEN tip.tipper::text ELSE '' END) AS patron_id + , (CASE WHEN tip.visibility > 1 THEN coalesce(tipper_p.username, '') ELSE '' END) AS patron_username + , (CASE WHEN tip.visibility > 1 THEN coalesce(tipper_p.public_name, '') ELSE '' END) AS patron_public_name + , ( CASE WHEN tip.visibility = 3 THEN 'public' + WHEN tip.visibility = 2 THEN 'private' + ELSE 'secret' + END + ) AS visibility + , (tip.amount).currency AS donation_currency + , (tip.amount).amount AS weekly_amount + , tip.ctime::date AS pledge_date + , tip.mtime::date AS last_modification_date + , ( SELECT pt.ctime::date + FROM payin_transfers pt + WHERE pt.payer = tip.tipper + AND coalesce(pt.team, pt.recipient) = tip.tippee + ORDER BY pt.ctime + LIMIT 1 + ) AS first_payment_date + , ( SELECT pt.ctime::date + FROM payin_transfers pt + WHERE pt.payer = tip.tipper + AND coalesce(pt.team, pt.recipient) = tip.tippee + AND pt.status = 'succeeded' + ORDER BY pt.ctime DESC + LIMIT 1 + ) AS last_payment_date + , ( SELECT count(*) + FROM transfers tr + WHERE tr.tipper = tip.tipper + AND coalesce(tr.team, tr.tippee) = tip.tippee + AND tr.context IN ('tip', 'take') + AND tr.status = 'succeeded' + ) AS number_of_weeks_active + , ( SELECT (sum(pt.amount, tip.amount::currency)).amount + FROM payin_transfers pt + WHERE pt.payer = tip.tipper + AND coalesce(pt.team, pt.recipient) = tip.tippee + AND pt.status = 'succeeded' + ) AS sum_received + , tipper_p.avatar_url AS patron_avatar_url + FROM current_tips tip + JOIN participants tipper_p ON tipper_p.id = tip.tipper + WHERE tip.tippee = %s + AND tip.paid_in_advance IS NOT NULL + AND tipper_p.is_suspended IS NOT true + ORDER BY tip.ctime, tip.id + """, (participant.id,)) + response.headers[b'Content-Disposition'] = ( + "attachment; filename*=UTF-8''liberapay-patrons-%s-%s.csv" % + (participant.username, today) + ).encode('utf8') + +[---] text/csv via csv_dump +patrons diff --git a/www/%username/patrons/index.spt b/www/%username/patrons/index.spt new file mode 100644 index 0000000000..f92d4b4481 --- /dev/null +++ b/www/%username/patrons/index.spt @@ -0,0 +1,111 @@ +from liberapay.utils import form_post_success, get_participant + +[---] + +participant = get_participant(state, restrict=True, allow_member=True) +if user != participant and not user.recipient_settings.patron_visibilities < 2: + raise response.error(403, "You haven't opted-in to see who your patrons are.") + +if request.method == 'POST': + see_patrons = request.body.parse_boolean('see_patrons') + participant.update_recipient_settings(patron_visibilities=(7 if see_patrons else 1)) + form_post_success(state) + +title = participant.username +subhead = _("Patrons") + +[---] text/html +% extends "templates/layouts/settings.html" + +% from "templates/macros/icons.html" import fontawesome, glyphicon with context + +% block content + +% set p = participant +% if participant == user +

    {{ ngettext( + "You have {n} active patron giving you {money_amount} per week.", + "You have {n} active patrons giving you a total of {money_amount} per week.", + p.npatrons + p.nteampatrons, money_amount=p.receiving + ) if p.receiving else _( + "You don't have any active patrons." + ) }}

    +% else +

    {{ ngettext( + "{0} receives {1} per week from {n} patron.", + "{0} receives {1} per week from {n} patrons.", + p.npatrons + p.nteampatrons, p.username, p.receiving + ) if p.receiving else _( + "{username} doesn't have any active patrons.", + username=participant.username + ) }}

    +% endif + +% set patron_visibilities = participant.recipient_settings.patron_visibilities +
    + + % if patron_visibilities == 0 +

    {{ glyphicon('info-sign') }} {{ _( + "Liberapay now supports non-anonymous donations, do you want to know " + "who your patrons are?" + ) }}

    +

    + +    + +

    + % elif patron_visibilities > 1 +

    {{ _( + "You have opted-in to see who your patrons are. If you change your mind, " + "then {link_start}click here to disable non-anonymous donations{link_end}.", + link_start=''|safe + ) }}

    + % else +

    {{ _( + "You've chosen not to see who your patrons are. If you change your mind, " + "then {link_start}click here to enable non-anonymous donations{link_end}.", + link_start=''|safe + ) }}

    + % endif +
    + +% if patron_visibilities > 1 +

    {{ _("Data export") }}

    +

    {{ fontawesome('download') }} {{ + _("Download the list of currently active patrons") +}}

    +

    {{ fontawesome('download') }} {{ + _("Download the record of all patrons in the last ten years") +}}

    + +% if participant.is_person +

    {{ _("Teams") }}

    + % if teams + % for team in teams +

    {{ team.username }}

    +

    {{ ngettext( + "{0} receives {1} per week from {n} patron.", + "{0} receives {1} per week from {n} patrons.", + team.npatrons + team.nteampatrons, + '%s'|safe % (team.path(''), team.username), + team.receiving + ) }}

    +

    {{ + _("View the patrons of {username}", username=team.username) + }}

    + % endfor + % else +

    {{ _( + "You are not a member of any team." + ) if participant == user else _( + "{username} isn't a member of any team.", username=participant.username + ) }}

    + % endif +% endif +% endif + +% endblock diff --git a/www/%username/patrons/public.spt b/www/%username/patrons/public.spt new file mode 100644 index 0000000000..5889086023 --- /dev/null +++ b/www/%username/patrons/public.spt @@ -0,0 +1,34 @@ +from pando.utils import utcnow + +from liberapay.utils import get_participant + +[---] + +participant = get_participant(state, restrict=False) + +public_patrons = website.db.all(""" + SELECT tip.ctime::date AS pledge_date + , tipper_p.id AS patron_id + , tipper_p.username AS patron_username + , coalesce(tipper_p.public_name, '') AS patron_public_name + , (tip.amount).currency AS donation_currency + , ( CASE WHEN tipper_p.hide_giving THEN 'private' + ELSE (tip.amount).amount::text + END ) AS weekly_amount + , tipper_p.avatar_url AS patron_avatar_url + FROM current_tips tip + JOIN participants tipper_p ON tipper_p.id = tip.tipper + WHERE tip.tippee = %s + AND tip.visibility = 3 + AND tip.paid_in_advance > 0 + AND tip.renewal_mode > 0 + ORDER BY tip.ctime, tip.id +""", (participant.id,)) + +response.headers[b'Content-Disposition'] = ( + "attachment; filename*=UTF-8''liberapay-public-patrons-of-%s-%s.csv" % + (participant.username, utcnow().date()) +).encode('utf8') + +[---] text/csv via csv_dump +public_patrons diff --git a/www/%username/receiving/index.html.spt b/www/%username/receiving/index.html.spt index f07703d525..c88f106ad5 100644 --- a/www/%username/receiving/index.html.spt +++ b/www/%username/receiving/index.html.spt @@ -22,7 +22,7 @@ recent_donation_changes = website.db.all(""" FROM tips t WHERE t.tippee = %(p_id)s AND t.mtime > current_timestamp - interval '30 days' - AND t.hidden IS NOT true + AND t.visibility > 0 ORDER BY t.mtime DESC, t.tipper LIMIT 30 ) t diff --git a/www/%username/tip.spt b/www/%username/tip.spt index 1d3b8ba470..42a78b13de 100644 --- a/www/%username/tip.spt +++ b/www/%username/tip.spt @@ -70,7 +70,8 @@ if request.method == 'POST': else: raise response.error(400, _("The donation amount is missing.")) renewal_mode = request.body.get_int('renewal_mode', default=1, minimum=1, maximum=2) - out = tipper.set_tip_to(tippee, amount, period, renewal_mode=renewal_mode) + visibility = request.body.get_int('visibility', default=1, minimum=1, maximum=3) + out = tipper.set_tip_to(tippee, amount, period, renewal_mode=renewal_mode, visibility=visibility) if not out: raise response.error(400, _("This donation doesn't exist or has already been stopped.")) if out['renewal_mode'] == 0: diff --git a/www/about/index.spt b/www/about/index.spt index 414f11d965..d02c1945b3 100644 --- a/www/about/index.spt +++ b/www/about/index.spt @@ -11,9 +11,9 @@ title = _("Introduction")

    {{ _("Liberapay is a way to donate money recurrently to people whose work you appreciate.") }}

    {{ _( - "Payments come with no strings attached. You don't know exactly who is " - "giving to you, and donations are capped at {0} per week per donor to " - "dampen undue influence." + "Payments come with no strings attached. By default, recipients don't " + "know who their patrons are, and donations are capped at {0} per week " + "per donor to dampen undue influence." , constants.DONATION_LIMITS[currency]['weekly'][1] ) }}

    diff --git a/www/index.html.spt b/www/index.html.spt index a8444f05d7..7a19589731 100644 --- a/www/index.html.spt +++ b/www/index.html.spt @@ -344,7 +344,11 @@ recent = website.db.one("""
    {{ avatar_img(p) }}
    {{ p.username }}
    -
    {{ locale.format_money(p.giving) }}
    +
    {{ _( + "{money_amount}{small}/week{end_small}", + money_amount=p.giving, + small=''|safe, end_small=''|safe + ) }}
    % endfor