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

bloomberg / pybossa / 13395416999

18 Feb 2025 04:29PM UTC coverage: 94.153%. Remained the same
13395416999

push

github

web-flow
Re-enable code coverage (#1044)

17585 of 18677 relevant lines covered (94.15%)

0.94 hits per line

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

94.77
/pybossa/forms/forms.py
1
# -*- coding: utf8 -*-
2
# This file is part of PYBOSSA.
3
#
4
# Copyright (C) 2015 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
from tempfile import NamedTemporaryFile
1✔
19

20
from flask import current_app
1✔
21
from flask import request
1✔
22
from flask_wtf import FlaskForm as Form
1✔
23
from flask_wtf.file import FileField, FileRequired, FileAllowed
1✔
24
from werkzeug.utils import secure_filename
1✔
25
from wtforms import IntegerField, DecimalField, TextField, BooleanField, HiddenField,\
1✔
26
    SelectField, validators, TextAreaField, PasswordField, FieldList
27
from wtforms import SelectMultipleField
1✔
28
from wtforms.fields.html5 import EmailField, URLField
1✔
29
from wtforms.widgets import HiddenInput
1✔
30
from flask_babel import lazy_gettext
1✔
31

32
from . import validator as pb_validator
1✔
33
from pybossa import util
1✔
34
from pybossa.core import project_repo, user_repo, task_repo
1✔
35
from pybossa.core import uploader
1✔
36
from pybossa.uploader import local
1✔
37
from werkzeug.utils import safe_join
1✔
38
from flask_login import current_user
1✔
39
import os
1✔
40
import json
1✔
41
from decimal import Decimal
1✔
42

43
from pybossa.forms.fields.time_field import TimeField
1✔
44
from pybossa.forms.fields.select_two import Select2Field
1✔
45
from pybossa.sched import sched_variants
1✔
46
from .validator import TimeFieldsValidator
1✔
47
from pybossa.core import enable_strong_password
1✔
48
from pybossa.util import get_file_path_for_import_csv
1✔
49
from flask import flash
1✔
50
import pybossa.data_access as data_access
1✔
51
import pybossa.app_settings as app_settings
1✔
52
import six
1✔
53
from iiif_prezi.loader import ManifestReader
1✔
54
from wtforms.validators import NumberRange
1✔
55
from wtforms.fields.html5 import DateField
1✔
56
from wtforms_components import DateRange
1✔
57
from pybossa.forms.fields.select_with_props import SelectFieldWithProps
1✔
58

59

60
EMAIL_MAX_LENGTH = 254
1✔
61
USER_NAME_MAX_LENGTH = 35
1✔
62
USER_FULLNAME_MAX_LENGTH = 35
1✔
63
PROJECT_PWD_MIN_LEN = 5
1✔
64

65
def create_nullable_select(label, items):
1✔
66
    return SelectField(
1✔
67
        label=lazy_gettext(label),
68
        choices=[(None, "NONE")] + [(x,x) for x in items],
69
        coerce=lambda x: None if x == "None" else x
70
    )
71

72
### Custom Validators
73

74
def is_json(json_type):
1✔
75
    def v(form, field):
1✔
76
        try:
1✔
77
            if field.data:
1✔
78
                assert isinstance(json.loads(field.data), json_type)
1✔
79
        except Exception:
×
80
            raise validators.ValidationError('Field must be JSON object.')
×
81
    return v
1✔
82

83
BooleanField.false_values = {False, 'false', '', 'off', 'n', 'no'}
1✔
84

85

86
class ProjectCommonForm(Form):
1✔
87
    def __init__(self, *args, **kwargs):
1✔
88
        super(ProjectCommonForm, self).__init__(*args, **kwargs)
1✔
89
        if current_app.config.get('PROJECT_PASSWORD_REQUIRED'):
1✔
90
            self.password.validators = [pb_validator.CheckPasswordStrength(
×
91
                                        min_len=PROJECT_PWD_MIN_LEN,
92
                                        special=False,
93
                                        password_required=True), validators.Required()]
94
        else:
95
            self.password.validators = [pb_validator.CheckPasswordStrength(
1✔
96
                                        min_len=PROJECT_PWD_MIN_LEN,
97
                                        special=False,
98
                                        password_required=False), validators.Optional()]
99

100
    name = TextField(lazy_gettext('Name'),
1✔
101
                     [validators.Required(),
102
                      pb_validator.Unique(project_repo.get_by, 'name',
103
                                          message=lazy_gettext("Name is already taken."))])
104

105
    short_name = TextField(lazy_gettext('Short Name'),
1✔
106
                           [validators.Required(),
107
                            pb_validator.NotAllowedChars(),
108
                            pb_validator.Unique(project_repo.get_by, 'short_name',
109
                                message=lazy_gettext(
110
                                    "Short Name is already taken.")),
111
                            pb_validator.ReservedName('project', current_app)])
112

113
    password = PasswordField(
1✔
114
                    lazy_gettext('Password'),
115
                    render_kw={'placeholder': 'Minimum length {} characters, 1 uppercase, 1 lowercase and 1 numeric.'.format(PROJECT_PWD_MIN_LEN)})
116
    input_data_class = SelectFieldWithProps(lazy_gettext('Input Data Classification'),
1✔
117
                                            validators=[validators.Required()], choices=[], default='')
118
    output_data_class = SelectFieldWithProps(lazy_gettext('Output Data Classification'),
1✔
119
                                             validators=[validators.Required()], choices=[], default='')
120

121

122
class ProjectForm(ProjectCommonForm):
1✔
123
    def __init__(self, *args, **kwargs):
1✔
124
        super(ProjectForm, self).__init__(*args, **kwargs)
1✔
125
    long_description = TextAreaField(lazy_gettext('Long Description'),
1✔
126
                                     [validators.Required()])
127
    description = TextAreaField(lazy_gettext('Description'),
1✔
128
                                [validators.Length(max=255)])
129
    product = SelectField(lazy_gettext('Product'),
1✔
130
                          [validators.Required()], choices=[("", "")], default="")
131
    subproduct = SelectField(lazy_gettext('Subproduct'),
1✔
132
                             [validators.Required()], choices=[("", "")], default="")
133

134
    kpi = DecimalField(lazy_gettext('KPI - Estimate of amount of minutes to complete one task (0.1-120)'), places=2,
1✔
135
        validators=[validators.Required(), NumberRange(Decimal('0.1'), 120)])
136

137

138
class ProjectUpdateForm(ProjectForm):
1✔
139
    id = IntegerField(label=None, widget=HiddenInput())
1✔
140
    description = TextAreaField(lazy_gettext('Description'),
1✔
141
                            [validators.Required(
142
                                message=lazy_gettext(
143
                                    "You must provide a description.")),
144
                             validators.Length(max=255)])
145
    short_name = TextField(label=None, widget=HiddenInput())
1✔
146
    long_description = TextAreaField(lazy_gettext('Long Description'))
1✔
147
    allow_anonymous_contributors = BooleanField(lazy_gettext('Allow Anonymous Contributors'))
1✔
148
    private_instance = data_access.data_access_levels.get("valid_access_levels", []) == ["L1", "L2", "L3", "L4"]
1✔
149
    if not private_instance:
1✔
150
        allow_taskrun_edit = BooleanField(lazy_gettext('Allow Editing of Task Submissions'))
1✔
151
    duplicate_task_check_duplicate_fields = Select2Field(lazy_gettext('Task.info field names to be included in duplication check'), choices=[], validate_choice=False)
1✔
152
    duplicate_task_check_completed_tasks = BooleanField(lazy_gettext('Include completed tasks in task duplication check'))
1✔
153
    enable_duplicate_task_check = BooleanField(lazy_gettext('Enable task duplication check'))
1✔
154
    zip_download = BooleanField(lazy_gettext('Allow ZIP data download'))
1✔
155
    category_id = SelectField(lazy_gettext('Category'), coerce=int)
1✔
156
    hidden = BooleanField(lazy_gettext('Hide?'))
1✔
157
    email_notif = BooleanField(lazy_gettext('Email Notifications'))
1✔
158
    password = PasswordField(
1✔
159
                    lazy_gettext('Password'),
160
                    [validators.Optional(),
161
                        pb_validator.CheckPasswordStrength(
162
                                        min_len=PROJECT_PWD_MIN_LEN,
163
                                        special=False)],
164
                    render_kw={'placeholder': 'Minimum length {} characters, 1 uppercase, 1 lowercase and 1 numeric.'.format(PROJECT_PWD_MIN_LEN)})
165

166
    webhook = TextField(lazy_gettext('Webhook'),
1✔
167
                        [pb_validator.Webhook()])
168
    sync_enabled = BooleanField(lazy_gettext('Enable Project Syncing'))
1✔
169

170
    def set_duplicate_task_check_duplicate_fields_options(self, options):
1✔
171
        self.duplicate_task_check_duplicate_fields.choices = options
1✔
172

173
class AnnotationForm(Form):
1✔
174
    dataset_description = TextAreaField(lazy_gettext('Dataset Description'))
1✔
175
    provider = create_nullable_select(
1✔
176
        'Annotation Provider',
177
        ["PERSONNEL","VENDOR","CONTINGENT_WORKER","FREELANCER","CROWDSOURCING_WORKER"]
178
    )
179
    restrictions_and_permissioning = TextAreaField(lazy_gettext('Restrictions & Permissioning'))
1✔
180
    sampling_method = create_nullable_select(
1✔
181
        'Sampling Method',
182
        ["RANDOM","SYSTEMATIC","STRATIFIED","CLUSTERED"]
183
    )
184
    sampling_script = TextField(lazy_gettext('Sampling Script Link'))
1✔
185
    label_aggregation_strategy = create_nullable_select(
1✔
186
        'Label Aggregation Strategy',
187
        ["MAJORITY","WORKER_TRUST"]
188
    )
189
    task_input_schema = TextAreaField(lazy_gettext('Task Input Schema'))
1✔
190
    task_output_schema = TextAreaField(lazy_gettext('Task Output Schema'))
1✔
191

192
class ProjectSyncForm(Form):
1✔
193
    target_key = TextField(lazy_gettext('API Key'))
1✔
194

195

196
class ProjectQuizForm(Form):
1✔
197
    enabled = BooleanField(lazy_gettext('Enable Quiz Mode'))
1✔
198
    questions = IntegerField(
1✔
199
        lazy_gettext('Number of questions per quiz'),
200
        [
201
            validators.InputRequired(lazy_gettext('This field must be a positive integer.')),
202
            validators.NumberRange(min=1)
203
        ]
204
    )
205
    passing = IntegerField(
1✔
206
        lazy_gettext('Number of correct answers to pass quiz'),
207
        [
208
            validators.InputRequired(lazy_gettext('This field must be a non-negative integer.')),
209
            validators.NumberRange(min=0) # Making this 0 to allow quizzes with free-form answers.
210
        ]
211
    )
212
    completion_mode = SelectField(
1✔
213
        lazy_gettext('Completion mode'),
214
        choices=[
215
            ('all_questions', 'Present all the quiz questions'),
216
            ('short_circuit', 'End as soon as pass/fail status is known')
217
        ]
218
    )
219

220
    def validate_passing(form, field):
1✔
221
        correct = field.data
1✔
222
        total = form.questions.data
1✔
223
        if correct > total:
1✔
224
            raise validators.ValidationError(
1✔
225
                lazy_gettext(
226
                    'Correct answers required to pass (%(correct)d) is greater than the number of questions per quiz (%(total)d). \
227
                    It must be less than or equal to the number of questions per quiz.',
228
                    correct=correct,
229
                    total=total
230
                )
231
            )
232

233
class TaskPresenterForm(Form):
1✔
234
    id = IntegerField(label=None, widget=HiddenInput())
1✔
235
    editor = TextAreaField('')
1✔
236
    guidelines = TextAreaField('')
1✔
237

238
class TaskDefaultRedundancyForm(Form):
1✔
239
    default_n_answers = IntegerField(lazy_gettext('Default Redundancy'),
1✔
240
                           [validators.Required(),
241
                            validators.NumberRange(
242
                                min=task_repo.MIN_REDUNDANCY,
243
                                max=task_repo.MAX_REDUNDANCY,
244
                                message=lazy_gettext(
245
                                    'Number of answers should be a \
246
                                     value between {} and {:,}'.format(
247
                                        task_repo.MIN_REDUNDANCY,
248
                                        task_repo.MAX_REDUNDANCY
249
                                    )))])
250

251
class TaskRedundancyForm(Form):
1✔
252
    n_answers = IntegerField(lazy_gettext('Redundancy'),
1✔
253
                             [validators.Required(),
254
                              validators.NumberRange(
255
                                  min=task_repo.MIN_REDUNDANCY,
256
                                  max=task_repo.MAX_REDUNDANCY,
257
                                  message=lazy_gettext(
258
                                      'Number of answers should be a \
259
                                       value between {} and {:,}'.format(
260
                                          task_repo.MIN_REDUNDANCY,
261
                                          task_repo.MAX_REDUNDANCY
262
                                      )))])
263

264
class TaskPriorityForm(Form):
1✔
265
    task_ids = TextField(lazy_gettext('Task IDs'),
1✔
266
                         [validators.Required(),
267
                          pb_validator.CommaSeparatedIntegers()])
268

269
    priority_0 = DecimalField(lazy_gettext('Priority'),
1✔
270
                              [validators.NumberRange(
271
                                  min=0, max=1,
272
                                  message=lazy_gettext('Priority should be a \
273
                                                       value between 0.0 and 1.0'))])
274

275

276
class TaskTimeoutForm(Form):
1✔
277
    min_seconds = 30
1✔
278
    max_minutes = 210
1✔
279
    minutes = IntegerField(lazy_gettext('Minutes (default 60)'),
1✔
280
                           [validators.NumberRange(
281
                                min=0,
282
                                max=max_minutes
283
                            )])
284
    seconds = IntegerField(lazy_gettext('Seconds (0 to 59)'),
1✔
285
                           [validators.NumberRange(min=0, max=59)])
286

287
    def in_range(self):
1✔
288
        minutes = self.minutes.data or 0
1✔
289
        seconds = self.seconds.data or 0
1✔
290
        return self.min_seconds <= minutes*60 + seconds <= self.max_minutes*60
1✔
291

292

293
class TaskNotificationForm(Form):
1✔
294
    remaining = IntegerField(lazy_gettext('Notify when the number of remaining tasks is less than or equal to'))
1✔
295
    webhook = TextField(lazy_gettext('Webhook URL'))
1✔
296

297

298
class TaskSchedulerForm(Form):
1✔
299
    _translate_names = lambda variant: (variant[0], lazy_gettext(variant[1]))
1✔
300
    _choices = list(map(_translate_names, sched_variants()))
1✔
301
    sched = SelectField(lazy_gettext('Task Scheduler'), choices=_choices)
1✔
302
    customized_columns = Select2Field(lazy_gettext('Customized columns'), choices=[], default="")
1✔
303
    reserve_category_columns = Select2Field(lazy_gettext('Reserve task by columns'), choices=[], default="")
1✔
304
    rand_within_priority = BooleanField(lazy_gettext('Randomize Within Priority'))
1✔
305
    gold_task_probability_validator = validators.NumberRange(
1✔
306
        min=0,
307
        max=1,
308
        message=lazy_gettext('Gold task probability must be a value between 0.0 and 1.0')
309
    )
310
    gold_task_probability = DecimalField(
1✔
311
        label=lazy_gettext('Gold Probability'),
312
        validators=[gold_task_probability_validator],
313
        description=lazy_gettext('Probability value between 0 and 1')
314
    )
315

316
    def set_customized_columns_options(self, new_options):
1✔
317
        self.customized_columns.choices = new_options
1✔
318

319
    def set_reserve_category_cols_options(self, options):
1✔
320
        self.reserve_category_columns.choices = options
1✔
321

322
    @classmethod
1✔
323
    def update_sched_options(cls, new_options):
1✔
324
        _translate_names = lambda variant: (variant[0], lazy_gettext(variant[1]))
1✔
325
        _choices = list(map(_translate_names, new_options))
1✔
326
        cls.sched.kwargs['choices'] = _choices
1✔
327

328

329
class AnnouncementForm(Form):
1✔
330
    id = IntegerField(label=None, widget=HiddenInput())
1✔
331
    title = TextField(lazy_gettext('Title'),
1✔
332
                     [validators.Required(message=lazy_gettext(
333
                                    "You must enter a title for the post."))])
334
    body = TextAreaField(lazy_gettext('Body'),
1✔
335
                           [validators.Required(message=lazy_gettext(
336
                                    "You must enter some text for the post."))])
337
    info = TextField(lazy_gettext('Info'),
1✔
338
                       [validators.Required(message=lazy_gettext(
339
                                "You must enter a level for the post.")),
340
                       is_json(dict)])
341
    media_url = TextField(lazy_gettext('URL'))
1✔
342
    published = BooleanField(lazy_gettext('Publish'))
1✔
343

344

345
class BlogpostForm(Form):
1✔
346
    id = IntegerField(label=None, widget=HiddenInput())
1✔
347
    title = TextField(lazy_gettext('Title'),
1✔
348
                     [validators.Required(message=lazy_gettext(
349
                                    "You must enter a title for the post."))])
350
    body = TextAreaField(lazy_gettext('Body'),
1✔
351
                           [validators.Required(message=lazy_gettext(
352
                                    "You must enter some text for the post."))])
353
    published = BooleanField(lazy_gettext('Publish'))
1✔
354

355

356
class PasswordForm(Form):
1✔
357
    password = PasswordField(lazy_gettext('Password'),
1✔
358
                        [validators.Required(message=lazy_gettext(
359
                                    "You must enter a password"))])
360

361

362
class BulkTaskCSVImportForm(Form):
1✔
363
    form_name = TextField(label=None, widget=HiddenInput(), default='csv')
1✔
364
    msg_required = lazy_gettext("You must provide a URL")
1✔
365
    msg_url = lazy_gettext("Oops! That's not a valid URL. "
1✔
366
                           "You must provide a valid URL")
367
    csv_url = TextField(lazy_gettext('URL'),
1✔
368
                        [validators.Required(message=msg_required),
369
                         validators.URL(message=msg_url)])
370

371
    def get_import_data(self):
1✔
372
        return {'type': 'csv', 'csv_url': self.csv_url.data}
1✔
373

374

375
class BulkTaskGDImportForm(Form):
1✔
376
    form_name = TextField(label=None, widget=HiddenInput(), default='gdocs')
1✔
377
    msg_required = lazy_gettext("You must provide a URL")
1✔
378
    msg_url = lazy_gettext("Oops! That's not a valid URL. "
1✔
379
                           "You must provide a valid URL")
380
    googledocs_url = TextField(lazy_gettext('URL'),
1✔
381
                               [validators.Required(message=msg_required),
382
                                   validators.URL(message=msg_url)])
383

384
    def get_import_data(self):
1✔
385
        return {'type': 'gdocs', 'googledocs_url': self.googledocs_url.data}
1✔
386

387

388
class BulkTaskLocalCSVImportForm(Form):
1✔
389
    form_name = TextField(label=None, widget=HiddenInput(), default='localCSV')
1✔
390
    do_not_validate_tp = BooleanField(
1✔
391
        label=lazy_gettext("Do not require all fields used in task presenter code to be present in the csv file"),
392
        default=False
393
    )
394

395
    _allowed_extensions = set(['csv'])
1✔
396
    def _allowed_file(self, filename):
1✔
397
        return '.' in filename and \
1✔
398
            filename.rsplit('.', 1)[1] in self._allowed_extensions
399

400
    def _container(self):
1✔
401
        return "user_%d" % current_user.id
×
402

403
    def _upload_path(self):
1✔
404
        container = self._container()
×
405
        filepath = None
×
406
        if isinstance(uploader, local.LocalUploader):
×
407
            filepath = safe_join(uploader.upload_folder, container)
×
408
            if not os.path.isdir(filepath):
×
409
                os.makedirs(filepath)
×
410
            return filepath
×
411

412
        current_app.logger.error('Failed to generate upload path {0}'.format(filepath))
×
413
        raise IOError('Local Upload folder is missing: {0}'.format(filepath))
×
414

415
    def get_import_data(self):
1✔
416
        def get_csv_filename():
1✔
417
            if request.method != 'POST':
1✔
418
                return
×
419
            if 'file' not in request.files:
1✔
420
                return
1✔
421
            csv_file = request.files['file']
1✔
422
            if csv_file.filename == '':
1✔
423
                return
1✔
424
            if csv_file and self._allowed_file(csv_file.filename):
1✔
425
                return get_file_path_for_import_csv(csv_file)
1✔
426

427
        return {
1✔
428
            'type': 'localCSV',
429
            'csv_filename': get_csv_filename(),
430
            'validate_tp': not self.do_not_validate_tp.data
431
        }
432

433

434
class BulkTaskEpiCollectPlusImportForm(Form):
1✔
435
    form_name = TextField(label=None, widget=HiddenInput(), default='epicollect')
1✔
436
    msg_required = lazy_gettext("You must provide an EpiCollect Plus "
1✔
437
                                "project name")
438
    msg_form_required = lazy_gettext("You must provide a Form name "
1✔
439
                                     "for the project")
440
    epicollect_project = TextField(lazy_gettext('Project Name'),
1✔
441
                                   [validators.Required(message=msg_required)])
442
    epicollect_form = TextField(lazy_gettext('Form name'),
1✔
443
                                [validators.Required(message=msg_required)])
444

445
    def get_import_data(self):
1✔
446
        return {'type': 'epicollect',
1✔
447
                'epicollect_project': self.epicollect_project.data,
448
                'epicollect_form': self.epicollect_form.data}
449

450

451
class BulkTaskFlickrImportForm(Form):
1✔
452
    form_name = TextField(label=None, widget=HiddenInput(), default='flickr')
1✔
453
    msg_required = lazy_gettext("You must provide a valid Flickr album ID")
1✔
454
    album_id = TextField(lazy_gettext('Album ID'),
1✔
455
                         [validators.Required(message=msg_required)])
456
    def get_import_data(self):
1✔
457
        return {'type': 'flickr', 'album_id': self.album_id.data}
1✔
458

459

460
class BulkTaskDropboxImportForm(Form):
1✔
461
    form_name = TextField(label=None, widget=HiddenInput(), default='dropbox')
1✔
462
    files = FieldList(TextField(label=None, widget=HiddenInput()))
1✔
463
    def get_import_data(self):
1✔
464
        return {'type': 'dropbox', 'files': self.files.data}
1✔
465

466

467
class BulkTaskTwitterImportForm(Form):
1✔
468
    form_name = TextField(label=None, widget=HiddenInput(), default='twitter')
1✔
469
    msg_required = lazy_gettext("You must provide some source for the tweets")
1✔
470
    source = TextField(lazy_gettext('Source'),
1✔
471
                       [validators.Required(message=msg_required)])
472
    max_tweets = IntegerField(lazy_gettext('Number of tweets'))
1✔
473
    user_credentials = TextField(label=None)
1✔
474
    def get_import_data(self):
1✔
475
        return {
1✔
476
            'type': 'twitter',
477
            'source': self.source.data,
478
            'max_tweets': self.max_tweets.data,
479
            'user_credentials': self.user_credentials.data,
480
        }
481

482

483
class BulkTaskYoutubeImportForm(Form):
1✔
484
    form_name = TextField(label=None, widget=HiddenInput(), default='youtube')
1✔
485
    msg_required = lazy_gettext("You must provide a valid playlist")
1✔
486
    playlist_url = URLField(lazy_gettext('Playlist'),
1✔
487
                             [validators.Required(message=msg_required)])
488
    def get_import_data(self):
1✔
489
        return {
1✔
490
          'type': 'youtube',
491
          'playlist_url': self.playlist_url.data
492
        }
493

494
class BulkTaskS3ImportForm(Form):
1✔
495
    form_name = TextField(label=None, widget=HiddenInput(), default='s3')
1✔
496
    files = FieldList(TextField(label=None, widget=HiddenInput()))
1✔
497
    msg_required = lazy_gettext("You must provide a valid bucket")
1✔
498
    bucket = TextField(lazy_gettext('Bucket'),
1✔
499
                       [validators.Required(message=msg_required)])
500
    def get_import_data(self):
1✔
501
        return {
1✔
502
            'type': 's3',
503
            'files': self.files.data,
504
            'bucket': self.bucket.data
505
        }
506

507
class BulkTaskIIIFImportForm(Form):
1✔
508
    form_name = TextField(label=None, widget=HiddenInput(), default='iiif')
1✔
509
    msg_required = lazy_gettext("You must provide a URL")
1✔
510
    msg_url = lazy_gettext("Oops! That's not a valid URL. "
1✔
511
                           "You must provide a valid URL")
512
    manifest_uri = TextField(lazy_gettext('URL'),
1✔
513
                             [validators.Required(message=msg_required),
514
                             validators.URL(message=msg_url)])
515
    version = SelectField(lazy_gettext('Presentation API version'), choices=[
1✔
516
        (ctx, ctx) for ctx in ManifestReader.contexts
517
    ], default='2.1')
518

519
    def get_import_data(self):
1✔
520
        return {
1✔
521
            'type': 'iiif',
522
            'manifest_uri': self.manifest_uri.data,
523
            'version': self.version.data
524
        }
525

526

527
class GenericBulkTaskImportForm(object):
1✔
528
    """Callable class that will return, when called, the appropriate form
529
    instance"""
530
    _forms = {
1✔
531
        'csv': BulkTaskCSVImportForm,
532
        'gdocs': BulkTaskGDImportForm,
533
        'epicollect': BulkTaskEpiCollectPlusImportForm,
534
        'flickr': BulkTaskFlickrImportForm,
535
        'dropbox': BulkTaskDropboxImportForm,
536
        'twitter': BulkTaskTwitterImportForm,
537
        's3': BulkTaskS3ImportForm,
538
        'youtube': BulkTaskYoutubeImportForm,
539
        'localCSV': BulkTaskLocalCSVImportForm,
540
        'iiif': BulkTaskIIIFImportForm
541
    }
542

543
    def __call__(self, form_name, *form_args, **form_kwargs):
1✔
544
        if form_name is None:
1✔
545
            return None
1✔
546
        return self._forms[form_name](*form_args, **form_kwargs)
1✔
547

548

549
### Forms for account view
550

551
class LoginForm(Form):
1✔
552

553
    """Login Form class for signin into PYBOSSA."""
554

555
    email = TextField(lazy_gettext('E-mail'),
1✔
556
                      [validators.Required(
557
                          message=lazy_gettext("The e-mail is required"))])
558

559
    password = PasswordField(lazy_gettext('Password'),
1✔
560
                             [validators.Required(
561
                                 message=lazy_gettext(
562
                                     "You must provide a password"))])
563

564

565
class RegisterForm(Form):
1✔
566

567
    """Register Form Class for creating an account in PYBOSSA."""
568

569
    err_msg = lazy_gettext("Full name must be between 3 and %(fullname)s "
1✔
570
                           "characters long", fullname=USER_FULLNAME_MAX_LENGTH)
571
    fullname = TextField(lazy_gettext('Full name'),
1✔
572
                         [validators.Length(min=3, max=USER_FULLNAME_MAX_LENGTH, message=err_msg)])
573

574
    err_msg = lazy_gettext("User name must be between 3 and %(username_length)s "
1✔
575
                           "characters long", username_length=USER_NAME_MAX_LENGTH)
576
    err_msg_2 = lazy_gettext("The user name is already taken")
1✔
577
    name = TextField(lazy_gettext('User name'),
1✔
578
                         [
579
                          validators.Length(min=3, max=USER_NAME_MAX_LENGTH, message=err_msg),
580
                          pb_validator.NotAllowedChars(),
581
                          pb_validator.Unique(user_repo.get_by, 'name', err_msg_2),
582
                          pb_validator.ReservedName('account', current_app)])
583

584
    err_msg = lazy_gettext("Email must be between 3 and %(email_length)s "
1✔
585
                           "characters long", email_length=EMAIL_MAX_LENGTH)
586
    err_msg_2 = lazy_gettext("Email is already taken")
1✔
587
    email_addr = EmailField(lazy_gettext('Email Address'),
1✔
588
                           [validators.Length(min=3,
589
                                              max=EMAIL_MAX_LENGTH,
590
                                              message=err_msg),
591
                            validators.Email(),
592
                            pb_validator.UniqueCaseInsensitive(
593
                                user_repo.search_by_email,
594
                                'email_addr',
595
                                err_msg_2)])
596

597
    err_msg = lazy_gettext("Password cannot be empty")
1✔
598
    err_msg_2 = lazy_gettext("Passwords must match")
1✔
599
    if enable_strong_password:
1✔
600
        password = PasswordField(
×
601
                        lazy_gettext('New Password'),
602
                        [validators.Required(err_msg),
603
                         validators.EqualTo('confirm', err_msg_2),
604
                         pb_validator.CheckPasswordStrength()])
605
    else:
606
        password = PasswordField(
1✔
607
                        lazy_gettext('New Password'),
608
                        [validators.Required(err_msg),
609
                            validators.EqualTo('confirm', err_msg_2)])
610

611
    confirm = PasswordField(lazy_gettext('Repeat Password'))
1✔
612
    project_slug = SelectMultipleField(lazy_gettext('Project'), choices=[])
1✔
613
    consent = BooleanField(default='checked', false_values=("False", "false", '', '0', 0))
1✔
614

615
    def generate_password(self):
1✔
616
        if self.data['password']:
1✔
617
            return
1✔
618
        password = util.generate_password()
1✔
619
        self.password.data = password
1✔
620
        self.confirm.data = password
1✔
621

622

623
class UpdateProfileForm(Form):
1✔
624

625
    """Form Class for updating PYBOSSA's user Profile."""
626

627
    id = IntegerField(label=None, widget=HiddenInput())
1✔
628

629
    err_msg = lazy_gettext("Full name must be between 3 and %(fullname)s "
1✔
630
                           "characters long" , fullname=USER_FULLNAME_MAX_LENGTH)
631
    fullname = TextField(lazy_gettext('Full name'),
1✔
632
                         [validators.Length(min=3, max=USER_FULLNAME_MAX_LENGTH, message=err_msg)])
633

634
    err_msg = lazy_gettext("User name must be between 3 and %(username_length)s "
1✔
635
                           "characters long", username_length=USER_NAME_MAX_LENGTH)
636
    err_msg_2 = lazy_gettext("The user name is already taken")
1✔
637
    name = TextField(lazy_gettext('Username'),
1✔
638
                     [validators.Length(min=3, max=USER_NAME_MAX_LENGTH, message=err_msg),
639
                      pb_validator.NotAllowedChars(),
640
                      pb_validator.Unique(user_repo.get_by, 'name', err_msg_2),
641
                      pb_validator.ReservedName('account', current_app)])
642

643
    err_msg = lazy_gettext("Email must be between 3 and %(email_length)s "
1✔
644
                           "characters long", email_length=EMAIL_MAX_LENGTH)
645
    err_msg_2 = lazy_gettext("Email is already taken")
1✔
646
    email_addr = EmailField(lazy_gettext('Email Address'),
1✔
647
                           [validators.Length(min=3,
648
                                              max=EMAIL_MAX_LENGTH,
649
                                              message=err_msg),
650
                            validators.Email(),
651
                            pb_validator.Unique(user_repo.get_by, 'email_addr', err_msg_2)])
652
    subscribed = BooleanField(lazy_gettext('Get email notifications'))
1✔
653

654
    locale = SelectField(lazy_gettext('Language'))
1✔
655
    ckan_api = HiddenField(lazy_gettext('CKAN API Key'))
1✔
656
    privacy_mode = BooleanField(lazy_gettext('Privacy Mode'))
1✔
657
    restrict = BooleanField(lazy_gettext('Restrict processing'))
1✔
658

659
    def set_locales(self, locales):
1✔
660
        """Fill the locale.choices."""
661
        choices = []
1✔
662
        for locale in locales:
1✔
663
            choices.append(locale)
1✔
664
        self.locale.choices = choices
1✔
665

666

667
class ChangePasswordForm(Form):
1✔
668

669
    """Form for changing user's password."""
670

671
    current_password = PasswordField(lazy_gettext('Current password'))
1✔
672

673
    err_msg = lazy_gettext("Password cannot be empty")
1✔
674
    err_msg_2 = lazy_gettext("Passwords must match")
1✔
675
    if enable_strong_password:
1✔
676
        new_password = PasswordField(
×
677
                        lazy_gettext('New Password'),
678
                        [validators.Required(err_msg),
679
                            pb_validator.CheckPasswordStrength(),
680
                            validators.EqualTo('confirm', err_msg_2)])
681
    else:
682
        new_password = PasswordField(
1✔
683
                        lazy_gettext('New password'),
684
                        [validators.Required(err_msg),
685
                            validators.EqualTo('confirm', err_msg_2)])
686
    confirm = PasswordField(lazy_gettext('Repeat password'))
1✔
687

688

689
class ResetPasswordForm(Form):
1✔
690

691
    """Class for resetting user's password."""
692

693
    err_msg = lazy_gettext("Password cannot be empty")
1✔
694
    err_msg_2 = lazy_gettext("Passwords must match")
1✔
695
    if enable_strong_password:
1✔
696
        new_password = PasswordField(
×
697
                        lazy_gettext('New Password'),
698
                        [validators.Required(err_msg),
699
                            pb_validator.CheckPasswordStrength(),
700
                            validators.EqualTo('confirm', err_msg_2)])
701
    else:
702
        new_password = PasswordField(
1✔
703
                        lazy_gettext('New Password'),
704
                        [validators.Required(err_msg),
705
                            validators.EqualTo('confirm', err_msg_2)])
706
    confirm = PasswordField(lazy_gettext('Repeat Password'))
1✔
707

708

709
class ForgotPasswordForm(Form):
1✔
710

711
    """Form Class for forgotten password."""
712

713
    err_msg = lazy_gettext("Email must be between 3 and %(email_length)s "
1✔
714
                           "characters long", email_length=EMAIL_MAX_LENGTH)
715
    email_addr = EmailField(lazy_gettext('Email Address'),
1✔
716
                           [validators.Length(min=3,
717
                                              max=EMAIL_MAX_LENGTH,
718
                                              message=err_msg),
719
                            validators.Email()])
720

721

722
class PasswordResetKeyForm(Form):
1✔
723
    password_reset_key = TextAreaField(lazy_gettext('Password Reset Key'),
1✔
724
                                       [validators.Required(message=lazy_gettext(
725
                                        'You must provide the Password Reset Key.'))])
726

727

728
class OTPForm(Form):
1✔
729
    otp = TextField(lazy_gettext('One Time Password'),
1✔
730
                    [validators.Required(message=lazy_gettext(
731
                        'You must provide a valid OTP code'))])
732

733

734
### Forms for admin view
735

736
class SearchForm(Form):
1✔
737
    user = TextField(lazy_gettext('User'))
1✔
738

739

740
class CategoryForm(Form):
1✔
741
    id = IntegerField(label=None, widget=HiddenInput())
1✔
742
    name = TextField(lazy_gettext('Name'),
1✔
743
                     [validators.Required(),
744
                      pb_validator.Unique(project_repo.get_category_by, 'name',
745
                                          message="Name is already taken.")])
746
    description = TextField(lazy_gettext('Description'),
1✔
747
                            [validators.Required()])
748

749

750
### Common forms
751
class AvatarUploadForm(Form):
1✔
752
    id = IntegerField(label=None, widget=HiddenInput())
1✔
753
    avatar = FileField(lazy_gettext('Avatar'),
1✔
754
                       validators=[FileRequired(),
755
                                   FileAllowed(['png', 'jpg', 'jpeg', 'gif'])])
756
    x1 = IntegerField(label=None, widget=HiddenInput(), default=0)
1✔
757
    y1 = IntegerField(label=None, widget=HiddenInput(), default=0)
1✔
758
    x2 = IntegerField(label=None, widget=HiddenInput(), default=0)
1✔
759
    y2 = IntegerField(label=None, widget=HiddenInput(), default=0)
1✔
760

761

762
class BulkUserCSVImportForm(Form):
1✔
763
    form_name = TextField(label=None, widget=HiddenInput(), default='usercsvimport')
1✔
764
    _allowed_extensions = set(['csv'])
1✔
765
    def _allowed_file(self, filename):
1✔
766
        return '.' in filename and \
1✔
767
            filename.rsplit('.', 1)[1] in self._allowed_extensions
768

769
    def get_import_data(self):
1✔
770
        if request.method == 'POST':
1✔
771
            if 'file' not in request.files:
1✔
772
                flash('No file part')
1✔
773
                return {'type': 'usercsvimport', 'csv_filename': None}
1✔
774
            csv_file = request.files['file']
1✔
775
            if csv_file.filename == '':
1✔
776
                flash('No file selected')
×
777
                return {'type': 'usercsvimport', 'csv_filename': None}
×
778
            if csv_file and self._allowed_file(csv_file.filename):
1✔
779
                with NamedTemporaryFile(delete=False) as tmpfile:
1✔
780
                    csv_file.save(tmpfile)
1✔
781
                return {'type': 'usercsvimport', 'csv_filename': tmpfile.name}
1✔
782
        return {'type': 'usercsvimport', 'csv_filename': None}
×
783

784

785
class GenericUserImportForm(object):
1✔
786
    """Callable class that will return, when called, the appropriate form
787
    instance"""
788
    _forms = {'usercsvimport': BulkUserCSVImportForm}
1✔
789

790
    def __call__(self, form_name, *form_args, **form_kwargs):
1✔
791
        if form_name is None:
1✔
792
            return None
×
793
        return self._forms[form_name](*form_args, **form_kwargs)
1✔
794

795

796
class UserPrefMetadataForm(Form):
1✔
797
    """Form for admins to add metadata for users or for users to update their
798
    own metadata"""
799
    languages = Select2Field(
1✔
800
        lazy_gettext('Language(s)'), choices=[],default="")
801
    locations = Select2Field(
1✔
802
        lazy_gettext('Location(s)'), choices=[], default="")
803
    country_codes = Select2Field(
1✔
804
        lazy_gettext('Location - Country Code(s)'), choices=[], default="")
805
    country_names = Select2Field(
1✔
806
        lazy_gettext('Location - Country Name(s)'), choices=[], default="")
807
    work_hours_from = TimeField(
1✔
808
        lazy_gettext('Work Hours From'),
809
        [TimeFieldsValidator(["work_hours_to", "timezone"],
810
        message="Work Hours From, Work Hours To, and Timezone must be filled out for submission")],
811
        default='')
812
    work_hours_to = TimeField(
1✔
813
        lazy_gettext('Work Hours To'),
814
        [TimeFieldsValidator(["work_hours_from", "timezone"],
815
        message="Work Hours From, Work Hours To, and Timezone must be filled out for submission")],
816
        default='')
817
    timezone = SelectField(lazy_gettext('Timezone'),
1✔
818
        [TimeFieldsValidator(["work_hours_from", "work_hours_to"],
819
        message="Work Hours From, Work Hours To, and Timezone must be filled out for submission")],
820
        choices=[], default="")
821
    user_type = SelectField(
1✔
822
        lazy_gettext('Type of user'), [validators.Required()], choices=[], default="")
823
    if data_access.data_access_levels:
1✔
824
        data_access = Select2Field(
×
825
            lazy_gettext('Data Access(s)'), [validators.Required(),
826
                pb_validator.UserTypeValiadator()],
827
            choices=data_access.data_access_levels['valid_user_access_levels'], default="")
828
    review = TextAreaField(
1✔
829
        lazy_gettext('Additional comments'), default="")
830
    profile = TextAreaField(
1✔
831
        lazy_gettext("<div>Task Preferences</div>"
832
                     "<div style='color:red; font-weight:normal; font-size:12px;'>"
833
                     "Must not include sensitive or personally identifiable information, e.g., name, email address, phone number, UUID, race, gender, health or financial information.</div>"),
834
            [is_json(dict)], default="",
835
            render_kw={"placeholder": '{"finance": 0.5, "english": 0.8}'})
836

837
    def __init__(self, *args, **kwargs):
1✔
838
        Form.__init__(self, *args, **kwargs)
1✔
839
        self.set_can_update(kwargs.get('can_update', (True, None, None)))
1✔
840

841
    def set_upref_mdata_choices(self):
1✔
842
        upref_mdata_choices = app_settings.upref_mdata.get_upref_mdata_choices()
1✔
843
        self.languages.choices = upref_mdata_choices['languages']
1✔
844
        self.locations.choices = upref_mdata_choices['locations']
1✔
845
        self.country_codes.choices = upref_mdata_choices['country_codes']
1✔
846
        self.country_names.choices = upref_mdata_choices['country_names']
1✔
847
        self.timezone.choices = upref_mdata_choices['timezones']
1✔
848
        self.user_type.choices = upref_mdata_choices['user_types']
1✔
849

850
    def set_can_update(self, can_update_info):
1✔
851
        self._disabled = self._get_disabled_fields(can_update_info)
1✔
852
        self._hide_fields(can_update_info)
1✔
853

854
    def _get_disabled_fields(self, can_update_info):
1✔
855
        (can_update, disabled_fields, hidden_fields) = can_update_info
1✔
856
        if not can_update:
1✔
857
            return {field: 'Form is not updatable.' for field in self}
1✔
858
        return {getattr(self, name): reason for name, reason in six.iteritems(disabled_fields or {})}
1✔
859

860
    def _hide_fields(self, can_update_info):
1✔
861
        (can_update, disabled_fields, hidden_fields) = can_update_info
1✔
862
        if not can_update:
1✔
863
            return {field: 'Form is not updatable.' for field in self}
1✔
864
        for name, _ in six.iteritems(hidden_fields or {}):
1✔
865
            f = getattr(self, name)
1✔
866
            f.widget = HiddenInput()
1✔
867
            f.label=""
1✔
868

869
    def is_disabled(self, field):
1✔
870
        return self._disabled.get(field, False)
1✔
871

872
class TransferOwnershipForm(Form):
1✔
873
    email_addr = EmailField(lazy_gettext('Email of the new owner'))
1✔
874

875

876
class RegisterFormWithUserPrefMetadata(RegisterForm, UserPrefMetadataForm):
1✔
877
    """Create User Form that has ability to set user preferences and metadata"""
878
    consent = BooleanField(default='checked', false_values=("False", "false", '', '0', 0))
1✔
879

880

881
class DataAccessForm(Form):
1✔
882
    """Form to configure data access levels"""
883

884
    #for future extensions
885

886
class ProjectReportForm(Form):
1✔
887
    """Register Form Class for generating project report."""
888

889
    start_date = DateField(
1✔
890
        lazy_gettext('Start date'), format='%Y-%m-%d',
891
        validators=[
892
            validators.Optional(),
893
            pb_validator.NotInFutureValidator()
894
        ]
895
    )
896
    end_date = DateField(
1✔
897
        lazy_gettext('End date'), format='%Y-%m-%d',
898
        validators=[
899
            pb_validator.EndDateValidator(),
900
            validators.Optional(),
901
            pb_validator.NotInFutureValidator()
902
        ]
903
    )
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

© 2026 Coveralls, Inc