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

bloomberg / pybossa / 19482063855

18 Nov 2025 09:34PM UTC coverage: 93.533% (-0.5%) from 94.065%
19482063855

Pull #1075

github

dchhabda
modified boto2-3 migration
Pull Request #1075: RDISCROWD-8392: deprecate old boto. use boto3 only (Updated)

10 of 19 new or added lines in 3 files covered. (52.63%)

87 existing lines in 5 files now uncovered.

17703 of 18927 relevant lines covered (93.53%)

0.94 hits per line

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

97.16
/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
            message = f"Created {cls_name}"
1✔
352
            self._log_operation(message, info=response_dict)
1✔
353
            return Response(json_response, mimetype='application/json')
1✔
354
        except Exception as e:
1✔
355
            return error.format_exception(
1✔
356
                e,
357
                target=self.__class__.__name__.lower(),
358
                action='POST')
359

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

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

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

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

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

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

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

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

405
        """
406
        try:
1✔
407
            self.valid_args()
1✔
408
            self._delete_instance(oid)
1✔
409
            cls_name = self.__class__.__name__
1✔
410
            self.refresh_cache(cls_name, oid)
1✔
411
            message = f"Deleted {cls_name} id {oid}"
1✔
412
            self._log_operation(message)
1✔
413
            return Response('', 204, mimetype='application/json')
1✔
414
        except Exception as e:
1✔
415
            return error.format_exception(
1✔
416
                e,
417
                target=self.__class__.__name__.lower(),
418
                action='DELETE')
419

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

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

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

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

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

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

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

504
    def _update_object(self, data_dict):
1✔
505
        """Update object.
506

507
        Method to be overriden in inheriting classes which wish to update
508
        data dict.
509

510
        """
511
        pass
1✔
512

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

518
        """
519

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

526
    def _custom_filter(self, query):
1✔
527
        """Method to be overriden in inheriting classes which wish to consider
528
        specific filtering criteria.
529
        """
530
        return query
1✔
531

532
    def _has_filterable_attribute(self, attribute):
1✔
533
        """Method to be overridden by inheriting classes that want
534
        to have custom filterable attributes"""
535
        getattr(self.__class__, attribute)
1✔
536

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

545
    def _after_save(self, original_data, instance):
1✔
546
        """Method to be overriden by inheriting classes to perform operations
547
        after new object has been saved
548
        """
549
        pass
1✔
550

551
    def _log_changes(self, old_obj, new_obj):
1✔
552
        """Method to be overriden by inheriting classes for logging purposes"""
553
        pass
1✔
554

555
    def _forbidden_attributes(self, data):
1✔
556
        """Method to be overriden by inheriting classes that will not allow for
557
        certain fields to be used in PUT or POST requests"""
558
        pass
×
559

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

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

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

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

630
    def _verify_auth(self, item):
1✔
631
        """Method to be overriden in inheriting classes for additional checks
632
        on the items to return
633
        """
634
        return True
1✔
635

636
    def _sign_item(self, item):
1✔
637
        """Apply custom signature"""
638
        pass
1✔
639

640
    def _copy_original(self, item):
1✔
641
        """change if need to keep some information about the original request"""
642
        return item
1✔
643

644
    def _log_operation(self, message, info=None):
1✔
645
        """Log api operation with message and additonal info provided"""
646
        if not info:
1✔
647
            current_app.logger.info("%s", message)
1✔
648
            return
1✔
649

650
        log_info = [f"{key} {info[key]}" for key in ["id", "name", "short_name", "owner_id", "project_id"] if key in info]
1✔
651
        current_app.logger.info("%s %s", message, ", ".join(log_info))
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

© 2025 Coveralls, Inc