def put(self, folio_id): db_session = data_engine.db_get_session() try: # Get portfolio folio = data_engine.get_portfolio(folio_id, _db_session=db_session) if folio is None: raise DoesNotExistError(str(folio_id)) # Check permissions permissions_engine.ensure_portfolio_permitted( folio, FolioPermission.ACCESS_EDIT, get_session_user()) # Update the object params = self._get_validated_object_parameters(request.form) permissions_changed = self._set_permissions( folio, params, db_session) changes = [] if params['human_id'] != folio.human_id: changes.append('short URL changed') if params['name'] != folio.name: changes.append('name changed') if params['description'] != folio.description: changes.append('description changed') if permissions_changed: changes.append('permissions changed') folio.human_id = params['human_id'] or Folio.create_human_id() folio.name = params['name'] folio.description = params['description'] # Note: folio.last_updated is only for image changes # (to know when to invalidate the exported zips) data_engine.add_portfolio_history(folio, get_session_user(), FolioHistory.ACTION_EDITED, ', '.join(changes).capitalize(), _db_session=db_session, _commit=False) data_engine.save_object( folio, _db_session=db_session, _commit=True # fail here if human_id not unique ) if permissions_changed: permissions_engine.reset_portfolio_permissions() # Return a clean object the same as for get(id) folio = data_engine.get_portfolio(folio.id, load_images=True, load_history=True) folio = _prep_folio_object(folio) return make_api_success_response( object_to_dict(folio, _omit_fields)) finally: db_session.close()
def portfolio_download(human_id, filename): logger.debug('GET ' + request.url) try: # Find the portfolio folio = data_engine.get_portfolio(human_id=human_id) if not folio: raise DoesNotExistError('Portfolio \'%s\' does not exist' % human_id) # Ensure that the user has permission to download the portfolio user = get_session_user() permissions_engine.ensure_portfolio_permitted( folio, FolioPermission.ACCESS_DOWNLOAD, user) # Check that the filename is valid (note: assumes folio.downloads is eager loaded) if not filename: raise DoesNotExistError('No filename specified') folio_exports = [ dl for dl in folio.downloads if dl.filename == filename ] if not folio_exports: raise DoesNotExistError('Download \'%s\' is not available' % filename) folio_export = folio_exports[0] # The physical file should always exist when the data+filename exists # This also checks that the file path lies inside IMAGES_BASE_DIR zip_path = get_portfolio_export_file_path(folio_export) ensure_path_exists(zip_path, require_file=True) # Prepare to serve the file response = send_file( get_abs_path(zip_path), mimetype='application/zip', as_attachment=True, conditional=True, cache_timeout=31536000 # zips never change once created ) # Lastly write an audit record data_engine.add_portfolio_history(folio, user, FolioHistory.ACTION_DOWNLOADED, folio_export.filename) return response except httpexc.HTTPException: # Pass through HTTP 4xx and 5xx raise except SecurityError as e: if app.config['DEBUG']: raise log_security_error(e, request) raise httpexc.Forbidden() except DoesNotExistError as e: logger.warning('404 Not found: ' + str(e)) raise httpexc.NotFound(safe_error_str(e)) except Exception as e: if app.config['DEBUG']: raise logger.error('500 Error for ' + request.url + '\n' + str(e)) raise httpexc.InternalServerError(safe_error_str(e))
def delete(self, folio_id, export_id): db_session = data_engine.db_get_session() try: # Get the portfolio folio = data_engine.get_portfolio(folio_id, _db_session=db_session) if folio is None: raise DoesNotExistError(str(folio_id)) # Check permissions permissions_engine.ensure_portfolio_permitted( folio, FolioPermission.ACCESS_EDIT, get_session_user()) # Get the single portfolio-export folio_export = data_engine.get_object(FolioExport, export_id, _db_session=db_session) if folio_export is None: raise DoesNotExistError(str(export_id)) if folio_export.folio_id != folio_id: raise ParameterError( 'export ID %d does not belong to portfolio ID %d' % (export_id, folio_id)) # Delete it and the export files delete_portfolio_export(folio_export, get_session_user(), 'Deleted: ' + folio_export.describe(True), _db_session=db_session) return make_api_success_response() finally: db_session.close()
def get(self, folio_id, export_id=None): # Get the portfolio folio = data_engine.get_portfolio(folio_id) if folio is None: raise DoesNotExistError(str(folio_id)) # Check permissions permissions_engine.ensure_portfolio_permitted( folio, FolioPermission.ACCESS_VIEW, get_session_user()) if export_id is None: # List portfolio exports exports_list = [ _prep_folioexport_object(folio, fe) for fe in folio.downloads ] return make_api_success_response( object_to_dict_list(exports_list, _omit_fields)) else: # Get a single portfolio-export folio_export = data_engine.get_object(FolioExport, export_id) if folio_export is None: raise DoesNotExistError(str(export_id)) if folio_export.folio_id != folio_id: raise ParameterError( 'export ID %d does not belong to portfolio ID %d' % (export_id, folio_id)) return make_api_success_response( object_to_dict(_prep_folioexport_object(folio, folio_export), _omit_fields))
def get(self, folio_id, image_id=None): if image_id is None: # List images in the portfolio folio = data_engine.get_portfolio(folio_id, load_images=True) if folio is None: raise DoesNotExistError(str(folio_id)) # Check permissions permissions_engine.ensure_portfolio_permitted( folio, FolioPermission.ACCESS_VIEW, get_session_user()) image_list = [_prep_folioimage_object(fi) for fi in folio.images] return make_api_success_response( object_to_dict_list(image_list, _omit_fields)) else: # Get a single portfolio-image db_session = data_engine.db_get_session() try: folio_image = data_engine.get_portfolio_image( AttrObject(id=folio_id), AttrObject(id=image_id), _db_session=db_session) if folio_image is None: raise DoesNotExistError( str(folio_id) + '/' + str(image_id)) # Check permissions permissions_engine.ensure_portfolio_permitted( folio_image.portfolio, FolioPermission.ACCESS_VIEW, get_session_user()) return make_api_success_response( object_to_dict(_prep_folioimage_object(folio_image), _omit_fields + ['portfolio'])) finally: db_session.close()
def post(self): # Require folios or admin_folios permission to create a portfolio permissions_engine.ensure_permitted(SystemPermissions.PERMIT_FOLIOS, get_session_user()) db_session = data_engine.db_get_session() try: params = self._get_validated_object_parameters(request.form) folio = Folio(params['human_id'] or Folio.create_human_id(), params['name'], params['description'], get_session_user()) self._set_permissions(folio, params, db_session) data_engine.create_portfolio( folio, get_session_user(), _db_session=db_session, _commit=True # fail here if human_id not unique ) # Return a clean object the same as for get(id) folio = data_engine.get_portfolio(folio.id, load_images=True, load_history=True) folio = _prep_folio_object(folio) return make_api_success_response( object_to_dict(folio, _omit_fields)) finally: db_session.close()
def post(self, folio_id): db_session = data_engine.db_get_session() try: # Get the portfolio folio = data_engine.get_portfolio(folio_id, _db_session=db_session) if folio is None: raise DoesNotExistError(str(folio_id)) # Check portfolio permissions permissions_engine.ensure_portfolio_permitted( folio, FolioPermission.ACCESS_EDIT, get_session_user()) # Get the image by either ID or src params = self._get_validated_object_parameters(request.form, True) if 'image_id' in params: image = data_engine.get_image(params['image_id'], _db_session=db_session) if image is None: raise DoesNotExistError(str(params['image_id'])) else: image = auto_sync_file(params['image_src'], data_engine, task_engine, anon_history=True, burst_pdf=False, _db_session=db_session) if image is None or image.status == Image.STATUS_DELETED: raise DoesNotExistError(params['image_src']) # Check image permissions permissions_engine.ensure_folder_permitted( image.folder, FolderPermission.ACCESS_VIEW, get_session_user(), False) # Add history first so that we only commit once at the end data_engine.add_portfolio_history(folio, get_session_user(), FolioHistory.ACTION_IMAGE_CHANGE, '%s added' % image.src, _db_session=db_session, _commit=False) # Flag that exported zips are now out of date folio.last_updated = datetime.utcnow() # Add the image and commit changes db_folio_image = data_engine.save_object(FolioImage( folio, image, params['image_parameters'], params['filename'], params['index']), refresh=True, _db_session=db_session, _commit=True) return make_api_success_response( object_to_dict(_prep_folioimage_object(db_folio_image), _omit_fields + ['portfolio'])) finally: db_session.close()
def post(self, folio_id): db_session = data_engine.db_get_session() try: # Get the portfolio folio = data_engine.get_portfolio(folio_id, _db_session=db_session) if folio is None: raise DoesNotExistError(str(folio_id)) # Check permissions permissions_engine.ensure_portfolio_permitted( folio, FolioPermission.ACCESS_EDIT, get_session_user()) # Block the export now if it would create an empty zip file if len(folio.images) == 0: raise ParameterError( 'this portfolio is empty and cannot be published') # Create a folio-export record and start the export as a background task params = self._get_validated_object_parameters(request.form) folio_export = FolioExport(folio, params['description'], params['originals'], params['image_parameters'], params['expiry_time']) data_engine.add_portfolio_history(folio, get_session_user(), FolioHistory.ACTION_PUBLISHED, folio_export.describe(True), _db_session=db_session, _commit=False) folio_export = data_engine.save_object(folio_export, refresh=True, _db_session=db_session, _commit=True) export_task = task_engine.add_task( get_session_user(), 'Export portfolio %d / export %d' % (folio.id, folio_export.id), 'export_portfolio', { 'export_id': folio_export.id, 'ignore_errors': False }, Task.PRIORITY_NORMAL, 'info', 'error', 60) # Update and return the folio-export record with the task ID folio_export.task_id = export_task.id data_engine.save_object(folio_export, _db_session=db_session, _commit=True) return make_api_success_response(object_to_dict( _prep_folioexport_object(folio, folio_export), _omit_fields + ['portfolio']), task_accepted=True) finally: db_session.close()
def delete(self, folio_id): # Get portfolio folio = data_engine.get_portfolio(folio_id) if folio is None: raise DoesNotExistError(str(folio_id)) # Check permissions permissions_engine.ensure_portfolio_permitted( folio, FolioPermission.ACCESS_DELETE, get_session_user()) # Double check the downloads were eager-loaded before we try to use them below if not data_engine.attr_is_loaded(folio, 'downloads'): raise ValueError('bug: folio.downloads should be present') # Delete - cascades to folio images, permissions, history, and exports data_engine.delete_object(folio) # If we got this far the database delete worked and we now need to # delete the exported zip files (if any) delete_dir(get_portfolio_directory(folio), recursive=True) return make_api_success_response()
def get(self, folio_id=None): # v4.1 Added support for /api/portfolios/?human_id=<human id> human_id = request.args.get('human_id', '') if not folio_id and not human_id: # List portfolios that the user can view folio_list = data_engine.list_portfolios( get_session_user(), FolioPermission.ACCESS_VIEW) folio_list = [_prep_folio_object(f) for f in folio_list] return make_api_success_response( object_to_dict_list(folio_list, _omit_fields)) else: # Get single portfolio by either ID or v4.1 by human ID folio = data_engine.get_portfolio(folio_id, human_id, load_images=True, load_history=True) if folio is None: raise DoesNotExistError(human_id or str(folio_id)) # Check permissions permissions_engine.ensure_portfolio_permitted( folio, FolioPermission.ACCESS_VIEW, get_session_user()) folio = _prep_folio_object(folio) return make_api_success_response( object_to_dict(folio, _omit_fields))
def put(self, folio_id, image_id): db_session = data_engine.db_get_session() try: # Get data folio_image = data_engine.get_portfolio_image( AttrObject(id=folio_id), AttrObject(id=image_id), _db_session=db_session) if folio_image is None: raise DoesNotExistError(str(folio_id) + '/' + str(image_id)) # Check permissions permissions_engine.ensure_portfolio_permitted( folio_image.portfolio, FolioPermission.ACCESS_EDIT, get_session_user()) # Update the portfolio params = self._get_validated_object_parameters(request.form) chd_folio_image = data_engine.reorder_portfolio( folio_image, params['index']) data_engine.add_portfolio_history( folio_image.portfolio, get_session_user(), FolioHistory.ACTION_IMAGE_CHANGE, '%s moved to position %d' % (folio_image.image.src, chd_folio_image.order_num + 1), _db_session=db_session, _commit=True) # Return the updated image list db_session.expire(folio_image) folio = data_engine.get_portfolio(folio_id, load_images=True, _db_session=db_session) image_list = [_prep_folioimage_object(fi) for fi in folio.images] return make_api_success_response( object_to_dict_list(image_list, _omit_fields + ['portfolio'])) finally: db_session.close()
def portfolio_view(human_id): try: # Find the portfolio folio = data_engine.get_portfolio(human_id=human_id, load_images=True) if not folio: raise DoesNotExistError('Portfolio \'%s\' does not exist' % human_id) # Ensure that the user has permission to view the portfolio user = get_session_user() permissions_engine.ensure_portfolio_permitted( folio, FolioPermission.ACCESS_VIEW, user ) # Filter out images that the user can't view # so that we don't get broken images in the UI checked_folders = {} # cache folders already checked folio_images_1 = folio.images folio_images_2 = [] for fol_img in folio_images_1: folder_path = fol_img.image.folder.path if folder_path in checked_folders: folio_images_2.append(fol_img) elif permissions_engine.is_folder_permitted( folder_path, FolderPermission.ACCESS_VIEW, user, folder_must_exist=False # though it should exist! ): checked_folders[folder_path] = True folio_images_2.append(fol_img) # Replace the original image list with the filtered one folio.images = folio_images_2 # Generate the image viewing URLs, including any portfolio-specific changes web_view_params = { 'format': 'jpg', 'colorspace': 'srgb' } sizing_view_params = { 'width': 800, 'height': 800, 'size_fit': True } pre_sized_images = [ fol_img for fol_img in folio.images if fol_img.parameters and ( ('width' in fol_img.parameters and fol_img.parameters['width']['value']) or ('height' in fol_img.parameters and fol_img.parameters['height']['value']) ) ] for fol_img in folio.images: image_attrs = get_portfolio_image_attrs(fol_img, False, False, False) image_attrs.apply_dict(web_view_params, True, False, False) if len(pre_sized_images) == 0: image_attrs.apply_dict(sizing_view_params, True, False, False) # Here we normalise the attrs only after everything has been applied image_attrs.normalise_values() fol_img.url = url_for_image_attrs(image_attrs) return render_template( 'portfolio_view.html', title=folio.name, folio=folio, removed_count=(len(folio_images_1) - len(folio_images_2)) ) except Exception as e: # Although this isn't a JSON API, we're still using it like a viewing API, # so get the correct HTTP status code to return. create_api_error_dict() also # logs security errors so we don't need to do that separately here. error_dict = create_api_error_dict(e, logger) if app.config['DEBUG']: raise return render_template( 'portfolio_view.html', title='Portfolio', err_msg='This portfolio cannot be viewed: ' + safe_error_str(e) ), error_dict['status']