Skip to content

Commit

Permalink
Merge pull request #506 from liberapay/control-referencing
Browse files Browse the repository at this point in the history
Better control referencing
  • Loading branch information
Changaco authored Jan 18, 2017
2 parents cdc4d2f + 9133478 commit 3f4e92a
Show file tree
Hide file tree
Showing 18 changed files with 225 additions and 31 deletions.
13 changes: 12 additions & 1 deletion js/forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,26 @@ Liberapay.forms.jsSubmit = function() {
function submit(e) {
e.preventDefault();
var $form = $(this.form || this);
var target = $form.attr('action');
var js_only = target == 'javascript:';
var data = $form.serializeArray();
if (js_only) {
// workaround for http://stackoverflow.com/q/11424037/2729778
$form.find('input[type="checkbox"]').each(function () {
var $input = $(this);
if (!$input.prop('checked')) {
data.push({name: $input.attr('name'), value: 'off'});
}
});
}
var button = this.tagName == 'BUTTON' ? this : null;
if (this.tagName == 'BUTTON') {
data.push({name: this.name, value: this.value});
}
var $inputs = $form.find(':not(:disabled)');
$inputs.prop('disabled', true);
jQuery.ajax({
url: $form.attr('action'),
url: js_only ? '' : target,
type: 'POST',
data: data,
dataType: 'json',
Expand Down
20 changes: 20 additions & 0 deletions liberapay/models/participant.py
Original file line number Diff line number Diff line change
Expand Up @@ -1783,6 +1783,26 @@ def controls(self, other):
other.kind == 'group' and self.member_of(other)
)

def update_bit(self, column, bit, on):
"""Updates one bit in an integer in the participants table.
Bits are used for email notification preferences and privacy settings.
"""
assert isinstance(getattr(self, column), int) # anti sql injection
if on:
mask = bit
op = '|'
else:
mask = 2147483647 ^ bit
op = '&'
r = self.db.one("""
UPDATE participants
SET {column} = {column} {op} %s
WHERE id = %s
RETURNING {column}
""".format(column=column, op=op), (mask, self.id))
self.set_attributes(**{column: r})


class NeedConfirmation(Exception):
"""Represent the case where we need user confirmation during a merge.
Expand Down
9 changes: 8 additions & 1 deletion liberapay/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,15 @@ def get_participant(state, restrict=True, redirect_stub=True, allow_member=False
if participant is None:
from liberapay.models.participant import Participant # avoid circular import
participant = Participant._from_thing(thing, value) if value else None
if participant is None or participant.kind == 'community':
if participant is None:
raise response.error(404)
elif participant.kind == 'community':
c_name = Participant.db.one("""
SELECT name
FROM communities
WHERE participant = %s
""", (participant.id,))
raise response.redirect('/for/%s' % c_name)

if redirect_canon and request.method in ('GET', 'HEAD'):
if slug != participant.username:
Expand Down
42 changes: 42 additions & 0 deletions sql/branch.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
BEGIN;

LOCK TABLE participants IN EXCLUSIVE MODE;

DROP VIEW sponsors;

ALTER TABLE participants
ALTER COLUMN profile_noindex DROP DEFAULT,
ALTER COLUMN profile_noindex SET DATA TYPE int USING (profile_noindex::int | 2),
ALTER COLUMN profile_noindex SET DEFAULT 2;

ALTER TABLE participants
ALTER COLUMN hide_from_lists DROP DEFAULT,
ALTER COLUMN hide_from_lists SET DATA TYPE int USING (hide_from_lists::int),
ALTER COLUMN hide_from_lists SET DEFAULT 0;

ALTER TABLE participants
ALTER COLUMN hide_from_search DROP DEFAULT,
ALTER COLUMN hide_from_search SET DATA TYPE int USING (hide_from_search::int),
ALTER COLUMN hide_from_search SET DEFAULT 0;

LOCK TABLE communities IN EXCLUSIVE MODE;
UPDATE participants p
SET hide_from_lists = c.is_hidden::int
FROM communities c
WHERE c.participant = p.id;
ALTER TABLE communities DROP COLUMN is_hidden;

CREATE OR REPLACE VIEW sponsors AS
SELECT *
FROM participants p
WHERE status = 'active'
AND kind = 'organization'
AND giving > receiving
AND giving >= 10
AND hide_from_lists = 0
AND profile_noindex = 0
;

END;

UPDATE participants SET profile_nofollow = true;
17 changes: 17 additions & 0 deletions style/base/base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,23 @@ li.mini-user {
}
}

.user-admin {
border-bottom: 1px solid #ddd;
padding-bottom: 1em;
}
.mini-user-admin {
border-right: 1px solid #ddd;
text-align: center;
img.avatar {
max-width: 110px;
}
.name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}

#subnav + p, #subnav + div.paragraph {
margin-top: 1.5em;
}
Expand Down
10 changes: 9 additions & 1 deletion templates/privacy-form.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@
<input type="hidden" name="back_to" value="{{ request.path.raw }}" />
<input type="hidden" name="privacy" value="{{ constants.PRIVACY_FIELDS_S }}" />
<div class="checkbox">
% set has_override = set()
% for name, label in constants.PRIVACY_FIELDS.items()
<label>
<input type="checkbox" name="{{ name }}" {{ 'checked' if participant[name] else '' }} />
<input type="checkbox" name="{{ name }}" {{ 'checked' if participant[name].__and__(1) else '' }} />
{{ _(label) }}
</label>
% if participant[name] == 2
{{ has_override.add(True) or '' }}
<span class="text-warning">({{ _("Admin override is on.*") }})</span>
% endif
<br />
% endfor
</div>
<button class="btn btn-default">{{ _("Save changes") }}</button>
% if has_override
<p class="help-block">{{ _("*The referencing of Liberapay profiles is subject to admin approval.") }}</p>
% endif
</form>
% endmacro
2 changes: 1 addition & 1 deletion tests/py/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def test_get_fuzzy_match(self):
assert data[0]['username'] == 'alice'

def test_hide_from_search(self):
self.make_participant('alice', hide_from_search=True)
self.make_participant('alice', hide_from_search=1)
response = self.client.GET('/search.json?q=alice&scope=usernames')
data = json.loads(response.text)['usernames']
assert data == []
6 changes: 3 additions & 3 deletions tests/py/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ def test_participant_can_modify_privacy_settings(self):
self.hit_edit(data=ALL_ON)
alice = Participant.from_id(self.alice.id)
for k in PRIVACY_FIELDS:
assert getattr(alice, k) is True
assert getattr(alice, k) in (1, 3, True)

# turn them all off
self.hit_edit(data=ALL_OFF)
alice = Participant.from_id(self.alice.id)
for k in PRIVACY_FIELDS:
assert getattr(alice, k) is False
assert getattr(alice, k) in (0, 2, False)

# Related to is-searchable

Expand All @@ -49,7 +49,7 @@ def test_team_participant_does_show_up_on_explore_teams(self):

def test_team_participant_doesnt_show_up_on_explore_teams(self):
alice = Participant.from_username('alice')
self.make_participant('A-Team', kind='group', hide_from_lists=True).add_member(alice)
self.make_participant('A-Team', kind='group', hide_from_lists=1).add_member(alice)
assert 'A-Team' not in self.client.GET("/explore/teams/").text


Expand Down
12 changes: 1 addition & 11 deletions www/%username/emails/notifications.json.spt
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,7 @@ for field in fields:
event = EVENTS.get(field)
if not event:
continue
if body.get(field) == 'on':
mask = event.bit
op = '|'
else:
mask = 2147483647 ^ event.bit
op = '&'
website.db.run("""
UPDATE participants
SET email_notif_bits = email_notif_bits {0} %s
WHERE id = %s
""".format(op), (mask, p_id))
participant.update_bit('email_notif_bits', event.bit, body.get(field) == 'on')

if request.headers.get(b'X-Requested-With') != b'XMLHttpRequest':
response.redirect('.')
Expand Down
3 changes: 2 additions & 1 deletion www/%username/giving/index.html.spt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ npledges = len(pledges)

if not freeload:
Liberapay = Participant.from_username('Liberapay')
Liberapay_goal = getattr(Liberapay, 'goal', None)
Liberapay_tip = participant.get_tip_to(getattr(Liberapay, 'id', -1))

weekly = total - participant.receiving
Expand Down Expand Up @@ -89,7 +90,7 @@ weekly = total - participant.receiving
) }}</p>
{{ tip_form(tip=Liberapay_tip, inline=True) }}
<p> </p>
% if Liberapay and Liberapay.receiving < Liberapay.goal * Decimal('0.5')
% if Liberapay_goal and Liberapay.receiving < Liberapay_goal * Decimal('0.5')
<p>{{ _(
"Building Liberapay is a lot of work, and there still is much to "
"do, but our developers, translators, and other contributors are "
Expand Down
13 changes: 8 additions & 5 deletions www/%username/settings/edit.spt
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@ elif 'privacy' in body:
if field not in PRIVACY_FIELDS:
continue
value = body.get(field) == 'on'
website.db.run("""
UPDATE participants
SET {0} = %s
WHERE id = %s
""".format(field), (value, p.id))
if isinstance(getattr(p, field), bool):
website.db.run("""
UPDATE participants
SET {0} = %s
WHERE id = %s
""".format(field), (value, p.id))
else:
p.update_bit(field, 1, value)
out['msg'] = _("Your privacy settings have been changed.")

elif 'username' in body:
Expand Down
91 changes: 91 additions & 0 deletions www/admin/users.spt
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# coding: utf8

import json

from liberapay.exceptions import LoginRequired
from liberapay.models.participant import Participant

def parse_int(o, **kw):
try:
return int(o)
except (ValueError, TypeError):
if 'default' in kw:
return kw['default']
raise response.error(400, "'%s' is not a valid integer" % o)

REFERENCING_ATTRS = ('profile_noindex', 'hide_from_lists', 'hide_from_search')

[---]

if user.ANON:
raise LoginRequired

if not user.is_admin:
raise response.error(403)

if request.method == 'POST':
p = Participant.from_id(request.body['p_id'])
updated = 0
for attr in REFERENCING_ATTRS:
value = request.body.get(attr)
if value is None:
continue
p.update_bit(attr, 2, value == 'on')
updated += 1
raise response.success(200, json.dumps({'msg': "Done, %i bits have been updated." % updated}))

participants = website.db.all("""
SELECT p
, (SELECT c.name FROM communities c WHERE c.participant = p.id) AS c_name
FROM participants p
WHERE p.id < %s
AND (p.status <> 'stub' OR p.receiving > 0)
ORDER BY p.id DESC
LIMIT 150
""", (parse_int(request.qs.get('last_showed'), default=float('inf')),))
last_id = participants[-1][0].id if participants else 0

title = "Users Admin"

[---] text/html
% from 'templates/avatar-url.html' import avatar_img with context

% extends "templates/base.html"

% block content

<noscript><div class="alert alert-danger">{{ _("JavaScript is required") }}</div></noscript>

% for p, c_name in participants
<div class="row user-admin">
<div class="col-md-2 mini-user-admin">
<a href="/{{ p.username }}/">
<div class="name">{{ c_name if c_name else p.username }}<br>({{ p.kind }}, {{ p.status }})</div>
{{ avatar_img(p) }}
<div class="age">{{ to_age_str(p.join_time, add_direction=True) if p.join_time }}</div>
</a>
</div>
<div class="col-md-10">
<form action="javascript:" method="POST" class="js-submit">
<input type="hidden" name="p_id" value="{{ p.id }}">
% for attr in REFERENCING_ATTRS
% set value = getattr(p, attr)
<label>
<input type="checkbox" name="{{ attr }}" {{ 'checked' if value.__and__(2) }} />
{{ attr }} (user value: {{ bool(value.__and__(1)) }})
</label>
<br>
% endfor
<button class="btn btn-warning">{{ _("Save") }}</button>
</form>
<br>
</div>
</div>
<br>
% endfor

% if last_id > 1
<a class="btn btn-default btn-lg" href="?last_showed={{ last_id }}">{{ _("Next") }} →</a>
% endif

% endblock
6 changes: 4 additions & 2 deletions www/explore/communities/index.spt
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ communities_top = query_cache.all("""
LIMIT 1
) AS subtitle
FROM communities c
JOIN participants cp ON cp.id = c.participant
WHERE (nmembers > 1 OR nsubscribers > 1)
AND NOT is_hidden
AND cp.hide_from_lists = 0
ORDER BY nmembers DESC, random()
LIMIT 15
""", (' '.join(request.accept_langs),))
Expand All @@ -33,8 +34,9 @@ communities_loc = query_cache.all("""
AND s.lang = c.lang
) AS subtitle
FROM communities c
JOIN participants cp ON cp.id = c.participant
WHERE lang = %s
AND NOT is_hidden
AND cp.hide_from_lists = 0
ORDER BY nmembers DESC, random()
LIMIT 15
""", (([l for l in request.accept_langs if l in communities_langs] + [''])[0],))
Expand Down
1 change: 1 addition & 0 deletions www/explore/pledges/index.spt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pledgees = website.db.all("""
JOIN elsewhere e ON e.participant = p.id
WHERE p.status = 'stub'
AND p.receiving > 0
AND p.hide_from_lists = 0
ORDER BY ctime DESC
LIMIT 20
""")
Expand Down
2 changes: 1 addition & 1 deletion www/explore/teams/index.spt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ teams = website.db.all("""
JOIN participants p
ON p.id = t.id
WHERE (p.goal >= 0 OR p.goal IS NULL)
AND NOT p.hide_from_lists
AND p.hide_from_lists = 0
ORDER BY receiving DESC, join_time DESC

""")
Expand Down
Loading

0 comments on commit 3f4e92a

Please sign in to comment.