• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bloomberg / pybossa / 19114618549

05 Nov 2025 07:59PM UTC coverage: 94.093% (+0.03%) from 94.065%
19114618549

Pull #1069

github

peterkle
add more tests
Pull Request #1069: RDISCROWD-8392 upgrade to boto3

214 of 223 new or added lines in 7 files covered. (95.96%)

152 existing lines in 8 files now uncovered.

17920 of 19045 relevant lines covered (94.09%)

0.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.23
/pybossa/view/account.py
1
# -*- coding: utf8 -*-
2
# This file is part of PYBOSSA.
3
#
4
# Copyright (C) 2017 Scifabric LTD.
5
#
6
# PYBOSSA is free software: you can redistribute it and/or modify
7
# it under the terms of the GNU Affero General Public License as published by
8
# the Free Software Foundation, either version 3 of the License, or
9
# (at your option) any later version.
10
#
11
# PYBOSSA is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU Affero General Public License for more details.
15
#
16
# You should have received a copy of the GNU Affero General Public License
17
# along with PYBOSSA.  If not, see <http://www.gnu.org/licenses/>.
18
"""
1✔
19
PYBOSSA Account view for web projects.
20

21
This module exports the following endpoints:
22
    * Accounts index: list of all registered users in PYBOSSA
23
    * Signin: method for signin into PYBOSSA
24
    * Signout: method for signout from PYBOSSA
25
    * Register: method for creating a new PYBOSSA account
26
    * Profile: method to manage user's profile (update data, reset password...)
27

28
"""
29
import json
1✔
30

31
from itsdangerous import BadData
1✔
32
from markdown import markdown
1✔
33

34
from flask import Blueprint, request, url_for, flash, redirect, abort, Response
1✔
35
from flask import render_template, current_app
1✔
36
from flask_login import login_required, login_user, logout_user, \
1✔
37
    current_user
38
from rq import Queue
1✔
39
from datetime import datetime
1✔
40

41
import pybossa.model as model
1✔
42
from flask_babel import gettext
1✔
43
from flask_wtf.csrf import generate_csrf
1✔
44
from flask import jsonify
1✔
45
from pybossa.core import signer, uploader, sentinel, newsletter, csrf
1✔
46
from pybossa.util import Pagination, handle_content_type, admin_required
1✔
47
from pybossa.util import admin_or_subadmin_required
1✔
48
from pybossa.util import get_user_signup_method, generate_invitation_email_for_new_user
1✔
49
from pybossa.util import generate_bsso_account_notification
1✔
50
from pybossa.util import redirect_content_type, is_own_url_or_else
1✔
51
from pybossa.util import get_avatar_url
1✔
52
from pybossa.util import can_update_user_info, url_for_app_type
1✔
53
from pybossa.cache import users as cached_users
1✔
54
from pybossa.cache.projects import get_all_projects, n_published, n_total_tasks
1✔
55
from pybossa.util import fuzzyboolean
1✔
56
from pybossa.auth import ensure_authorized_to
1✔
57
from pybossa.jobs import send_mail, export_userdata, delete_account
1✔
58
from pybossa.core import user_repo, ldap
1✔
59
from pybossa.feed import get_update_feed
1✔
60
from pybossa.messages import *
1✔
61
from pybossa.model import make_timestamp
1✔
62

63
from pybossa.forms.forms import UserPrefMetadataForm, RegisterFormWithUserPrefMetadata
1✔
64
from pybossa.forms.account_view_forms import *
1✔
65
from pybossa import otp
1✔
66
import time
1✔
67
from pybossa.sched import release_user_locks
1✔
68
from pybossa.data_access import (data_access_levels, ensure_user_data_access_assignment_from_form,
1✔
69
    copy_user_data_access_levels)
70
import pybossa.app_settings as app_settings
1✔
71
from flask import make_response
1✔
72
import six
1✔
73
import re
1✔
74

75
blueprint = Blueprint('account', __name__)
1✔
76

77
mail_queue = Queue('email', connection=sentinel.master)
1✔
78
export_queue = Queue('high', connection=sentinel.master)
1✔
79
super_queue = Queue('super', connection=sentinel.master)
1✔
80

81
MAX_BOOKMARK_NAME_LEN = 100
1✔
82
MAX_BOOKMARK_URL_LEN = 2000
1✔
83

84
@blueprint.route('/')
1✔
85
@blueprint.route('/page/<int:page>')
1✔
86
@login_required
1✔
87
def index(page=1):
1✔
88
    """Index page for all PYBOSSA registered users."""
89
    update_feed = get_update_feed()
1✔
90
    per_page = 24
1✔
91
    count = cached_users.get_total_users()
1✔
92
    accounts = cached_users.get_users_page(page, per_page)
1✔
93
    if not accounts and page != 1:
1✔
94
        abort(404)
1✔
95
    pagination = Pagination(page, per_page, count)
1✔
96
    if current_user.is_authenticated:
1✔
97
        user_id = current_user.id
1✔
98
    else:
99
        user_id = None
×
100
    top_users = cached_users.get_leaderboard(current_app.config['LEADERBOARD'],
1✔
101
                                             user_id)
102
    tmp = dict(template='account/index.html', accounts=accounts,
1✔
103
               total=count,
104
               top_users=top_users,
105
               title="Community", pagination=pagination,
106
               update_feed=update_feed)
107
    return handle_content_type(tmp)
1✔
108

109

110
@blueprint.route('/signin', methods=['GET', 'POST'])
1✔
111
def signin():
1✔
112
    """
113
    Signin method for PYBOSSA users.
114

115
    Returns a Jinja2 template with the result of signing process.
116

117
    """
118
    form = LoginForm(request.body)
1✔
119
    isLdap = current_app.config.get('LDAP_HOST', False)
1✔
120
    if (request.method == 'POST' and form.validate()
1✔
121
            and isLdap is False):
122
        password = form.password.data
1✔
123
        email_addr = form.email.data.lower()
1✔
124
        user = user_repo.search_by_email(email_addr=email_addr)
1✔
125
        if user and user.check_password(password):
1✔
126
            # Check if the user can bypass two-factor authentication.
127
            if otp.is_enabled(user.email_addr, current_app.config):
1✔
128
                # Enforce two-factor authentication.
129
                if not user.enabled:
1✔
130
                    return disable_redirect()
1✔
131
                _email_two_factor_auth(user)
1✔
132
                url_token = otp.generate_url_token(user.email_addr)
1✔
133
                next_url = is_own_url_or_else(request.args.get('next'), url_for('home.home'))
1✔
134

135
                return redirect_content_type(url_for('account.otpvalidation',
1✔
136
                                             token=url_token,
137
                                             next=next_url))
138
            else:
139
                # Bypass two-factor authentication.
140
                msg_1 = gettext('Welcome back') + ' ' + user.fullname
1✔
141
                flash(msg_1, 'success')
1✔
142
                return _sign_in_user(user)
1✔
143
        elif user:
1✔
144
            msg, method = get_user_signup_method(user)
1✔
145
            if method == 'local':
1✔
146
                msg = gettext('Ooops, Incorrect email/password')
1✔
147
                flash(msg, 'error')
1✔
148
            else:
149
                flash(msg, 'info')
1✔
150
        else:
151
            msg = gettext("Ooops, we didn't find you in the system, \
1✔
152
                          did you sign up?")
153
            flash(msg, 'info')
1✔
154

155
    if (request.method == 'POST' and form.validate()
1✔
156
            and isLdap):
157
        password = form.password.data
1✔
158
        cn = form.email.data
1✔
159
        ldap_user = None
1✔
160
        if ldap.bind_user(cn, password):
1✔
161
            ldap_user = ldap.get_object_details(cn)
1✔
162
            key = current_app.config.get('LDAP_USER_FILTER_FIELD')
1✔
163
            value = ldap_user[key][0]
1✔
164
            user_db = user_repo.get_by(ldap=value)
1✔
165
            if (user_db is None):
1✔
166
                keyfields = current_app.config.get('LDAP_PYBOSSA_FIELDS')
1✔
167
                user_data = dict(fullname=ldap_user[keyfields['fullname']][0],
1✔
168
                                 name=ldap_user[keyfields['name']][0],
169
                                 email_addr=ldap_user[keyfields['email_addr']][0],
170
                                 valid_email=True,
171
                                 ldap=value,
172
                                 consent=True)
173
                create_account(user_data, ldap_disabled=False)
1✔
174
            else:
175
                login_user(user_db, remember=True)
1✔
176
        else:
177
            msg = gettext("User LDAP credentials are wrong.")
1✔
178
            flash(msg, 'info')
1✔
179

180
    if request.method == 'POST' and not form.validate():
1✔
181
        flash(gettext('Please correct the errors'), 'error')
1✔
182
    auth = {'twitter': False, 'facebook': False, 'google': False}
1✔
183
    if current_user.is_anonymous:
1✔
184
        # If Twitter is enabled in config, show the Twitter Sign in button
185
        if (isLdap is False):
1✔
186
            if ('twitter' in current_app.blueprints):  # pragma: no cover
187
                auth['twitter'] = True
188
            if ('facebook' in current_app.blueprints):  # pragma: no cover
189
                auth['facebook'] = True
190
            if ('google' in current_app.blueprints):  # pragma: no cover
191
                auth['google'] = True
192
        next_url = is_own_url_or_else(request.args.get('next'), url_for('home.home'))
1✔
193
        response = dict(template='account/signin.html',
1✔
194
                        title="Sign in",
195
                        form=form,
196
                        auth=auth,
197
                        next=next_url)
198
        return handle_content_type(response)
1✔
199
    else:
200
        # User already signed in, so redirect to home page
201
        return redirect_content_type(url_for("home.home"))
1✔
202

203
def disable_redirect():
1✔
204
    brand = current_app.config['BRAND']
1✔
205
    flash(gettext('Your account is disabled. '
1✔
206
                'Please contact your {} administrator.'.format(brand)),
207
        'error')
208
    return redirect(url_for('home.home'))
1✔
209

210
def _sign_in_user(user, next_url=None):
1✔
211
    brand = current_app.config['BRAND']
1✔
212
    if not user:
1✔
213
        flash(gettext('There was a problem signing you in. '
1✔
214
                      'Please contact your {} administrator.'.format(brand)),
215
              'error')
216
        return redirect(url_for('home.home'))
1✔
217
    if not user.enabled:
1✔
218
        return disable_redirect()
1✔
219

220
    login_user(user, remember=False)
1✔
221
    user.last_login = model.make_timestamp()
1✔
222
    user_repo.update(user)
1✔
223
    next_url = (next_url or
1✔
224
                is_own_url_or_else(request.args.get('next'), url_for('home.home')) or
225
                url_for('home.home'))
226
    if (current_app.config.get('MAILCHIMP_API_KEY') and
1✔
227
            newsletter.ask_user_to_subscribe(user)):
228
        return redirect_content_type(url_for('account.newsletter_subscribe',
1✔
229
                                             next=next_url))
230
    return redirect_content_type(next_url)
1✔
231

232

233
def _email_two_factor_auth(user, invalid_token=False):
1✔
234
    subject = 'One time password generation details for {}'
1✔
235
    msg = dict(subject=subject.format(current_app.config.get('BRAND')),
1✔
236
               recipients=[user.email_addr])
237
    otp_code = otp.generate_otp_secret(user.email_addr)
1✔
238
    current_app.logger.debug('otp code generated before sending email: '
1✔
239
                             '{}, for email: {}'.format(otp_code,
240
                                                        user.email_addr))
241
    msg['body'] = render_template(
1✔
242
                        '/account/email/otp.md',
243
                        user=user, otpcode=otp_code)
244
    msg['html'] = render_template(
1✔
245
                        '/account/email/otp.html',
246
                        user=user, otpcode=otp_code)
247
    current_app.logger.info('sending email with otp for user %s', user.email_addr)
1✔
248
    mail_queue.enqueue(send_mail, msg)
1✔
249
    if not invalid_token:
1✔
250
        flash(gettext('an email has been sent to you with one time password'),
1✔
251
              'success')
252

253

254
@blueprint.route('/<token>/otpvalidation', methods=['GET', 'POST'])
1✔
255
def otpvalidation(token):
1✔
256
    email = otp.retrieve_email_for_token(token)
1✔
257
    # bytes to unicode string
258
    if type(email) == bytes:
1✔
259
        email = email.decode()
×
260
    if not email:
1✔
261
        flash(gettext('Please sign in.'), 'error')
1✔
262
        return redirect_content_type(url_for('account.signin'))
1✔
263
    form = OTPForm(request.body)
1✔
264
    user_otp = form.otp.data
1✔
265
    user = user_repo.get_by(email_addr=email)
1✔
266
    current_app.logger.info('validating otp for user email: {}'.format(email))
1✔
267
    if request.method == 'POST' and form.validate():
1✔
268
        otp_code = otp.retrieve_user_otp_secret(email)
1✔
269
        # bytes to unicode string
270
        if type(otp_code) == bytes:
1✔
271
            otp_code = otp_code.decode()
1✔
272
        if otp_code is not None:
1✔
273
            if otp_code == user_otp:
1✔
274
                msg = gettext('OTP verified. You are logged in to the system')
1✔
275
                current_app.logger.info('otp verified for user %s', email)
1✔
276
                flash(msg, 'success')
1✔
277
                otp.expire_token(token)
1✔
278
                return _sign_in_user(user)
1✔
279
            else:
280
                msg = gettext('Invalid one time password, a newly generated '
1✔
281
                              'one time password was sent to your email.')
282
                flash(msg, 'error')
1✔
283
        else:
284
            msg = gettext('Expired one time password, a newly generated one '
1✔
285
                          'time password was sent to your email.')
286
            flash(msg, 'error')
1✔
287

288
        current_app.logger.info(('Invalid OTP. retrieved: {}, submitted: {}, '
1✔
289
                                 'email: {}').format(otp_code, user_otp, email))
290
        _email_two_factor_auth(user, True)
1✔
291
        form.otp.data = ''
1✔
292
    response = dict(template='/account/otpvalidation.html',
1✔
293
                    title='Verify OTP',
294
                    form=form,
295
                    user=user.to_public_json(),
296
                    next=request.args.get('next'),
297
                    token=token)
298
    return handle_content_type(response)
1✔
299

300

301
@blueprint.route('/signout')
1✔
302
def signout():
1✔
303
    """
304
    Signout PYBOSSA users.
305

306
    Returns a redirection to PYBOSSA home page.
307

308
    """
309
    if current_user.is_authenticated:
1✔
310
        release_user_locks(current_user.id)
1✔
311
    logout_user()
1✔
312
    flash(gettext('You are now signed out'), SUCCESS)
1✔
313
    return redirect_content_type(url_for('home.home'), status=SUCCESS)
1✔
314

315

316
def get_email_confirmation_url(account):
1✔
317
    """Return confirmation url for a given user email."""
318
    key = signer.dumps(account, salt='account-validation')
1✔
319
    scheme = current_app.config.get('PREFERRED_URL_SCHEME')
1✔
320
    if (scheme):
1✔
321
        return url_for_app_type('.confirm_account',
1✔
322
                                key=key,
323
                                _scheme=scheme,
324
                                _external=True)
325
    else:
326
        return url_for_app_type('.confirm_account', key=key, _external=True)
×
327

328

329
@blueprint.route('/confirm-email')
1✔
330
@login_required
1✔
331
def confirm_email():
1✔
332
    """Send email to confirm user email."""
333
    acc_conf_dis = current_app.config.get('ACCOUNT_CONFIRMATION_DISABLED')
1✔
334
    if acc_conf_dis:
1✔
335
        return abort(404)
×
336
    if current_user.valid_email is False:
1✔
337
        user = user_repo.get(current_user.id)
1✔
338
        account = dict(fullname=current_user.fullname, name=current_user.name,
1✔
339
                       email_addr=current_user.email_addr)
340
        confirm_url = get_email_confirmation_url(account)
1✔
341
        subject = ('Verify your email in %s' % current_app.config.get('BRAND'))
1✔
342
        msg = dict(subject=subject,
1✔
343
                   recipients=[current_user.email_addr],
344
                   body=render_template('/account/email/validate_email.md',
345
                                        user=account, confirm_url=confirm_url))
346
        msg['html'] = render_template('/account/email/validate_email.html',
1✔
347
                                      user=account, confirm_url=confirm_url)
348
        mail_queue.enqueue(send_mail, msg)
1✔
349
        msg = gettext("An e-mail has been sent to \
1✔
350
                       validate your e-mail address.")
351
        flash(msg, 'info')
1✔
352
        user.confirmation_email_sent = True
1✔
353
        user_repo.update(user)
1✔
354
    return redirect_content_type(url_for('.profile', name=current_user.name))
1✔
355

356

357
def get_project_choices():
1✔
358
    choices = [(project['short_name'], project['name'])
1✔
359
               for project in get_all_projects()]
360
    choices.sort(key=lambda x: x[1])
1✔
361
    choices.insert(0, ('', ''))
1✔
362
    return choices
1✔
363

364

365
@blueprint.route('/register', methods=['GET', 'POST'])
1✔
366
@login_required
1✔
367
@admin_required
1✔
368
def register():
1✔
369
    """
370
    Register method for creating a PYBOSSA account.
371

372
    Returns a Jinja2 template
373

374
    """
375
    if current_app.config.get('LDAP_HOST', False):
1✔
376
        return abort(404)
×
377
    if not app_settings.upref_mdata:
1✔
378
        form = RegisterForm(request.body)
1✔
379
    else:
380
        form = RegisterFormWithUserPrefMetadata(request.body)
1✔
381
        form.set_upref_mdata_choices()
1✔
382

383
    form.project_slug.choices = get_project_choices()
1✔
384
    msg = "I accept receiving emails from %s" % current_app.config.get('BRAND')
1✔
385
    form.consent.label = msg
1✔
386
    if request.method == 'POST':
1✔
387
        form.generate_password()
1✔
388
    if request.method == 'POST' and form.validate():
1✔
389
        if app_settings.upref_mdata:
1✔
390
            user_pref, metadata = get_user_pref_and_metadata(form.name.data, form)
1✔
391
            account = dict(fullname=form.fullname.data, name=form.name.data,
1✔
392
                           email_addr=form.email_addr.data,
393
                           password=form.password.data,
394
                           consent=form.consent.data,
395
                           user_type=form.user_type.data)
396
            account['user_pref'] = user_pref
1✔
397
            account['metadata'] = metadata
1✔
398
        else:
399
            account = dict(fullname=form.fullname.data, name=form.name.data,
1✔
400
                           email_addr=form.email_addr.data,
401
                           password=form.password.data,
402
                           consent=form.consent.data)
403

404
        # guarentee that the full name is well defined with no spaces at the beginning or end
405
        account['fullname'] = account['fullname'].strip()
1✔
406

407
        ensure_user_data_access_assignment_from_form(account, form)
1✔
408
        confirm_url = get_email_confirmation_url(account)
1✔
409
        if current_app.config.get('ACCOUNT_CONFIRMATION_DISABLED'):
1✔
410
            project_slugs=form.project_slug.data
1✔
411
            create_account(account, project_slugs=project_slugs)
1✔
412
            flash(gettext('Created user successfully!'), 'success')
1✔
413
            return redirect_content_type(url_for("home.home"))
1✔
414
        msg = dict(subject='Welcome to %s!' % current_app.config.get('BRAND'),
1✔
415
                   recipients=[account['email_addr']],
416
                   body=render_template('/account/email/validate_account.md',
417
                                        user=account, confirm_url=confirm_url))
418
        msg['html'] = markdown(msg['body'])
1✔
419
        mail_queue.enqueue(send_mail, msg)
1✔
420
        data = dict(template='account/account_validation.html',
1✔
421
                    title=gettext("Account validation"),
422
                    status='sent')
423
        return handle_content_type(data)
1✔
424
    if request.method == 'POST' and not form.validate():
1✔
425
        flash(gettext('Please correct the errors'), 'error')
1✔
426
    del form.password
1✔
427
    del form.confirm
1✔
428

429
    data = dict(template='account/register.html',
1✔
430
                title=gettext("Register"), form=form)
431
    return handle_content_type(data)
1✔
432

433

434
@blueprint.route('/newsletter')
1✔
435
@login_required
1✔
436
def newsletter_subscribe():
1✔
437
    """
438
    Register method for subscribing user to PYBOSSA newsletter.
439

440
    Returns a Jinja2 template
441

442
    """
443
    # Save that we've prompted the user to sign up in the newsletter
444
    if newsletter.is_initialized() and current_user.is_authenticated:
1✔
445
        next_url = request.args.get('next') or url_for('home.home')
1✔
446
        user = user_repo.get(current_user.id)
1✔
447
        if current_user.newsletter_prompted is False:
1✔
448
            user.newsletter_prompted = True
1✔
449
            user_repo.update(user)
1✔
450
        if request.args.get('subscribe') == 'True':
1✔
451
            newsletter.subscribe_user(user)
1✔
452
            flash("You are subscribed to our newsletter!", 'success')
1✔
453
            return redirect_content_type(next_url)
1✔
454
        elif request.args.get('subscribe') == 'False':
1✔
455
            return redirect_content_type(next_url)
1✔
456
        else:
457
            response = dict(template='account/newsletter.html',
1✔
458
                            title=gettext("Subscribe to our Newsletter"),
459
                            next=next_url)
460
            return handle_content_type(response)
1✔
461
    else:
462
        return abort(404)
1✔
463

464

465
@blueprint.route('/register/confirmation', methods=['GET'])
1✔
466
def confirm_account():
1✔
467
    """Confirm account endpoint."""
468
    key = request.args.get('key')
1✔
469
    if key is None:
1✔
470
        abort(403)
1✔
471
    try:
1✔
472
        timeout = current_app.config.get('ACCOUNT_LINK_EXPIRATION', 3600)
1✔
473
        userdict = signer.loads(key, max_age=timeout, salt='account-validation')
1✔
474
    except BadData:
1✔
475
        abort(403)
1✔
476
    # First check if the user exists
477
    user = user_repo.get_by_name(userdict['name'])
1✔
478
    if user is not None:
1✔
479
        return _update_user_with_valid_email(user, userdict['email_addr'])
1✔
480
    create_account(userdict)
1✔
481
    flash(gettext('Created user successfully!'), 'success')
1✔
482
    return redirect(url_for("home.home"))
1✔
483

484

485
def create_account(user_data, project_slugs=None, ldap_disabled=True, auto_create=False):
1✔
486
    """Creates the Gigwork account based on the criteria included in the user_data.
487
    The auto-create tag is set when the create_account is triggered by a BSSO sign-in attempt."""
488
    new_user = model.user.User(fullname=user_data['fullname'],
1✔
489
                               name=user_data['name'],
490
                               email_addr=user_data['email_addr'],
491
                               valid_email=True,
492
                               consent=user_data.get('consent', True))
493

494
    if user_data.get('user_pref'):
1✔
495
        new_user.user_pref = user_data['user_pref']
1✔
496

497
    user_metadata = user_data.get('metadata', {})
1✔
498
    new_user.info = dict(metadata=user_metadata)
1✔
499

500
    new_user.info['metadata'].update({"user_type": user_metadata.get('user_type', None),
1✔
501
        "admin": user_data.get('admin', None)})
502

503
    if ldap_disabled:
1✔
504
        new_user.set_password(user_data['password'])
1✔
505
    else:
506
        if user_data.get('ldap'):
1✔
507
            new_user.ldap = user_data['ldap']
1✔
508

509
    copy_user_data_access_levels(new_user.info, user_data.get('data_access'))
1✔
510
    user_repo.save(new_user)
1✔
511
    if not ldap_disabled:
1✔
512
        flash(gettext('Thanks for signing-up'), 'success')
1✔
513
        return _sign_in_user(new_user)
1✔
514
    user_info = dict(fullname=user_data['fullname'],
1✔
515
                     email_addr=user_data['email_addr'],
516
                     password=user_data['password'])
517
    msg = generate_invitation_email_for_new_user(user=user_info, project_slugs=project_slugs)
1✔
518
    mail_queue.enqueue(send_mail, msg)
1✔
519

520
    if not auto_create:
1✔
521
        return
1✔
522

523
    alert_msg = generate_bsso_account_notification(user=user_data)
1✔
524
    mail_queue.enqueue(send_mail, alert_msg)
1✔
525

526
def _update_user_with_valid_email(user, email_addr):
1✔
527
    user.valid_email = True
1✔
528
    user.confirmation_email_sent = False
1✔
529
    user.email_addr = email_addr
1✔
530
    user_repo.update(user)
1✔
531
    flash(gettext('Your email has been validated.'))
1✔
532
    return _sign_in_user(user)
1✔
533

534

535
@blueprint.route('/profile', methods=['GET'])
1✔
536
@login_required
1✔
537
def redirect_profile():
1✔
538
    """Redirect method for profile."""
539

540
    if current_user.is_anonymous:  # pragma: no cover
541
        return redirect_content_type(url_for('.signin'), status='not_signed_in')
542
    if (request.headers.get('Content-Type') == 'application/json') and current_user.is_authenticated:
1✔
543
        form = None
1✔
544
        if app_settings.upref_mdata:
1✔
545
            form_data = cached_users.get_user_pref_metadata(current_user.name)
1✔
546
            form = UserPrefMetadataForm(**form_data)
1✔
547
            form.set_upref_mdata_choices()
1✔
548
        can_update = can_update_user_info(current_user, current_user)
1✔
549
        return _show_own_profile(current_user, form, current_user, can_update)
1✔
550
    else:
551
        return redirect_content_type(url_for('.profile', name=current_user.name))
1✔
552

553

554
@blueprint.route('/<name>/', methods=['GET'])
1✔
555
@login_required
1✔
556
def profile(name):
1✔
557
    """
558
    Get user profile.
559

560
    Returns a Jinja2 template with the user information.
561

562
    """
563
    user = user_repo.get_by_name(name=name)
1✔
564
    if user is None or current_user.is_anonymous:
1✔
565
        raise abort(404)
1✔
566

567
    form = None
1✔
568
    (can_update, disabled_fields, hidden_fields) = can_update_user_info(current_user, user)
1✔
569
    if app_settings.upref_mdata:
1✔
570
        form_data = cached_users.get_user_pref_metadata(user.name)
1✔
571
        form = UserPrefMetadataForm(can_update=(can_update, disabled_fields, hidden_fields), **form_data)
1✔
572
        form.set_upref_mdata_choices()
1✔
573
    if user.id != current_user.id:
1✔
574
        return _show_public_profile(user, form, can_update)
1✔
575
    else:
576
        return _show_own_profile(user, form, current_user, can_update)
1✔
577

578

579
def _show_public_profile(user, form, can_update):
1✔
580
    if current_user.id == user.id:
1✔
581
        user_dict = cached_users.get_user_summary(user.name)
×
582
    else:
583
        user_dict = cached_users.public_get_user_summary(user.name)
1✔
584
    if user_dict and current_user.admin:
1✔
585
        user_dict['email_addr'] = user.email_addr
1✔
586
    projects_contributed = cached_users.public_projects_contributed_cached(user.id)
1✔
587
    projects_created = cached_users.public_published_projects_cached(user.id)
1✔
588
    total_projects_contributed = '{} / {}'.format(cached_users.n_projects_contributed(user.id), n_published())
1✔
589
    percentage_tasks_completed = user_dict['n_answers'] * 100 / (n_total_tasks() or 1) if user_dict else None
1✔
590

591
    if current_user.is_authenticated and current_user.admin:
1✔
592
        draft_projects = cached_users.draft_projects(user.id)
1✔
593
        projects_created.extend(draft_projects)
1✔
594

595
    if user.restrict is False:
1✔
596
        title = "%s &middot; User Profile" % user_dict['fullname']
1✔
597
    else:
598
        title = "User data is restricted"
1✔
599
        projects_contributed = []
1✔
600
        projects_created = []
1✔
601
        form = None
1✔
602
    response = dict(template='/account/public_profile.html',
1✔
603
                    title=title,
604
                    user=user_dict,
605
                    projects=projects_contributed,
606
                    projects_created=projects_created,
607
                    total_projects_contributed=total_projects_contributed,
608
                    percentage_tasks_completed=percentage_tasks_completed,
609
                    form=form,
610
                    can_update=can_update,
611
                    private_instance=bool(data_access_levels),
612
                    upref_mdata_enabled=bool(app_settings.upref_mdata),
613
                    country_name_to_country_code=app_settings.upref_mdata.country_name_to_country_code,
614
                    country_code_to_country_name=app_settings.upref_mdata.country_code_to_country_name)
615

616
    return handle_content_type(response)
1✔
617

618

619
def _show_own_profile(user, form, current_user, can_update):
1✔
620
    user_dict = cached_users.get_user_summary(user.name, current_user)
1✔
621
    rank_and_score = cached_users.rank_and_score(user.id)
1✔
622
    user.rank = rank_and_score['rank']
1✔
623
    user.score = rank_and_score['score']
1✔
624
    user.total = cached_users.get_total_users()
1✔
625
    projects_contributed = cached_users.public_projects_contributed_cached(user.id)
1✔
626
    projects_published, projects_draft = _get_user_projects(user.id)
1✔
627
    cached_users.get_user_summary(user.name)
1✔
628

629
    response = dict(template='account/profile.html',
1✔
630
                    title=gettext("Profile"),
631
                    projects_contrib=projects_contributed,
632
                    projects_published=projects_published,
633
                    projects_draft=projects_draft,
634
                    user=user_dict,
635
                    form=form,
636
                    can_update=can_update,
637
                    private_instance=bool(data_access_levels),
638
                    upref_mdata_enabled=bool(app_settings.upref_mdata),
639
                    country_name_to_country_code=app_settings.upref_mdata.country_name_to_country_code,
640
                    country_code_to_country_name=app_settings.upref_mdata.country_code_to_country_name)
641

642
    response = make_response(handle_content_type(response))
1✔
643
    response.headers['Cache-Control'] = 'no-store'
1✔
644
    response.headers['Pragma'] = 'no-cache'
1✔
645
    return response
1✔
646

647

648
utc_dt_re = re.compile(r'^\d{4}(-\d{2}){2}T(\d{2}:){2}\d{2}\.\d{1,6}Z$')
1✔
649

650

651
@blueprint.route('/<name>/recent_tasks', methods=['GET'])
1✔
652
@login_required
1✔
653
def recent_tasks(name):
1✔
654
    current_app.logger.debug('recent_tasks: {}'.format(name))
1✔
655
    start_time_utc = request.args.get('start')
1✔
656
    if (not start_time_utc) or (not utc_dt_re.search(start_time_utc)):
1✔
657
        abort(400)
1✔
658
    user = user_repo.get_by_name(name)
1✔
659
    recent = cached_users.get_tasks_completed_between(user.id, beginning_time_utc=start_time_utc[:-1])
1✔
660
    return jsonify(dict(count=len(recent)))
1✔
661

662

663
columns = {
1✔
664
    "created_on": "Created On",
665
    "project_name": "Project Name"
666
}
667

668
directions = {
1✔
669
    "desc": "Descending",
670
    "asc": "Ascending"
671
}
672

673

674
def get_project_browse_args(args):
1✔
675
    if args is None:
1✔
676
        args = {}
×
677
    query_str = args.get("order_by", "created_on:desc")
1✔
678
    col, order = query_str.split(":") if ":" in query_str \
1✔
679
        else (query_str, "")
680
    column = col if col in columns else "created_on"
1✔
681
    sort_order = order if order in directions else "desc"
1✔
682
    return dict(column=column, order=sort_order)
1✔
683

684

685
@blueprint.route('/<name>/applications')
1✔
686
@blueprint.route('/<name>/projects')
1✔
687
@login_required
1✔
688
def projects(name):
1✔
689
    """
690
    List user's project list.
691

692
    Returns a Jinja2 template with the list of projects of the user.
693

694
    """
695
    user = user_repo.get_by_name(name)
1✔
696
    if not user:
1✔
697
        return abort(404)
1✔
698
    if current_user.name != name:
1✔
699
        return abort(403)
1✔
700

701
    user = user_repo.get(current_user.id)
1✔
702
    args = get_project_browse_args(request.args)
1✔
703
    projects_published, projects_draft = _get_user_projects(user.id, args)
1✔
704

705
    sort_options = {
1✔
706
        "columns": {
707
            "entries": columns,
708
            "id": "project-column-selection",
709
            "current_selection": args["column"]
710
        },
711
        "directions": {
712
            "entries": directions,
713
            "id": "project-dir-selection",
714
            "current_selection": args["order"]
715
        }
716
    }
717

718
    response = dict(template='account/projects.html',
1✔
719
                    title=gettext("Projects"),
720
                    projects_published=projects_published,
721
                    projects_draft=projects_draft,
722
                    sort_options=sort_options)
723
    return handle_content_type(response)
1✔
724

725

726
def _get_user_projects(user_id, opts=None):
1✔
727
    projects_published = cached_users.published_projects(user_id, opts)
1✔
728
    projects_draft = cached_users.draft_projects(user_id)
1✔
729
    return projects_published, projects_draft
1✔
730

731

732
@blueprint.route('/<name>/update', methods=['GET', 'POST'])
1✔
733
@login_required
1✔
734
def update_profile(name):
1✔
735
    """
736
    Update user's profile.
737

738
    Returns Jinja2 template.
739

740
    """
741
    user = user_repo.get_by_name(name)
1✔
742
    if not user:
1✔
743
        return abort(404)
1✔
744
    if current_user.name != name:
1✔
745
        return abort(403)
1✔
746
    ensure_authorized_to('update', user)
1✔
747
    show_passwd_form = True
1✔
748
    if user.twitter_user_id or user.google_user_id or user.facebook_user_id:
1✔
749
        show_passwd_form = False
1✔
750
    usr = cached_users.get_user_summary(name, current_user)
1✔
751
    # Extend the values
752
    user.rank = usr.get('rank')
1✔
753
    user.score = usr.get('score')
1✔
754
    btn = request.body.get('btn', 'None').capitalize()
1✔
755
    if btn != 'Profile':
1✔
756
        update_form = UpdateProfileForm(formdata=None, obj=user)
1✔
757
    else:
758
        update_form = UpdateProfileForm(obj=user)
1✔
759
    update_form.set_locales(current_app.config['LOCALES'])
1✔
760
    avatar_form = AvatarUploadForm()
1✔
761
    password_form = ChangePasswordForm()
1✔
762

763
    title_msg = "Update your profile: %s" % user.fullname
1✔
764

765
    if request.method == 'POST':
1✔
766
        # Update user avatar
767
        succeed = False
1✔
768
        btn = request.body.get('btn', 'None').capitalize()
1✔
769
        if btn == 'Upload':
1✔
770
            succeed = _handle_avatar_update(user, avatar_form)
1✔
771
        # Update user profile
772
        elif btn == 'Profile':
1✔
773
            succeed = _handle_profile_update(user, update_form)
1✔
774
        # Update user password
775
        elif btn == 'Password':
1✔
776
            succeed = _handle_password_update(user, password_form)
1✔
777
        # Update user external services
778
        elif btn == 'External':
×
779
            succeed = _handle_external_services_update(user, update_form)
×
780
        # Otherwise return 415
781
        else:
782
            return abort(415)
×
783
        if succeed:
1✔
784
            cached_users.delete_user_summary(user.name)
1✔
785
            return redirect_content_type(url_for('.update_profile',
1✔
786
                                                 name=user.name),
787
                                         status=SUCCESS)
788
        else:
789
            data = dict(template='/account/update.html',
1✔
790
                        form=update_form,
791
                        upload_form=avatar_form,
792
                        password_form=password_form,
793
                        title=title_msg,
794
                        show_passwd_form=show_passwd_form)
795
            return handle_content_type(data)
1✔
796

797
    data = dict(template='/account/update.html',
1✔
798
                form=update_form,
799
                upload_form=avatar_form,
800
                password_form=password_form,
801
                title=title_msg,
802
                show_passwd_form=show_passwd_form)
803
    return handle_content_type(data)
1✔
804

805

806
def _handle_avatar_update(user, avatar_form):
1✔
807
    if avatar_form.validate_on_submit():
1✔
808
        _file = request.files['avatar']
1✔
809
        coordinates = (avatar_form.x1.data, avatar_form.y1.data,
1✔
810
                       avatar_form.x2.data, avatar_form.y2.data)
811
        prefix = time.time()
1✔
812
        _file.filename = "%s_avatar.png" % prefix
1✔
813
        container = "user_%s" % user.id
1✔
814
        uploader.upload_file(_file,
1✔
815
                             container=container,
816
                             coordinates=coordinates)
817
        # Delete previous avatar from storage
818
        if user.info.get('avatar'):
1✔
819
            uploader.delete_file(user.info['avatar'], container)
1✔
820
        upload_method = current_app.config.get('UPLOAD_METHOD')
1✔
821
        avatar_url = get_avatar_url(upload_method,
1✔
822
                                    _file.filename,
823
                                    container,
824
                                    current_app.config.get('AVATAR_ABSOLUTE'))
825
        user.info['avatar'] = _file.filename
1✔
826
        user.info['container'] = container
1✔
827
        user.info['avatar_url'] = avatar_url
1✔
828
        user_repo.update(user)
1✔
829
        cached_users.delete_user_summary(user.name)
1✔
830
        flash(gettext('Your avatar has been updated! It may \
1✔
831
                      take some minutes to refresh...'), 'success')
832
        return True
1✔
833
    else:
834
        flash("You have to provide an image file to update your avatar", "error")
1✔
835
        return False
1✔
836

837

838
def _handle_profile_update(user, update_form):
1✔
839
    acc_conf_dis = current_app.config.get('ACCOUNT_CONFIRMATION_DISABLED')
1✔
840
    if update_form.validate_on_submit():
1✔
841
        user.id = update_form.id.data
1✔
842
        user.fullname = update_form.fullname.data
1✔
843
        user.name = update_form.name.data
1✔
844
        account, domain = update_form.email_addr.data.split('@')
1✔
845
        if (user.email_addr != update_form.email_addr.data and
1✔
846
                acc_conf_dis is False and
847
                domain not in current_app.config.get('SPAM')):
848
            user.valid_email = False
1✔
849
            user.newsletter_prompted = False
1✔
850
            account = dict(fullname=update_form.fullname.data,
1✔
851
                           name=update_form.name.data,
852
                           email_addr=update_form.email_addr.data)
853
            confirm_url = get_email_confirmation_url(account)
1✔
854
            subject = ('You have updated your email in %s! Verify it'
1✔
855
                       % current_app.config.get('BRAND'))
856
            msg = dict(subject=subject,
1✔
857
                       recipients=[update_form.email_addr.data],
858
                       body=render_template(
859
                           '/account/email/validate_email.md',
860
                           user=account, confirm_url=confirm_url))
861
            msg['html'] = markdown(msg['body'])
1✔
862
            mail_queue.enqueue(send_mail, msg)
1✔
863
            user.confirmation_email_sent = True
1✔
864
            fls = gettext('An email has been sent to verify your \
1✔
865
                          new email: %s. Once you verify it, it will \
866
                          be updated.' % account['email_addr'])
867
            flash(fls, 'info')
1✔
868
            return True
1✔
869
        if acc_conf_dis is False and domain in current_app.config.get('SPAM'):
1✔
870
            fls = gettext('Use a valid email account')
1✔
871
            flash(fls, 'info')
1✔
872
            return False
1✔
873
        if acc_conf_dis:
1✔
874
            user.email_addr = update_form.email_addr.data
1✔
875
        user.privacy_mode = fuzzyboolean(update_form.privacy_mode.data)
1✔
876
        user.restrict = fuzzyboolean(update_form.restrict.data)
1✔
877
        user.locale = update_form.locale.data
1✔
878
        user.subscribed = fuzzyboolean(update_form.subscribed.data)
1✔
879
        user_repo.update(user)
1✔
880
        cached_users.delete_user_summary(user.name)
1✔
881
        flash(gettext('Your profile has been updated!'), 'success')
1✔
882
        return True
1✔
883
    else:
884
        flash(gettext('Please correct the errors'), 'error')
1✔
885
        return False
1✔
886

887

888
def _handle_password_update(user, password_form):
1✔
889
    if password_form.validate_on_submit():
1✔
890
        user = user_repo.get(user.id)
1✔
891
        if user.check_password(password_form.current_password.data):
1✔
892
            user.set_password(password_form.new_password.data)
1✔
893
            user_repo.update(user)
1✔
894
            flash(gettext('Yay, you changed your password successfully!'),
1✔
895
                  'success')
896
            return True
1✔
897
        else:
898
            msg = gettext("Your current password doesn't match the "
1✔
899
                          "one in our records")
900
            flash(msg, 'error')
1✔
901
            return False
1✔
902
    else:
903
        flash(gettext('Please correct the errors'), 'error')
1✔
904
        return False
1✔
905

906

907
def _handle_external_services_update(user, update_form):
1✔
908
    del update_form.locale
×
909
    del update_form.email_addr
×
910
    del update_form.fullname
×
911
    del update_form.name
×
912
    if update_form.validate():
×
913
        user.ckan_api = update_form.ckan_api.data or None
×
914
        user_repo.update(user)
×
915
        cached_users.delete_user_summary(user.name)
×
916
        flash(gettext('Your profile has been updated!'), 'success')
×
917
        return True
×
918
    else:
919
        flash(gettext('Please correct the errors'), 'error')
×
920
        return False
×
921

922

923
@blueprint.route('/password-reset-key', methods=['GET', 'POST'])
1✔
924
def password_reset_key():
1✔
925
    form = PasswordResetKeyForm(request.body)
1✔
926
    if request.method == 'GET' or not form.validate_on_submit():
1✔
927
        response = dict(template='/account/password_reset_key.html', form=form)
1✔
928
    else:
929
        return redirect_content_type(url_for('account.reset_password', key=form.password_reset_key.data))
1✔
930
    return handle_content_type(response)
1✔
931

932

933
@blueprint.route('/reset-password', methods=['GET', 'POST'])
1✔
934
def reset_password():
1✔
935
    """
936
    Reset password method.
937

938
    Returns a Jinja2 template.
939

940
    """
941
    key = request.args.get('key')
1✔
942
    if key is None:
1✔
943
        abort(403)
1✔
944
    userdict = {}
1✔
945
    try:
1✔
946
        timeout = current_app.config.get('ACCOUNT_LINK_EXPIRATION', 3600)
1✔
947
        userdict = signer.loads(key, max_age=timeout, salt='password-reset')
1✔
948
    except BadData:
1✔
949
        abort(403)
1✔
950
    username = userdict.get('user')
1✔
951
    if not username or not userdict.get('password'):
1✔
952
        abort(403)
1✔
953
    user = user_repo.get_by_name(username)
1✔
954
    if user.passwd_hash != userdict.get('password'):
1✔
955
        abort(403)
1✔
956
    form = ChangePasswordForm(request.body)
1✔
957
    if form.validate_on_submit():
1✔
958
        user.set_password(form.new_password.data)
1✔
959
        user_repo.update(user)
1✔
960
        flash(gettext('You reset your password successfully!'), 'success')
1✔
961
        return _sign_in_user(user)
1✔
962
    if request.method == 'POST' and not form.validate():
1✔
963
        flash(gettext('Please correct the errors'), 'error')
1✔
964
    response = dict(template='/account/password_reset.html', form=form)
1✔
965
    return handle_content_type(response)
1✔
966

967

968
@blueprint.route('/forgot-password', methods=['GET', 'POST'])
1✔
969
def forgot_password():
1✔
970
    """
971
    Request a forgotten password for a user.
972

973
    Returns a Jinja2 template.
974

975
    """
976
    form = ForgotPasswordForm(request.body)
1✔
977
    data = dict(template='/account/password_forgot.html',
1✔
978
                form=form)
979

980
    if form.validate_on_submit():
1✔
981
        email_addr = form.email_addr.data.lower()
1✔
982
        user = user_repo.get_by(email_addr=email_addr)
1✔
983
        if user and not user.enabled:
1✔
984
            brand = current_app.config['BRAND']
×
985
            flash(gettext('Your account is disabled. '
×
986
                          'Please contact your {} administrator.'.format(brand)),
987
                  'error')
988
            return handle_content_type(data)
×
989
        if user and user.email_addr:
1✔
990
            msg = dict(subject='Account Recovery',
1✔
991
                       recipients=[user.email_addr])
992
            if user.twitter_user_id:
1✔
993
                msg['body'] = render_template(
1✔
994
                    '/account/email/forgot_password_openid.md',
995
                    user=user, account_name='Twitter')
996
                msg['html'] = render_template(
1✔
997
                    '/account/email/forgot_password_openid.html',
998
                    user=user, account_name='Twitter')
999
            elif user.facebook_user_id:
1✔
1000
                msg['body'] = render_template(
1✔
1001
                    '/account/email/forgot_password_openid.md',
1002
                    user=user, account_name='Facebook')
1003
                msg['html'] = render_template(
1✔
1004
                    '/account/email/forgot_password_openid.html',
1005
                    user=user, account_name='Facebook')
1006
            elif user.google_user_id:
1✔
1007
                msg['body'] = render_template(
1✔
1008
                    '/account/email/forgot_password_openid.md',
1009
                    user=user, account_name='Google')
1010
                msg['html'] = render_template(
1✔
1011
                    '/account/email/forgot_password_openid.html',
1012
                    user=user, account_name='Google')
1013
            else:
1014
                userdict = {'user': user.name, 'password': user.passwd_hash}
1✔
1015
                key = signer.dumps(userdict, salt='password-reset')
1✔
1016
                recovery_url = url_for_app_type('.reset_password',
1✔
1017
                                                key=key, _external=True)
1018
                msg['body'] = render_template(
1✔
1019
                    '/account/email/forgot_password.md',
1020
                    user=user, recovery_url=recovery_url, key=key)
1021
                msg['html'] = render_template(
1✔
1022
                    '/account/email/forgot_password.html',
1023
                    user=user, recovery_url=recovery_url, key=key)
1024
            mail_queue.enqueue(send_mail, msg)
1✔
1025
            flash(gettext("We've sent you an email with account "
1✔
1026
                          "recovery instructions!"),
1027
                  'success')
1028
        else:
1029
            flash(gettext("We don't have this email in our records. "
1✔
1030
                          "You may have signed up with a different "
1031
                          "email"), 'error')
1032
    if request.method == 'POST':
1✔
1033
        if not form.validate():
1✔
1034
            flash(gettext('Something went wrong, please correct the errors on the '
1✔
1035
                'form'), 'error')
1036
        else:
1037
            return redirect_content_type(url_for('account.password_reset_key'))
1✔
1038
    return handle_content_type(data)
1✔
1039

1040

1041
@blueprint.route('/<name>/export')
1✔
1042
@login_required
1✔
1043
@admin_required
1✔
1044
def start_export(name):
1✔
1045
    """
1046
    Starts a export of all user data according to EU GDPR
1047

1048
    Data will be available on GET /export after it is processed
1049

1050
    """
1051
    user = user_repo.get_by_name(name)
1✔
1052
    if not user:
1✔
1053
        return abort(404)
×
1054

1055
    ensure_authorized_to('update', user)
1✔
1056
    export_queue.enqueue(export_userdata,
1✔
1057
                         user_id=user.id,
1058
                         admin_addr=current_user.email_addr)
1059
    msg = gettext('GDPR export started')
1✔
1060
    flash(msg, 'success')
1✔
1061
    return redirect_content_type(url_for('account.profile', name=name))
1✔
1062

1063

1064
@blueprint.route('/<name>/resetapikey', methods=['GET', 'POST'])
1✔
1065
@login_required
1✔
1066
def reset_api_key(name):
1✔
1067
    """
1068
    Reset API-KEY for user.
1069

1070
    Returns a Jinja2 template.
1071

1072
    """
1073
    if request.method == 'POST':
1✔
1074
        user = user_repo.get_by_name(name)
1✔
1075
        if not user:
1✔
1076
            return abort(404)
1✔
1077
        ensure_authorized_to('update', user)
1✔
1078
        user.api_key = model.make_uuid()
1✔
1079
        user_repo.update(user)
1✔
1080
        cached_users.delete_user_summary(user.name)
1✔
1081
        msg = gettext('New API-KEY generated')
1✔
1082
        flash(msg, 'success')
1✔
1083
        return redirect_content_type(url_for('account.profile', name=name))
1✔
1084
    else:
1085
        csrf = dict(form=dict(csrf=generate_csrf()))
1✔
1086
        return jsonify(csrf)
1✔
1087

1088

1089
@blueprint.route('/<name>/delete')
1✔
1090
@login_required
1✔
1091
@admin_required
1✔
1092
def delete(name):
1✔
1093
    """
1094
    Delete user account.
1095
    """
1096
    user = user_repo.get_by_name(name)
1✔
1097
    if not user:
1✔
1098
        return abort(404)
1✔
1099
    if user.admin:
1✔
1100
        return abort(403)
1✔
1101

1102
    super_queue.enqueue(delete_account, user.id, current_user.email_addr)
1✔
1103

1104
    if (request.headers.get('Content-Type') == 'application/json' or
1✔
1105
        request.args.get('response_format') == 'json'):
1106

1107
        response = dict(job='enqueued', template='account/delete.html')
1✔
1108
        return handle_content_type(response)
1✔
1109
    else:
1110
        return redirect(url_for('admin.index'))
1✔
1111

1112

1113
@blueprint.route('/save_metadata/<name>', methods=['POST'])
1✔
1114
def add_metadata(name):
1✔
1115
    """
1116
    Admin can save metadata for selected user.
1117
    Regular user can save their own metadata.
1118

1119
    Redirects to public profile page for selected user.
1120

1121
    """
1122
    user = user_repo.get_by_name(name=name)
1✔
1123
    (can_update, disabled_fields, hidden_fields) = can_update_user_info(current_user, user)
1✔
1124
    if not can_update:
1✔
1125
        abort(403)
1✔
1126
    form_data = get_form_data(request, user, disabled_fields)
1✔
1127
    form = UserPrefMetadataForm(form_data, can_update=(can_update, disabled_fields, hidden_fields))
1✔
1128
    form.set_upref_mdata_choices()
1✔
1129

1130
    if not form.validate():
1✔
1131
        if current_user.id == user.id:
1✔
1132
            user_dict = cached_users.get_user_summary(user.name)
1✔
1133
        else:
1134
            user_dict = cached_users.public_get_user_summary(user.name)
×
1135
        projects_contributed = cached_users.projects_contributed_cached(user.id)
1✔
1136
        projects_created = cached_users.published_projects_cached(user.id)
1✔
1137
        total_projects_contributed = '{} / {}'.format(cached_users.n_projects_contributed(user.id), n_published())
1✔
1138
        percentage_tasks_completed = user_dict['n_answers'] * 100 / (n_total_tasks() or 1)
1✔
1139
        if current_user.is_authenticated and current_user.admin:
1✔
1140
            draft_projects = cached_users.draft_projects(user.id)
1✔
1141
            projects_created.extend(draft_projects)
1✔
1142
        title = "%s &middot; User Profile" % user.name
1✔
1143
        flash("Please fix the errors", 'message')
1✔
1144
        return render_template('/account/public_profile.html',
1✔
1145
                               title=title, user=user,
1146
                               projects=projects_contributed,
1147
                               projects_created=projects_created,
1148
                               total_projects_contributed=total_projects_contributed,
1149
                               percentage_tasks_completed=percentage_tasks_completed,
1150
                               form=form,
1151
                               input_form=True,
1152
                               can_update=can_update,
1153
                               upref_mdata_enabled=bool(app_settings.upref_mdata),
1154
                               country_name_to_country_code=app_settings.upref_mdata.country_name_to_country_code,
1155
                               country_code_to_country_name=app_settings.upref_mdata.country_code_to_country_name)
1156

1157
    user_pref, metadata = get_user_pref_and_metadata(name, form)
1✔
1158
    user.info['metadata'] = metadata
1✔
1159
    ensure_user_data_access_assignment_from_form(user.info, form)
1✔
1160
    user.user_pref = user_pref
1✔
1161
    user_repo.update(user)
1✔
1162
    cached_users.delete_user_pref_metadata(user)
1✔
1163
    flash("Input saved successfully", "info")
1✔
1164
    return redirect(url_for('account.profile', name=name))
1✔
1165

1166

1167
def bookmarks_dict_to_array(bookmarks_dict, order_by, desc):
1✔
1168
    order_by =  order_by or "name"
1✔
1169
    desc = desc or False
1✔
1170

1171
    bookmarks_array = [{'name': name, **meta} for name, meta in bookmarks_dict.items()]
1✔
1172
    bookmarks_array.sort(key=lambda b: b[order_by], reverse=desc)
1✔
1173
    return bookmarks_array
1✔
1174

1175

1176
def get_bookmarks(user_name, short_name, order_by, desc):
1✔
1177
    taskbrowse_bookmarks = cached_users.get_taskbrowse_bookmarks(user_name)
1✔
1178
    proj_bookmarks = taskbrowse_bookmarks.get(short_name, {})
1✔
1179
    return bookmarks_dict_to_array(proj_bookmarks, order_by, desc)
1✔
1180

1181

1182
def add_bookmark(user_name, short_name, bookmark_name, bookmark_url, order_by, desc):
1✔
1183

1184
    if bookmark_name is None or len(bookmark_name) > MAX_BOOKMARK_NAME_LEN:
1✔
1185
        raise ValueError(f'Bookmark name must be between 1-{MAX_BOOKMARK_NAME_LEN} characters.')
1✔
1186
    if bookmark_url is None or len(bookmark_url) > MAX_BOOKMARK_URL_LEN:
1✔
1187
        raise ValueError(f'Bookmark URL must be between 1-{MAX_BOOKMARK_URL_LEN} characters.')
1✔
1188
    bookmark_name = str(bookmark_name)
1✔
1189

1190
    user = user_repo.get_by_name(name=user_name)
1✔
1191
    taskbrowse_bookmarks = user.info.get('taskbrowse_bookmarks', {})
1✔
1192
    proj_bookmarks = taskbrowse_bookmarks.get(short_name, {})
1✔
1193

1194
    old_bookmark = proj_bookmarks.get(bookmark_name, None)
1✔
1195
    if old_bookmark is not None:
1✔
1196
        created_date = old_bookmark['created']
1✔
1197
    else:
1198
        created_date = model.make_timestamp()
1✔
1199
    updated_date = model.make_timestamp()
1✔
1200
    bookmark_data = {
1✔
1201
        'url': bookmark_url,
1202
        'created': created_date,
1203
        'updated': updated_date
1204
    }
1205
    proj_bookmarks[bookmark_name] =  bookmark_data
1✔
1206
    taskbrowse_bookmarks[short_name] = proj_bookmarks
1✔
1207
    user.info['taskbrowse_bookmarks'] = taskbrowse_bookmarks
1✔
1208

1209
    user_repo.update(user)
1✔
1210
    cached_users.delete_taskbrowse_bookmarks(user)
1✔
1211
    return bookmarks_dict_to_array(proj_bookmarks, order_by, desc)
1✔
1212

1213

1214
def delete_bookmark(user_name, short_name, bookmark_name, order_by, desc):
1✔
1215
    bookmark_name = str(bookmark_name)
1✔
1216

1217
    user = user_repo.get_by_name(name=user_name)
1✔
1218
    taskbrowse_bookmarks = user.info.get('taskbrowse_bookmarks', {})
1✔
1219
    proj_bookmarks = taskbrowse_bookmarks.get(short_name, {})
1✔
1220

1221
    if bookmark_name not in proj_bookmarks:
1✔
1222
        raise ValueError('Bookmark not found')
1✔
1223
    del proj_bookmarks[bookmark_name]
1✔
1224
    # if no bookmarks left for this project, delete the mapping entry
1225
    if len(proj_bookmarks) == 0:
1✔
1226
        del taskbrowse_bookmarks[short_name]
1✔
1227
    else:
1228
        taskbrowse_bookmarks[short_name] = proj_bookmarks
1✔
1229

1230
    user.info['taskbrowse_bookmarks'] = taskbrowse_bookmarks
1✔
1231
    user_repo.update(user)
1✔
1232
    cached_users.delete_taskbrowse_bookmarks(user)
1✔
1233
    return bookmarks_dict_to_array(proj_bookmarks, order_by, desc)
1✔
1234

1235

1236
@blueprint.route('/<user_name>/taskbrowse_bookmarks/<short_name>', methods=['GET', 'POST', 'DELETE'])
1✔
1237
@login_required
1✔
1238
@csrf.exempt
1✔
1239
def taskbrowse_bookmarks(user_name, short_name):
1✔
1240
    if current_user.name != user_name:
1✔
1241
        return abort(404)
1✔
1242

1243
    order_by  = request.args.get('order_by', None, type=str)
1✔
1244
    desc_str  = request.args.get('desc', None)
1✔
1245
    desc = {'true': True, 'false': False}.get(desc_str)
1✔
1246

1247
    # get bookmarks for project from cache
1248
    if request.method == 'GET':
1✔
1249
        res_bookmarks = get_bookmarks(user_name, short_name, order_by, desc)
1✔
1250

1251
    # add a bookmark
1252
    elif request.method == 'POST':
1✔
1253
        bookmark_name = request.json.get('name', None)
1✔
1254
        bookmark_url = request.json.get('url', None)
1✔
1255
        try:
1✔
1256
            res_bookmarks = add_bookmark(user_name, short_name, bookmark_name, bookmark_url, order_by, desc)
1✔
1257
        except ValueError as e:
1✔
1258
            error_msg = str(e)
1✔
1259
            current_app.logger.exception(f'Bad request: {error_msg},  project: {short_name}, bookmark_name:{bookmark_name}')
1✔
1260
            return jsonify({"description": error_msg}), 400
1✔
1261

1262
    # delete a bookmark
1263
    elif request.method == 'DELETE':
1✔
1264
        bookmark_name = request.json.get('name', None)
1✔
1265
        try:
1✔
1266
            res_bookmarks = delete_bookmark(user_name, short_name, bookmark_name, order_by, desc)
1✔
1267
        except ValueError as e:
1✔
1268
            error_msg = str(e)
1✔
1269
            current_app.logger.exception(f'Bad request: {error_msg},  project: {short_name}, bookmark_name:{bookmark_name}')
1✔
1270
            return jsonify({"description": error_msg}), 400
1✔
1271

1272
    return jsonify(res_bookmarks)
1✔
1273

1274

1275
# This is only called if can_update is True.
1276
def get_form_data(request, user, disabled_fields):
1✔
1277
    if not disabled_fields:
1✔
1278
        return request.form
1✔
1279
    # Some fields are not updatable.
1280
    # Replace (or add if missing) the data that was submitted for those fields
1281
    # with the current actual data for the user
1282
    # so that they won't be updated.
1283
    user_data = get_user_data_as_form(user)
1✔
1284
    # Get a mutable MultiDict
1285
    result = request.form.copy()
1✔
1286
    for field_name in six.iterkeys(disabled_fields):
1✔
1287
        value = user_data[field_name]
1✔
1288
        if not isinstance(value, list):
1✔
1289
            value = [value]
1✔
1290
        result.setlist(field_name, value)
1✔
1291
    return result
1✔
1292

1293
def get_user_data_as_form(user):
1✔
1294
    user_pref = user.user_pref or {}
1✔
1295
    metadata = user.info.get('metadata', {})
1✔
1296
    return {
1✔
1297
        'languages': user_pref.get('languages'),
1298
        'locations': user_pref.get('locations'),
1299
        'country_codes': user_pref.get('country_codes'),
1300
        'country_names': user_pref.get('country_names'),
1301
        'user_type': metadata.get('user_type'),
1302
        'work_hours_from': metadata.get('work_hours_from'),
1303
        'work_hours_to': metadata.get('work_hours_to'),
1304
        'review': metadata.get('review'),
1305
        'timezone': metadata.get('timezone'),
1306
        'data_access': user.info.get('data_access'),
1307
        'profile': metadata.get('profile')
1308
    }
1309

1310

1311
def get_user_pref_and_metadata(user_name, form):
1✔
1312
    user_pref = {}
1✔
1313
    metadata = {}
1✔
1314
    if not any(value for value in form.data.values()):
1✔
1315
        return user_pref, metadata
1✔
1316

1317
    if form.validate():
1✔
1318
        metadata = dict(admin=current_user.name, time_stamp=time.ctime(),
1✔
1319
                        user_type=form.user_type.data, work_hours_from=form.work_hours_from.data,
1320
                        work_hours_to=form.work_hours_to.data, review=form.review.data,
1321
                        timezone=form.timezone.data, profile_name=user_name,
1322
                        profile=form.profile.data)
1323
        if form.languages.data:
1✔
1324
            user_pref['languages'] = form.languages.data
1✔
1325

1326
        if form.country_names.data and form.country_codes.data:
1✔
1327
            # combine the two arrays
UNCOV
1328
            user_pref['locations'] = [x for arr in (form.country_names.data, form.country_codes.data) if arr is not None for x in arr]
×
1329
        elif form.country_names.data:
1✔
UNCOV
1330
            user_pref['locations'] = form.country_names.data
×
1331
        elif form.country_codes.data:
1✔
UNCOV
1332
            user_pref['locations'] = form.country_codes.data
×
1333
        elif form.locations.data:
1✔
1334
            user_pref['locations'] = form.locations.data
1✔
1335
        return user_pref, metadata
1✔
1336

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc