Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better control referencing #506

Merged
merged 8 commits into from
Jan 18, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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