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

bloomberg / pybossa / 13476970231

22 Feb 2025 10:07PM UTC coverage: 94.149% (-0.007%) from 94.156%
13476970231

Pull #1047

github

dchhabda
add logs. file load duration. crud activity
Pull Request #1047: No Jira: Add logs. file load duration. crud activity

12 of 16 new or added lines in 2 files covered. (75.0%)

9 existing lines in 2 files now uncovered.

17604 of 18698 relevant lines covered (94.15%)

0.94 hits per line

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

97.35
/pybossa/api/api_base.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
"""
1✔
19
PYBOSSA api module for exposing domain objects via an API.
20

21
This package adds GET, POST, PUT and DELETE methods for any class:
22
    * projects,
23
    * tasks,
24
    * task_runs,
25
    * users,
26
    * etc.
27

28
"""
29
import json
1✔
30
from flask import request, abort, Response, current_app
1✔
31
from flask_login import current_user
1✔
32
from flask.views import MethodView
1✔
33
from flasgger import swag_from
1✔
34
from werkzeug.exceptions import NotFound, Unauthorized, Forbidden, BadRequest
1✔
35
from werkzeug.exceptions import MethodNotAllowed
1✔
36
from pybossa.util import jsonpify, fuzzyboolean, get_avatar_url
1✔
37
from pybossa.util import get_user_id_or_ip
1✔
38
from pybossa.core import ratelimits, uploader
1✔
39
from pybossa.auth import ensure_authorized_to
1✔
40
from pybossa.hateoas import Hateoas
1✔
41
from pybossa.ratelimit import ratelimit
1✔
42
from pybossa.error import ErrorStatus
1✔
43
from pybossa.core import project_repo, user_repo, task_repo, result_repo, auditlog_repo
1✔
44
from pybossa.core import announcement_repo, blog_repo, helping_repo, performance_stats_repo
1✔
45
from pybossa.core import project_stats_repo
1✔
46
from pybossa.model import DomainObject, announcement
1✔
47
from pybossa.model.task import Task
1✔
48
from pybossa.cache.projects import clean_project
1✔
49
from pybossa.cache.users import delete_user_summary_id
1✔
50
from pybossa.cache.categories import reset as reset_categories
1✔
51
from pybossa.cache.announcements import reset as reset_announcements
1✔
52

53
repos = {'Task': {'repo': task_repo, 'filter': 'filter_tasks_by',
1✔
54
                  'get': 'get_task', 'save': 'save', 'update': 'update',
55
                  'delete': 'delete'},
56
         'TaskRun': {'repo': task_repo, 'filter': 'filter_task_runs_by',
57
                     'get': 'get_task_run',  'save': 'save',
58
                     'update': 'update', 'delete': 'delete'},
59
         'User': {'repo': user_repo, 'filter': 'filter_by', 'get': 'get',
60
                  'save': 'save', 'update': 'update'},
61
         'Project': {'repo': project_repo, 'filter': 'filter_by',
62
                     'context': 'filter_owner_by', 'get': 'get',
63
                     'save': 'save', 'update': 'update', 'delete': 'delete'},
64
         'ProjectStats': {'repo': project_stats_repo, 'filter': 'filter_by',
65
                          'get': 'get'},
66
         'Category': {'repo': project_repo, 'filter': 'filter_categories_by',
67
                      'get': 'get_category', 'save': 'save_category',
68
                      'update': 'update_category',
69
                      'delete': 'delete_category'},
70
         'Result': {'repo': result_repo, 'filter': 'filter_by', 'get': 'get',
71
                    'update': 'update', 'save': 'save'},
72
         'Announcement': {'repo': announcement_repo, 'filter': 'filter_by', 'get': 'get',
73
                          'get_all_announcements': 'get_all_announcements',
74
                          'update': 'update', 'save': 'save', 'delete': 'delete'},
75
         'Blogpost': {'repo': blog_repo, 'filter': 'filter_by', 'get': 'get',
76
                      'update': 'update', 'save': 'save', 'delete': 'delete'},
77
         'HelpingMaterial': {'repo': helping_repo, 'filter': 'filter_by',
78
                             'get': 'get', 'update': 'update',
79
                             'save': 'save', 'delete': 'delete'},
80
         'PerformanceStats': {'repo': performance_stats_repo, 'filter': 'filter_by',
81
                              'get': 'get'},
82
         'Auditlog': {'repo': auditlog_repo, 'filter': 'filter_by',
83
                              'get': 'get'}
84
        }
85

86
caching = {'Project': {'refresh': clean_project},
1✔
87
           'User': {'refresh': delete_user_summary_id},
88
           'Category': {'refresh': reset_categories},
89
           'Announcement': {'refresh': reset_announcements}}
90

91
cors_headers = ['Content-Type', 'Authorization']
1✔
92

93
error = ErrorStatus()
1✔
94

95

96
class APIBase(MethodView):
1✔
97

98
    """Class to create CRUD methods."""
99

100
    hateoas = Hateoas()
1✔
101

102
    allowed_classes_upload = ['blogpost',
1✔
103
                              'helpingmaterial',
104
                              'announcement']
105

106
    immutable_keys = set(['short_name'])
1✔
107

108
    def refresh_cache(self, cls_name, oid):
1✔
109
        """Refresh the cache."""
110
        if caching.get(cls_name):
1✔
111
            if cls_name not in ['Category', 'Announcement']:
1✔
112
                caching.get(cls_name)['refresh'](oid)
1✔
113
            else:
114
                caching.get(cls_name)['refresh']()
1✔
115

116
    def valid_args(self):
1✔
117
        """Check if the domain object args are valid."""
118
        for k in request.args.keys():
1✔
119
            if k not in ['api_key']:
1✔
120
                getattr(self.__class__, k)
1✔
121

122
    def options(self, **kwargs):  # pragma: no cover
123
        """Return '' for Options method."""
124
        return ''
125

126
    @jsonpify
1✔
127
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
1✔
128
    @swag_from('docs/task/task_get.yaml', endpoint='api.api_task_oid')
1✔
129
    @swag_from('docs/project/project_get.yaml', endpoint='api.api_project_oid')
1✔
130
    def get(self, oid):
1✔
131
        """Get an object.
132

133
        Returns an item from the DB with the request.data JSON object or all
134
        the items if oid == None
135

136
        :arg self: The class of the object to be retrieved
137
        :arg integer oid: the ID of the object in the DB
138
        :returns: The JSON item/s stored in the DB
139

140
        """
141
        try:
1✔
142
            ensure_authorized_to('read', self.__class__)
1✔
143
            query = self._db_query(oid)
1✔
144
            json_response = self._create_json_response(query, oid)
1✔
145
            return Response(json_response, mimetype='application/json')
1✔
146
        except Exception as e:
1✔
147
            return error.format_exception(
1✔
148
                e,
149
                target=self.__class__.__name__.lower(),
150
                action='GET')
151

152
    def _create_json_response(self, query_result, oid):
1✔
153
        if len(query_result) == 1 and query_result[0] is None:
1✔
154
            raise abort(404)
1✔
155
        items = []
1✔
156
        for result in query_result:
1✔
157
            # This is for n_favs orderby case
158
            if not isinstance(result, DomainObject):
1✔
159
                if 'n_favs' in result.keys():
1✔
160
                    result = result[0]
1✔
161
            try:
1✔
162
                if (result.__class__ != self.__class__):
1✔
163
                    (item, headline, rank) = result
1✔
164
                else:
165
                    item = result
1✔
166
                    headline = None
1✔
167
                    rank = None
1✔
168
                if not self._verify_auth(item):
1✔
169
                    continue
1✔
170
                datum = self._create_dict_from_model(item)
1✔
171
                if headline:
1✔
172
                    datum['headline'] = headline
1✔
173
                if rank:
1✔
174
                    datum['rank'] = rank
1✔
175
                ensure_authorized_to('read', item)
1✔
176
                items.append(datum)
1✔
177
            except (Forbidden, Unauthorized):
1✔
178
                # pass as it is 401 or 403
179
                pass
1✔
180
            except Exception:  # pragma: no cover
181
                raise
182
        if oid is not None:
1✔
183
            if not items:
1✔
184
                raise Forbidden('Forbidden')
1✔
185
            ensure_authorized_to('read', query_result[0])
1✔
186
            self._sign_item(items[0])
1✔
187
            items = items[0]
1✔
188
        return json.dumps(items)
1✔
189

190
    def _create_dict_from_model(self, model):
1✔
191
        return self._select_attributes(self._add_hateoas_links(model))
1✔
192

193
    def _add_hateoas_links(self, item):
1✔
194
        obj = item.dictize()
1✔
195
        related = request.args.get('related')
1✔
196
        if related:
1✔
197
            if item.__class__.__name__ == 'Task':
1✔
198
                obj['task_runs'] = []
1✔
199
                obj['result'] = None
1✔
200
                task_runs = task_repo.filter_task_runs_by(task_id=item.id)
1✔
201
                results = result_repo.filter_by(task_id=item.id, last_version=True)
1✔
202
                for tr in task_runs:
1✔
203
                    obj['task_runs'].append(tr.dictize())
1✔
204
                for r in results:
1✔
205
                    obj['result'] = r.dictize()
1✔
206

207
            if item.__class__.__name__ == 'TaskRun':
1✔
208
                tasks = task_repo.filter_tasks_by(id=item.task_id)
1✔
209
                results = result_repo.filter_by(task_id=item.task_id, last_version=True)
1✔
210
                obj['task'] = None
1✔
211
                obj['result'] = None
1✔
212
                for t in tasks:
1✔
213
                    obj['task'] = t.dictize()
1✔
214
                for r in results:
1✔
215
                    obj['result'] = r.dictize()
1✔
216

217
            if item.__class__.__name__ == 'Result':
1✔
218
                tasks = task_repo.filter_tasks_by(id=item.task_id)
1✔
219
                task_runs = task_repo.filter_task_runs_by(task_id=item.task_id)
1✔
220
                obj['task_runs'] = []
1✔
221
                for t in tasks:
1✔
222
                    obj['task'] = t.dictize()
1✔
223
                for tr in task_runs:
1✔
224
                    obj['task_runs'].append(tr.dictize())
1✔
225

226
        stats = request.args.get('stats')
1✔
227
        if stats:
1✔
228
            if item.__class__.__name__ == 'Project':
1✔
229
                stats = project_stats_repo.filter_by()
1✔
230
                obj['stats'] = stats[0].dictize() if stats else {}
1✔
231

232
        links, link = self.hateoas.create_links(item)
1✔
233
        if links:
1✔
234
            obj['links'] = links
1✔
235
        if link:
1✔
236
            obj['link'] = link
1✔
237
        return obj
1✔
238

239
    def _db_query(self, oid):
1✔
240
        """Returns a list with the results of the query"""
241
        repo_info = repos[self.__class__.__name__]
1✔
242
        if oid is None:
1✔
243
            limit, offset, orderby = self._set_limit_and_offset()
1✔
244
            results = self._filter_query(repo_info, limit, offset, orderby)
1✔
245
        else:
246
            repo = repo_info['repo']
1✔
247
            query_func = repo_info['get']
1✔
248
            results = [getattr(repo, query_func)(oid)]
1✔
249
        return results
1✔
250

251
    def api_context(self, all_arg, **filters):
1✔
252
        if current_user.is_authenticated:
1✔
253
            filters['owner_id'] = current_user.id
1✔
254
        if filters.get('owner_id') and all_arg == '1':
1✔
255
            del filters['owner_id']
1✔
256
        return filters
1✔
257

258
    def _filter_query(self, repo_info, limit, offset, orderby):
1✔
259
        filters = {}
1✔
260
        for k in request.args.keys():
1✔
261
            if k not in ['limit', 'offset', 'api_key', 'last_id', 'all',
1✔
262
                         'fulltextsearch', 'desc', 'orderby', 'related',
263
                         'participated', 'full', 'stats',
264
                         'from_finish_time', 'to_finish_time', 'created_from', 'created_to']:
265
                # Raise an error if the k arg is not a column
266
                if self.__class__ == Task and k == 'external_uid':
1✔
267
                    pass
×
268
                else:
269
                    self._has_filterable_attribute(k)
1✔
270
                filters[k] = request.args[k]
1✔
271

272
        repo = repo_info['repo']
1✔
273
        filters = self.api_context(all_arg=request.args.get('all'), **filters)
1✔
274
        query_func = repo_info['filter']
1✔
275
        filters = self._custom_filter(filters)
1✔
276
        last_id = request.args.get('last_id')
1✔
277
        if request.args.get('participated'):
1✔
278
            filters['participated'] = get_user_id_or_ip()
1✔
279
        fulltextsearch = request.args.get('fulltextsearch')
1✔
280
        desc = request.args.get('desc') if request.args.get('desc') else False
1✔
281
        desc = fuzzyboolean(desc)
1✔
282

283
        if request.args.get('created_from'):
1✔
284
            filters['created_from'] = request.args.get('created_from')
1✔
285

286
        if request.args.get('created_to'):
1✔
287
            filters['created_to'] = request.args.get('created_to')
1✔
288

289
        if last_id:
1✔
290
            results = getattr(repo, query_func)(limit=limit, last_id=last_id,
1✔
291
                                                fulltextsearch=fulltextsearch,
292
                                                desc=False,
293
                                                orderby=orderby,
294
                                                **filters)
295
        else:
296
            results = getattr(repo, query_func)(limit=limit, offset=offset,
1✔
297
                                                fulltextsearch=fulltextsearch,
298
                                                desc=desc,
299
                                                orderby=orderby,
300
                                                **filters)
301
        return results
1✔
302

303
    def _set_limit_and_offset(self):
1✔
304
        try:
1✔
305
            limit = min(100, int(request.args.get('limit')))
1✔
306
        except (ValueError, TypeError):
1✔
307
            limit = 20
1✔
308
        try:
1✔
309
            offset = int(request.args.get('offset'))
1✔
310
        except (ValueError, TypeError):
1✔
311
            offset = 0
1✔
312
        try:
1✔
313
            orderby = request.args.get('orderby') if request.args.get('orderby') else 'id'
1✔
314
        except (ValueError, TypeError):
×
315
            orderby = 'updated'
×
316
        return limit, offset, orderby
1✔
317

318
    @jsonpify
1✔
319
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
1✔
320
    @swag_from('docs/task/task_post.yaml', endpoint='api.api_task')
1✔
321
    @swag_from('docs/project/project_post.yaml', endpoint='api.api_project')
1✔
322
    def post(self):
1✔
323
        """Post an item to the DB with the request.data JSON object.
324

325
        :arg self: The class of the object to be inserted
326
        :returns: The JSON item stored in the DB
327

328
        """
329
        try:
1✔
330
            cls_name = self.__class__.__name__
1✔
331
            data = None
1✔
332
            self.valid_args()
1✔
333
            self._preprocess_request(request)
1✔
334
            data = self._file_upload(request)
1✔
335
            if data is None:
1✔
336
                data = self._parse_request_data()
1✔
337
            original_data = self._copy_original(data)
1✔
338
            self._forbidden_attributes(data)
1✔
339
            self._restricted_attributes(data)
1✔
340
            self._preprocess_post_data(data)
1✔
341
            inst = self._create_instance_from_request(data)
1✔
342
            repo = repos[self.__class__.__name__]['repo']
1✔
343
            save_func = repos[self.__class__.__name__]['save']
1✔
344
            getattr(repo, save_func)(inst)
1✔
345
            self._after_save(original_data, inst)
1✔
346
            self._log_changes(None, inst)
1✔
347
            self.refresh_cache(cls_name, inst.id)
1✔
348
            response_dict = inst.dictize()
1✔
349
            self._customize_response_dict(response_dict)
1✔
350
            json_response = json.dumps(response_dict)
1✔
351
            current_app.logger.info("Created %s id %s", cls_name, json_response)
1✔
352
            return Response(json_response, mimetype='application/json')
1✔
353
        except Exception as e:
1✔
354
            return error.format_exception(
1✔
355
                e,
356
                target=self.__class__.__name__.lower(),
357
                action='POST')
358

359
    def _customize_response_dict(self, response_dict):
1✔
360
        """Method to be overridden by inheriting classes that want
361
        to modify the returned response to something other than
362
        the raw data from the DB."""
363
        pass
1✔
364

365
    def _parse_request_data(self):
1✔
366
        if 'request_json' in request.form:
1✔
367
            data = json.loads(request.form['request_json'])
1✔
368
        else:
369
            data = json.loads(request.data)
1✔
370
        return data
1✔
371

372
    def _preprocess_post_data(self, data):
1✔
373
        """Method to be overridden by inheriting classes that will
374
        perform preprocessing on the POST data"""
375
        pass
1✔
376

377
    def _preprocess_request(self, request):
1✔
378
        """Method to be overridden by inheriting classes that will
379
        perform preprocessong on the POST and PUT request"""
380
        pass
1✔
381

382
    def _create_instance_from_request(self, data):
1✔
383
        data = self.hateoas.remove_links(data)
1✔
384
        inst = self.__class__(**data)
1✔
385
        self._update_object(inst)
1✔
386
        ensure_authorized_to('create', inst)
1✔
387
        self._validate_instance(inst)
1✔
388
        return inst
1✔
389

390
    @jsonpify
1✔
391
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
1✔
392
    @swag_from('docs/task/task_delete.yaml', endpoint='api.api_task_oid')
1✔
393
    @swag_from('docs/project/project_delete.yaml', endpoint='api.api_project_oid')
1✔
394
    def delete(self, oid):
1✔
395
        """Delete a single item from the DB.
396

397
        :arg self: The class of the object to be deleted
398
        :arg integer oid: the ID of the object in the DB
399
        :returns: An HTTP status code based on the output of the action.
400

401
        More info about HTTP status codes for this action `here
402
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7>`_.
403

404
        """
405
        try:
1✔
406
            self.valid_args()
1✔
407
            self._delete_instance(oid)
1✔
408
            cls_name = self.__class__.__name__
1✔
409
            self.refresh_cache(cls_name, oid)
1✔
410
            current_app.logger.info("Deleted %s id %d", cls_name, oid)
1✔
411
            return Response('', 204, mimetype='application/json')
1✔
412
        except Exception as e:
1✔
413
            return error.format_exception(
1✔
414
                e,
415
                target=self.__class__.__name__.lower(),
416
                action='DELETE')
417

418
    def _delete_instance(self, oid):
1✔
419
        repo = repos[self.__class__.__name__]['repo']
1✔
420
        query_func = repos[self.__class__.__name__]['get']
1✔
421
        inst = getattr(repo, query_func)(oid)
1✔
422
        if inst is None:
1✔
423
            raise NotFound
1✔
424
        ensure_authorized_to('delete', inst)
1✔
425
        self._file_delete(request, inst)
1✔
426
        self._log_changes(inst, None)
1✔
427
        delete_func = repos[self.__class__.__name__]['delete']
1✔
428
        getattr(repo, delete_func)(inst)
1✔
429
        return inst
1✔
430

431
    @jsonpify
1✔
432
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
1✔
433
    @swag_from('docs/project/project_put.yaml', endpoint='api.api_project_oid')
1✔
434
    def put(self, oid):
1✔
435
        """Update a single item in the DB.
436

437
        :arg self: The class of the object to be updated
438
        :arg integer oid: the ID of the object in the DB
439
        :returns: An HTTP status code based on the output of the action.
440

441
        More info about HTTP status codes for this action `here
442
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6>`_.
443

444
        """
445
        try:
1✔
446
            self.valid_args()
1✔
447
            self._preprocess_request(request)
1✔
448
            cls_name = self.__class__.__name__
1✔
449
            repo = repos[cls_name]['repo']
1✔
450
            query_func = repos[cls_name]['get']
1✔
451
            existing = getattr(repo, query_func)(oid)
1✔
452
            if existing is None:
1✔
453
                raise NotFound
1✔
454
            ensure_authorized_to('update', existing)
1✔
455
            data = self._file_upload(request)
1✔
456
            inst = self._update_instance(existing, repo,
1✔
457
                                         repos,
458
                                         new_upload=data)
459
            self.refresh_cache(cls_name, oid)
1✔
460
            current_app.logger.info("Updated %s id %d", cls_name, oid)
1✔
461
            return Response(json.dumps(inst.dictize()), 200,
1✔
462
                            mimetype='application/json')
463
        except Exception as e:
1✔
464
            return error.format_exception(
1✔
465
                e,
466
                target=self.__class__.__name__.lower(),
467
                action='PUT')
468

469
    def _update_instance(self, existing, repo, repos, new_upload=None):
1✔
470
        data = dict()
1✔
471
        if new_upload is None:
1✔
472
            data = json.loads(request.data)
1✔
473
            new_data = data
1✔
474
        else:
475
            new_data = request.form
1✔
476
        self._forbidden_attributes(new_data)
1✔
477
        self._restricted_attributes(new_data)
1✔
478
        # Remove hateoas links
479
        data = self.hateoas.remove_links(data)
1✔
480
        # may be missing the id as we allow partial updates
481
        self.__class__(**data)
1✔
482
        old = self.__class__(**existing.dictize())
1✔
483
        for key in data:
1✔
484
            if key not in self.immutable_keys:
1✔
485
                setattr(existing, key, data[key])
1✔
486
            elif not (getattr(existing, key) == data[key]):
1✔
487
                raise Forbidden('Cannot change {} via API'.format(key))
1✔
488

489
        if new_upload:
1✔
490
            existing.media_url = new_upload['media_url']
1✔
491
            existing.info['container'] = new_upload['info']['container']
1✔
492
            existing.info['file_name'] = new_upload['info']['file_name']
1✔
493
        self._update_attribute(existing, old)
1✔
494
        update_func = repos[self.__class__.__name__]['update']
1✔
495
        self._validate_instance(existing)
1✔
496
        getattr(repo, update_func)(existing)
1✔
497
        self._log_changes(old, existing)
1✔
498
        return existing
1✔
499

500
    def _update_object(self, data_dict):
1✔
501
        """Update object.
502

503
        Method to be overriden in inheriting classes which wish to update
504
        data dict.
505

506
        """
507
        pass
1✔
508

509
    def _update_attribute(self, new, old):
1✔
510
        """Update object attribute if new value is passed.
511
        Method to be overriden in inheriting classes which wish to update
512
        data dict.
513

514
        """
515

516
    def _select_attributes(self, item_data):
1✔
517
        """Method to be overriden in inheriting classes in case it is not
518
        desired that every object attribute is returned by the API.
519
        """
520
        return item_data
1✔
521

522
    def _custom_filter(self, query):
1✔
523
        """Method to be overriden in inheriting classes which wish to consider
524
        specific filtering criteria.
525
        """
526
        return query
1✔
527

528
    def _has_filterable_attribute(self, attribute):
1✔
529
        """Method to be overridden by inheriting classes that want
530
        to have custom filterable attributes"""
531
        getattr(self.__class__, attribute)
1✔
532

533
    def _validate_instance(self, instance):
1✔
534
        """Method to be overriden in inheriting classes which may need to
535
        validate the creation (POST) or modification (PUT) of a domain object
536
        for reasons other than business logic ones (e.g. overlapping of a
537
        project name witht a URL).
538
        """
539
        pass
1✔
540

541
    def _after_save(self, original_data, instance):
1✔
542
        """Method to be overriden by inheriting classes to perform operations
543
        after new object has been saved
544
        """
545
        pass
1✔
546

547
    def _log_changes(self, old_obj, new_obj):
1✔
548
        """Method to be overriden by inheriting classes for logging purposes"""
549
        pass
1✔
550

551
    def _forbidden_attributes(self, data):
1✔
552
        """Method to be overriden by inheriting classes that will not allow for
553
        certain fields to be used in PUT or POST requests"""
UNCOV
554
        pass
×
555

556
    def _restricted_attributes(self, data):
1✔
557
        """Method to be overriden by inheriting classes that will restrict
558
        certain fields to be used in PUT or POST requests for certain users"""
559
        pass
1✔
560

561
    def _file_upload(self, data):
1✔
562
        """Method that must be overriden by the class to allow file uploads for
563
        only a few classes."""
564
        cls_name = self.__class__.__name__.lower()
1✔
565
        content_type = 'multipart/form-data'
1✔
566
        if (content_type in request.headers.get('Content-Type', []) and
1✔
567
                cls_name in self.allowed_classes_upload):
568
            tmp = dict()
1✔
569
            for key in request.form.keys():
1✔
570
                tmp[key] = request.form[key]
1✔
571

572
            if isinstance(self, announcement.Announcement):
1✔
573
                # don't check project id for announcements
574
                ensure_authorized_to('create', self)
1✔
575
                if tmp.get('info') is not None:
1✔
UNCOV
576
                    try:
×
UNCOV
577
                        tmp['info'] = json.loads(tmp['info'])
×
UNCOV
578
                    except ValueError:
×
UNCOV
579
                        raise BadRequest
×
580
                upload_method = current_app.config.get('UPLOAD_METHOD')
1✔
581
                if request.files.get('file') is None:
1✔
582
                    raise AttributeError
×
583
                _file = request.files['file']
1✔
584
                container = "user_%s" % current_user.id
1✔
585
            else:
586
                ensure_authorized_to('create', self.__class__,
1✔
587
                                     project_id=tmp['project_id'])
588
                project = project_repo.get(tmp['project_id'])
1✔
589
                upload_method = current_app.config.get('UPLOAD_METHOD')
1✔
590
                if request.files.get('file') is None:
1✔
591
                    raise AttributeError
1✔
592
                _file = request.files['file']
1✔
593
                if current_user.is_authenticated:
1✔
594
                    if current_user.admin:
1✔
595
                        container = "user_%s" % project.owner.id
1✔
596
                    else:
597
                        container = "user_%s" % current_user.id
1✔
598
                else:
UNCOV
599
                    container = "anonymous"
×
600
            uploader.upload_file(_file,
1✔
601
                                 container=container)
602
            avatar_absolute = current_app.config.get('AVATAR_ABSOLUTE')
1✔
603
            file_url = get_avatar_url(upload_method,
1✔
604
                                      _file.filename,
605
                                      container,
606
                                      avatar_absolute)
607
            tmp['media_url'] = file_url
1✔
608
            if tmp.get('info') is None:
1✔
609
                tmp['info'] = dict()
1✔
610
            tmp['info']['container'] = container
1✔
611
            tmp['info']['file_name'] = _file.filename
1✔
612
            return tmp
1✔
613
        else:
614
            return None
1✔
615

616
    def _file_delete(self, request, obj):
1✔
617
        """Delete file object."""
618
        cls_name = self.__class__.__name__.lower()
1✔
619
        if cls_name in self.allowed_classes_upload:
1✔
620
            keys = obj.info.keys()
1✔
621
            if 'file_name' in keys and 'container' in keys:
1✔
622
                ensure_authorized_to('delete', obj)
1✔
623
                uploader.delete_file(obj.info['file_name'],
1✔
624
                                     obj.info['container'])
625

626
    def _verify_auth(self, item):
1✔
627
        """Method to be overriden in inheriting classes for additional checks
628
        on the items to return
629
        """
630
        return True
1✔
631

632
    def _sign_item(self, item):
1✔
633
        """Apply custom signature"""
634
        pass
1✔
635

636
    def _copy_original(self, item):
1✔
637
        """change if need to keep some information about the original request"""
638
        return item
1✔
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