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

Improve the handling of “unsettling” profiles (a.k.a. Not Safe For Work) #2098

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions js/10-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Liberapay.init = function() {
Liberapay.payments.init();
Liberapay.s3_uploader_init();
Liberapay.stripe_init();
Liberapay.view_unsettling.init();

$('div[href]').css('cursor', 'pointer').on('click auxclick', function(event) {
if (event.target.tagName == 'A') {
Expand Down
22 changes: 22 additions & 0 deletions js/viewUnsettling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Liberapay.view_unsettling = {};

Liberapay.view_unsettling.init = function () {
Liberapay.view_unsettling.once();
Liberapay.view_unsettling.opt_in();
};

Liberapay.view_unsettling.once = function () {
$(".display-unsettling-once").click(function () {
$(this).remove();
$("#unsettling-content-display").remove();
});
};

Liberapay.view_unsettling.opt_in = function () {
$(".display-unsettling-opt-in").click(function () {
$(this).remove();
$("#unsettling-content-display").remove();

document.cookie = 'always_view_unsettling=True;domain=;path=/';
});
};
9 changes: 5 additions & 4 deletions liberapay/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,14 +379,15 @@ def __missing__(self, currency):
'country', 'region', 'city', 'postal_code', 'local_address'
)

PRIVACY_FIELDS = OrderedDict([
('hide_giving', (_("Do not publish the amounts of money I send."), False)),
('hide_receiving', (_("Do not publish the amounts of money I receive."), False)),
PROFILE_VISIBILITY_FIELDS = OrderedDict([
('hide_giving', (_("Hide total giving from others."), False)),
('hide_receiving', (_("Hide total receiving from others."), 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)),
('is_unsettling', (_("Mark this profile as displaying content that is upsetting or embarrassing."), True)),
])
PRIVACY_FIELDS_S = ' '.join(PRIVACY_FIELDS.keys())
PROFILE_VISIBILITY_FIELDS_S = ' '.join(PROFILE_VISIBILITY_FIELDS.keys())

PRIVILEGES = dict(admin=1, run_payday=2)
check_bits(list(PRIVILEGES.values()))
Expand Down
2 changes: 1 addition & 1 deletion liberapay/models/_mixin_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def get_current_takes_for_display(self, cursor=None):
assert self.kind == 'group'
TAKES = """
SELECT p.id AS member_id, p.username AS member_name, p.avatar_url
, p.is_suspended
, p.is_suspended, p.is_unsettling
, t.amount, t.actual_amount, t.ctime, t.mtime
FROM current_takes t
JOIN participants p ON p.id = member
Expand Down
27 changes: 27 additions & 0 deletions style/base/base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,11 @@ a.account-link[href="#not-available"] {
}
}

.panel-blur {
filter: blur(4px);
-webkit-filter: blur(4px);
}

.profile-header {
align-items: center;
clear: both;
Expand Down Expand Up @@ -375,6 +380,18 @@ p.summary {
}
}

<<<<<<< HEAD
=======
.repo-search-blur {
filter: blur(3px);
-webkit-filter: blur(3px);
}

>>>>>>> 2faaf781829831874e7d681daec0a7ce2368daca
.repo-search-display {
display: inline;
}

@media (max-width: $screen-xs-min - 1) {
.inline-boxes > .inline-box {
display: block;
Expand Down Expand Up @@ -1145,3 +1162,13 @@ abbr[title] {
/* Bootstrap 3.3.6 adds a bottom border but doesn't disable text-decoration */
text-decoration: none;
}

.blur-3px {
filter: blur(3px);
-webkit-filter: blur(3px);
}

.blur-4px {
filter: blur(4px);
-webkit-filter: blur(4px);
}
46 changes: 33 additions & 13 deletions templates/layouts/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -96,21 +96,41 @@
</nav>

<div id="main">
% block main
{% block before_container %}{% endblock %}
<div class="container">
% block before_content
% block heading
% if title
<h1 class="main-title {{ 'hidden-xs hidden-sm' if subhead else '' }}">{{ title }}</h1>
% endif
% if participant and participant.id != user.id
% set is_unsettling = participant.is_unsettling
% set hide_profile = is_unsettling and 'always_view_unsettling' not in request.headers.cookie
% endif

% if hide_profile
<div id="unsettling-content-display">
<h3 class="text-center">{{ _("This page is marked as containing potentially upsetting or embarrassing content. Viewing it is unrecommended if you are a minor. Would you still like to view it?") }}</h3>

<div class="text-center">
<button class="btn btn-warning display-unsettling-once" type="button" data-toggle="collapse" data-target="#unsettling-content">{{ _("View this page") }}</button>
<button class="btn btn-warning display-unsettling-opt-in" type="button" data-toggle="collapse" data-target="#unsettling-content">{{ _("Always view unsettling pages (in this browser)") }}</button>
</div>
</div>

<div class="collapse" id="unsettling-content">
% block main
{% block before_container %}{% endblock %}
<div class="container">
% block before_content
% block heading
% if title
<h1 class="main-title {{ 'hidden-xs hidden-sm' if subhead else '' }}">{{ title }}</h1>
% endif
% endblock
<div id="subnav">{% block subnav %}{% endblock %}</div>
% endblock
{% block content %}{% endblock %}
{% block after_content %}{% endblock %}
</div>
% endblock
<div id="subnav">{% block subnav %}{% endblock %}</div>
% endblock
{% block content %}{% endblock %}
{% block after_content %}{% endblock %}
</div>
% endblock
% else
{{ self.main() }}
% endif
</div>

<footer class="container" role="navigation">
Expand Down
2 changes: 1 addition & 1 deletion templates/macros/nav.html
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
('/repositories', _("Repositories")),
('/teams', _("Teams")),
('/communities', _("Communities")),
('/privacy', _("Privacy"))
('/visibility', _("Visibility"))
], base=participant.path('edit')),
'togglable': True,
}),
Expand Down
4 changes: 3 additions & 1 deletion templates/macros/profile-box.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
% from 'templates/macros/elsewhere.html' import platform_icon

% macro profile_box_embedded_wrapper(participant, path, style='default')
% set blur = participant.is_unsettling and 'always_view_unsettling' not in request.headers.cookie
<div class="inline-box">
<div class="panel panel-{{ style }} profile-box-embedded"
href="{{ path }}">
<div class="panel-body">
<div class="panel-body {{ 'blur-4px' if blur and user.id != participant.id }}">
<a href="{{ path }}" class="avatar-inline">{{
avatar_img(participant, size=120)
}}</a>
Expand All @@ -23,6 +24,7 @@

% macro profile_box_embedded_participant(participant, summary, nmembers=None)
% set username = participant.username
% set unsettling = participant.is_unsettling
longj724 marked this conversation as resolved.
Show resolved Hide resolved

<h4><a href="/{{ username }}/">{{ username }}</a></h4>

Expand Down
4 changes: 3 additions & 1 deletion templates/macros/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@

% macro members_listing(participant)
% from "templates/macros/avatar-url.html" import avatar_img with context

<div>
% for t in participant.get_current_takes_for_display()
<div class="mini-user">
% set blur = t.is_unsettling and 'always_view_unsettling' not in request.headers.cookie
<div class="mini-user {{ 'blur-3px' if blur and t.id != user.id }}">
<a href="/{{ t.member_name }}/">
{{ avatar_img(t) }}
<span class="name">{{ t.member_name }}</span>
Expand Down
4 changes: 2 additions & 2 deletions tests/py/test_charts_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ def test_anonymous_receiver(self):
self.run_payday()
self.run_payday()
r = self.client.PxST(
'/carl/edit/privacy',
{'privacy': 'hide_receiving', 'hide_receiving': 'on'},
'/carl/edit/visibility',
{'visibility': 'hide_receiving', 'hide_receiving': 'on'},
auth_as=self.carl,
)
assert r.code == 302
Expand Down
8 changes: 4 additions & 4 deletions tests/py/test_public_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ def test_giving_and_receiving(self):

# Hide alice's giving
r = self.client.PxST(
'/alice/edit/privacy',
{'privacy': 'hide_giving', 'hide_giving': 'on'},
'/alice/edit/visibility',
{'visibility': 'hide_giving', 'hide_giving': 'on'},
auth_as=self.alice,
)
assert r.code == 302
Expand All @@ -41,8 +41,8 @@ def test_giving_and_receiving(self):

# Hide alice's receiving
r = self.client.PxST(
'/alice/edit/privacy',
{'privacy': 'hide_receiving', 'hide_receiving': 'on'},
'/alice/edit/visibility',
{'visibility': 'hide_receiving', 'hide_receiving': 'on'},
auth_as=self.alice,
)
assert r.code == 302
Expand Down
72 changes: 55 additions & 17 deletions tests/py/test_settings.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from liberapay.constants import PRIVACY_FIELDS, PRIVACY_FIELDS_S
from liberapay.constants import PROFILE_VISIBILITY_FIELDS, PROFILE_VISIBILITY_FIELDS_S
from liberapay.testing import Harness
from liberapay.models.participant import Participant


ALL_OFF = {'privacy': PRIVACY_FIELDS_S}
ALL_ON = dict({k: 'on' for k in PRIVACY_FIELDS}, **ALL_OFF)
ALL_OFF = {'visibility': PROFILE_VISIBILITY_FIELDS_S}
ALL_ON = dict({k: 'on' for k in PROFILE_VISIBILITY_FIELDS}, **ALL_OFF)


class TestPrivacy(Harness):
Expand All @@ -14,24 +14,11 @@ def setUp(self):
self.alice = self.make_participant('alice')

def hit_edit(self, expected_code=302, **kw):
response = self.client.PxST("/alice/edit/privacy", auth_as=self.alice, **kw)
response = self.client.PxST("/alice/edit/visibility", auth_as=self.alice, **kw)
if response.code != expected_code:
print(response.text)
return response

def test_participant_can_modify_privacy_settings(self):
# turn them all on
self.hit_edit(data=ALL_ON)
alice = Participant.from_id(self.alice.id)
for k in PRIVACY_FIELDS:
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) in (0, 2, False)

# Related to is-searchable

def test_meta_robots_tag_added_on_opt_out(self):
Expand All @@ -50,6 +37,57 @@ def test_team_participant_doesnt_show_up_on_explore_teams(self):
assert 'A-Team' not in self.client.GET("/explore/teams/").text


class TestProfileVisibility(Harness):

def setUp(self):
Harness.setUp(self)
self.alice = self.make_participant('alice')
self.view_unsettling_prompt = "This page is marked as containing potentially upsetting or embarrassing content."

def hit_edit(self, expected_code=302, **kw):
response = self.client.PxST("/alice/edit/visibility", auth_as=self.alice, **kw)
if response.code != expected_code:
print(response.text)
return response

def test_participant_can_modify_visibility_settings(self):
# turn them all on
self.hit_edit(data=ALL_ON)
alice = Participant.from_id(self.alice.id)
for k in PROFILE_VISIBILITY_FIELDS:
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 PROFILE_VISIBILITY_FIELDS:
assert getattr(alice, k) in (0, 2, False)

def test_unsettling_participant_blurred_search(self):
self.make_participant('bob', is_unsettling=1)
bob_search_result = self.client.GET("/search?q=bob").text
assert '<div class="mini-user blur-3px">' in bob_search_result

def test_unsettling_team_blurred(self):
alice = Participant.from_username('alice')
self.make_participant('A-Team', kind="group", is_unsettling=1).add_member(alice)
response = self.client.GET("/explore/teams")
explore_page = response.text
cookies = response.headers.cookie
assert 'always_view_unsettling' not in cookies
assert '<div class="panel-body blur-4px">' in explore_page

def test_participant_view_unsettling_prompt(self):
self.make_participant('bob', is_unsettling=1)
bobs_page = self.client.GET("/bob/").text
assert self.view_unsettling_prompt in bobs_page

def test_participant_always_view_unsettling_cookie_set(self):
self.make_participant('bob', is_unsettling=1)
bob_search_result = self.client.GET("/search?q=bob", cookies={'always_view_unsettling': 'True'}).text
assert self.view_unsettling_prompt not in bob_search_result


class TestUsername(Harness):

def test_participant_can_set_username(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from liberapay.constants import PRIVACY_FIELDS
from liberapay.constants import PROFILE_VISIBILITY_FIELDS
from liberapay.utils import form_post_success, get_participant

[---]
participant = get_participant(state, restrict=True, allow_member=True)

if request.method == 'POST':
fields = request.body['privacy'].split()
fields = request.body['visibility'].split()
for field in fields:
if field not in PRIVACY_FIELDS:
if field not in PROFILE_VISIBILITY_FIELDS:
continue
if not (PRIVACY_FIELDS[field][1] or participant.is_person):
if not (PROFILE_VISIBILITY_FIELDS[field][1] or participant.is_person):
continue
value = request.body.get(field) == 'on'
if isinstance(getattr(participant, field), bool):
Expand All @@ -20,10 +20,10 @@ if request.method == 'POST':
""".format(field), (value, participant.id))
else:
participant.update_bit(field, 1, value)
form_post_success(state, msg=_("Your privacy settings have been changed."))
form_post_success(state, msg=_("Your visibility settings have been changed."))

title = participant.username
subhead = _("Privacy")
subhead = _("Visibility")

[---] text/html
% extends "templates/layouts/profile-edit.html"
Expand All @@ -33,10 +33,10 @@ subhead = _("Privacy")
<form action="" method="POST" class="js-submit">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
<input type="hidden" name="back_to" value="{{ request.path.raw }}" />
<input type="hidden" name="privacy" value="{{ constants.PRIVACY_FIELDS_S }}" />
<input type="hidden" name="visibility" value="{{ constants.PROFILE_VISIBILITY_FIELDS_S }}" />
<div class="checkbox">
% set has_override = set()
% for name, (label, show_for_teams) in constants.PRIVACY_FIELDS.items()
% for name, (label, show_for_teams) in constants.PROFILE_VISIBILITY_FIELDS.items()
% if show_for_teams or participant.kind != 'group'
<label>
<input type="checkbox" name="{{ name }}" {{ 'checked' if participant[name].__and__(1) else '' }} />
Expand Down
1 change: 1 addition & 0 deletions www/%username/index.html.spt
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public_donees = website.db.all("""

show_income = not participant.hide_receiving and participant.accepts_tips


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This extraneous blank line should be removed.

[-----------------------------------------------------------------------------]
% extends "templates/layouts/profile.html"

Expand Down
Loading