class Schema(object): key = fields.String(description='The unique name of the resource.', enum=CONFIGURATION_KEYS) value = fields.Custom( fields.Any(), attribute='key', description='The current value of the configuration setting. ', io='r', formatter=get_configuration, ) description = fields.Custom( fields.String(), attribute='key', description= 'An explanation of what the configuration setting does.', io='r', formatter=lambda key: _DEFAULT_CONFIGURATION_STORE[key][ 'description'], ) defaultValue = fields.Custom( fields.Any(), attribute='key', description='The default value of the setting.', io='r', formatter=lambda key: _DEFAULT_CONFIGURATION_STORE[key]['value'], )
def test_any(self): self.assertEqual(3, fields.Any().format(3)) self.assertEqual('Hi', fields.Any().format('Hi')) self.assertEqual(None, fields.Any().format(None)) self.assertEqual({}, fields.Any().format({})) self.assertEqual(1.23, fields.Any().format(1.23)) self.assertEqual(3, fields.Any().convert(3)) self.assertEqual('Hi', fields.Any().convert('Hi')) self.assertEqual(None, fields.Any().convert(None)) self.assertEqual(1.23, fields.Any().convert(1.23))
class ShareAnalysisResource(Resource): class Meta: name = 'share' title = 'Share Analysis API' description = 'The API for sharing analysis' # pylint: disable=no-member # pylint: disable=R0201 @Route.POST( '/email', title='Share analysis by email', description='Shares an analysis by email', schema=FieldSet(SHARE_ANALYSIS_EMAIL_SCHEMA), response_schema=fields.Any(), ) def share_by_email(self, **kwargs): shared_image_name = 'share_analysis.png' logger = g.request_logger if hasattr(g, 'request_logger') else LOG attachments = [("attachment", (attachment['filename'], attachment['content'])) for attachment in kwargs['attachments']] base64_image_str = kwargs.get('image_url') if base64_image_str: base64_image_str = base64_image_str.replace( 'data:image/png;base64,', '') attachments.append(( "inline", ( shared_image_name, base64.decodebytes(base64_image_str.encode('utf-8')), ), )) else: shared_image_name = '' for recipient in kwargs['recipients']: msg = current_app.email_renderer.create_share_analysis_email( subject=kwargs['subject'], recipient=recipient, reply_to=kwargs['sender'], body=kwargs['message'], attachments=attachments, query_url=kwargs['query_url'], image=shared_image_name, ) try: current_app.notification_service.send_email(msg) except NotificationError: error = 'Failed to send share analysis email to: \'%s\'' % recipient logger.error(error) raise BadGateway(error) message = (SHARE_ANALYSIS_PREVIEW_MESSAGE.format( recipient=','.join(kwargs['recipients'])) if kwargs.get( 'is_preview', False) else SHARE_ANALYSIS_MESSAGE) return {'message': message}
def overview(self) -> fields.Any(): res = [] for entry in WorkEntry.query.order_by(sqlalchemy.desc( WorkEntry.start)).all(): res.append({ 'client': entry.client.name, 'project': entry.project.name, 'date': entry.start.strftime("%d/%m/%y"), 'duration': entry.duration, 'message': entry.message, }) return res
def billing( self, client, month: fields.Integer(minimum=1, maximum=12)) -> fields.Any(): now = datetime.datetime.utcnow() _, stop_day = calendar.monthrange(now.year, month) start = datetime.datetime(now.year, month, 1, 1, 1, 1) stop = datetime.datetime(now.year, month, stop_day, 23, 59, 59) res = {'sum': 0, 'projects': []} for project in client.projects: p = {'name': project.name, 'duration': 0} for entry in project.workentries.filter(WorkEntry.start >= start, WorkEntry.start <= stop): p['duration'] += entry.duration res['projects'].append(p) res['sum'] += p['duration'] return res
INTERNATIONALIZED_ALPHANUMERIC_AND_DELIMITER = ( r'(^[A-zÀ-ÿ0-9]+[A-zÀ-ÿ0-9-_ ]*[A-zÀ-ÿ0-9]*)$') INTERNATIONALIZED_ALPHANUMERIC_AND_DELIMITER_OR_EMPTY = ( r'(^([A-zÀ-ÿ0-9]+[A-zÀ-ÿ0-9-_ ]*[A-zÀ-ÿ0-9]*))|(\s*)$') HISTORY_CHANGE_FIELDS = { 'user': fields.ItemUri('web.server.api.user_api_models.UserResource', attribute='user_id'), 'revisionId': fields.Integer(attribute='id', description='The unique history identifier'), 'objectUri': None, 'changes': fields.Any(attribute='changes', description='History item changes'), 'revisionDate': fields.DateTimeString(attribute='created', description='History item created date'), } def generate_history_record_schema(uri_field, resource_name): updated_fields = HISTORY_CHANGE_FIELDS.copy() updated_fields['objectUri'] = uri_field return alchemy_fields.InlineModel( updated_fields, description='A history record for {resource_name}'.format( resource_name=resource_name), model=HistoryRecord, )
class ConfigurationResource(PrincipalResource): '''The potion class for performing CRUD operations on the `Configuration` class. ''' class Meta(object): model = Configuration id_attribute = 'key' id_field_class = fields.String # Read Permissions are conferred upon all users. # # Create, Update and Delete Permissions are enforced by the # Signal Handlers installed when the API for this Resource # are initialized in `web.server.security.signal_handlers.py` permissions = {'read': 'yes'} read_only_fields = ('description', 'key') exclude_fields = ('overwritten_value', 'overwritten') # Allow API users to filter on resources by name and value filters = {'key': True, 'value': True} class Schema(object): key = fields.String(description='The unique name of the resource.', enum=CONFIGURATION_KEYS) value = fields.Custom( fields.Any(), attribute='key', description='The current value of the configuration setting. ', io='r', formatter=get_configuration, ) description = fields.Custom( fields.String(), attribute='key', description= 'An explanation of what the configuration setting does.', io='r', formatter=lambda key: _DEFAULT_CONFIGURATION_STORE[key][ 'description'], ) defaultValue = fields.Custom( fields.Any(), attribute='key', description='The default value of the setting.', io='r', formatter=lambda key: _DEFAULT_CONFIGURATION_STORE[key]['value'], ) # Flask Potion requires this to be an instance method. # pylint: disable=R0201, E1101 @ItemRoute.POST( '/reset', title='Reset configuration', description='Resets the value of the configuration to its default.', schema=None, response_schema=fields.Inline('self'), rel='reset', ) def reset_configuration(self, configuration): with AuthorizedOperation( 'edit_resource', 'configuration', configuration.id), Transaction() as transaction: key = configuration.key default_value = _DEFAULT_CONFIGURATION_STORE[key]['value'] old_value = get_configuration(key) message = ( 'The configuration for \'%s\' is being reset to its default value. ' 'The existing value is \'%s\'. ' 'The new (and default) value is \'%s\'. ') % (key, old_value, default_value) g.request_logger.info(message) # By setting `overwritten` to False, we are signifying that we want to have the default # value apply. For housekeeping reasons, we also set `overwritten_value` back to None. configuration.overwritten_value = None configuration.overwritten = False transaction.add_or_update(configuration, flush=True) # Restart gunicorn when a datasource is selected. restart_gunicorn_on_datasource_change(key, old_value, default_value) return configuration # Flask Potion requires this to be an instance method. # pylint: disable=R0201, E1101 @ItemRoute.POST( '/set', title='Set configuration', description= 'Updates the value of the configuration to the value provided.', schema=fields.Any(), response_schema=fields.Inline('self'), rel='set', ) def set_configuration(self, configuration, updated_value): with AuthorizedOperation( 'edit_resource', 'configuration', configuration.id), Transaction() as transaction: key = configuration.key try: old_value = get_configuration(key) assert_valid_configuration(key, updated_value) message = ('The configuration for \'%s\' is being updated. ' 'The existing value is \'%s\'. ' 'The new value is \'%s\'. ') % (key, old_value, updated_value) g.request_logger.info(message) except Exception as e: raise BadRequest(description=e.message) configuration.overwritten_value = (updated_value, ) configuration.overwritten = True transaction.add_or_update(configuration, flush=True) # NOTE(vedant): I have NO idea why the object is stored by default as an array so we # always unpack it and reset the value. configuration.overwritten_value = configuration.overwritten_value[ 0] transaction.add_or_update(configuration) # Restart gunicorn when a new datasource is selected. restart_gunicorn_on_datasource_change(key, old_value, updated_value) return configuration
class DashboardResource(PrincipalResource): '''The potion class for performing CRUD operations on the `Dashboard` class. ''' resource = Relation('resource', io='r') author = Relation('user', io='r') class Meta(object): manager = principals(DashboardManager) model = Dashboard natural_key = 'slug' excluded_fields = ('id',) id_attribute = 'resource_id' permissions = {'read': 'view_resource'} filters = { 'slug': True, 'title': True, 'created': True, 'author': {'eq': UserFilter, None: UserFilter}, 'isOfficial': True, } class Schema(object): title = TITLE_SCHEMA slug = NULLABLE_SLUG_SCHEMA description = DESCRIPTION_SCHEMA specification = SPECIFICATION_SCHEMA authorUsername = AUTHOR_USERNAME_SCHEMA author = AUTHOR_URI_SCHEMA resource = RESOURCE_URI_SCHEMA created = CREATED_SCHEMA isOfficial = IS_OFFICIAL_SCHEMA # NOTE(stephen): These fields are now pulled **from metadata** not from # the Dashboard model directly. They are only represented here to make # flask potion happy EVEN THOUGH THEY WILL NEVER EVER BE SENT BY THE # CLIENT, FLASK POTION STILL COMPLAINS. lastModified = UNUSED_LAST_MODIFIED_SCHEMA totalViews = UNUSED_TOTAL_VIEWS_SCHEMA # HACK(stephen): Attaching dashboard metadata to the base dashboard model # response is like fitting a square peg into a round hole. To add that # information in ways that Flask-Potion would naturally work causes us to # issue a huge number of queries per dashboard (previously 14 queries per # dashboard with a naive implementation). This query encapsulates all the # information needed for the dashboard response into a single query. # TODO(stephen): If we have to write workarounds like this, it probably # means we shouldn't be jamming too much information into a single API. def _attach_metadata_to_query(self, query): # NOTE(stephen): I don't think a transaction is necessary for this # read only query, but it is an easy way to access the session. with Transaction() as transaction: session = transaction._session subquery = DashboardUserMetadata.summary_by_dashboard_for_user( session, current_user.id ).subquery() return ( query # Join in the summarized metadata for each dashboard. .outerjoin(subquery, Dashboard.id == subquery.c.dashboard_id) # Attach user info so we can extract the author username. .outerjoin(User, Dashboard.author_id == User.id) # Make sure all the metadata columns are included. .add_columns(subquery) # Also include all dashboard columns since otherwise a new query # will be issued EACH TIME we access a dashboard in the query # result. .add_columns(Dashboard.__table__) # Manually set up author_username since hybrid properties weren't # transferring. .add_columns(User.username.label('author_username')) ) # HACK(stephen): To ensure endpoints that return a single dashboard also # include the appropriate metadata, we must join in the dashboard metadata # to our single dashboard query. def _get_single_dashboard_with_metadata(self, resource_id): # NOTE(stephen): The resource_id being filtered on here is **not** # Dashboard.id. This is because we use the `resource_id` column for # lookups and reference. query = self.manager._query().filter(self.manager.id_column == resource_id) return self._attach_metadata_to_query(query).one() # pylint: disable=R0201 # pylint: disable=E1101 # Flask Potion does not allow class methods. # Override the default "get all dashboards" route to augment the response # with dashboard specific metadata. @Route.GET( '', rel='instances', title='Something', description='Something', schema=Instances(), response_schema=fields.Array(fields.Object(DASHBOARD_SIMPLE_FIELDS)), ) def get_instances(self, page, per_page, where, sort): base_query = self.manager.instances(where, sort) # NOTE(stephen): I'm not sure why this wouldn't exist, but I think it # only happens when there are no dashboards in the DB. if not base_query: return [] return self._attach_metadata_to_query(base_query).paginate(page, per_page).items @Route.POST( '/upgrade', title='Upgrade Dashboard Specification', description='Upgrades the provided dashboard specification to the ' 'latest schema version supported by the server.', schema=fields.Any(), rel='upgrade', ) def upgrade_dashboard(self, dashboard_specification): # The validation is being done in the conversion defined by SPECIFICATION_SCHEMA. If there # are any errors, they will be thrown as an exception to the client. No action needs to be # taken here. return format_and_upgrade_specification(dashboard_specification) @ItemRoute.POST( '/visualization', title='Add Simple Query Visualization', description='Adds an item from Simple Query Tool to the dashboard.', schema=ADD_QUERY_TO_DASHBOARD_SCHEMA, response_schema=DETAILED_DASHBOARD_SCHEMA, rel='addVisualization', ) def add_visualization(self, dashboard, request): resource_id = dashboard.resource.id with AuthorizedOperation('edit_resource', 'dashboard', resource_id): _add_visualization(dashboard, request, False) track_dashboard_access(dashboard.id, True) return self._get_single_dashboard_with_metadata(resource_id) @ItemRoute.POST( '/visualization/advanced', title='Add Advanced Query Visualization', description='Adds an item from Advanced Query Tool to the dashboard.', schema=ADD_QUERY_TO_DASHBOARD_SCHEMA, response_schema=DETAILED_DASHBOARD_SCHEMA, rel='addAdvancedVisualization', ) def add_advanced_visualization(self, dashboard, request): resource_id = dashboard.resource.id with AuthorizedOperation('edit_resource', 'dashboard', resource_id): _add_visualization(dashboard, request, True) track_dashboard_access(dashboard.id, True) return self._get_single_dashboard_with_metadata(resource_id) @ItemRoute.POST( '/transfer', title='Transfer Ownership', description='Transfers the ownership of this dashboard from the current' 'owner to the one specified. ', schema=USER_URI_SCHEMA, ) def transfer_ownership(self, dashboard, new_author): with AuthorizedOperation( 'update_users', 'dashboard', dashboard.resource_id ), AuthorizedOperation('view_resource', 'user', dashboard.author.id): new_author = lookup_author(author_id=new_author) api_transfer_dashboard_ownership(dashboard, new_author) return None, NO_CONTENT @ItemRoute.POST( '/transfer/username', title='Transfer Ownership', description='Transfers the ownership of this dashboard from the current' 'owner to the one specified. ', schema=USERNAME_SCHEMA, ) def transfer_ownership_by_username(self, dashboard, new_author): with AuthorizedOperation( 'update_users', 'dashboard', dashboard.resource_id ), AuthorizedOperation('view_resource', 'user', dashboard.author.id): new_author = lookup_author(author_username=new_author) api_transfer_dashboard_ownership(dashboard, new_author) return None, NO_CONTENT @Route.POST( '/transfer', title='Transfer Ownership', description='Transfers the ownership of ALL dashboards from one user ' 'to another.', schema=fields.Object( {'sourceAuthor': USER_URI_SCHEMA, 'targetAuthor': USER_URI_SCHEMA} ), ) @authorization_required('update_users', 'dashboard') def transfer_bulk_ownership(self, request): source_author = lookup_author(author_id=request['sourceAuthor']) target_author = lookup_author(author_id=request['targetAuthor']) api_bulk_transfer_dashboard_ownership(source_author, target_author) return None, NO_CONTENT @Route.POST( '/transfer/username', title='Transfer Ownership', description='Transfers the ownership of ALL dashboards from one user ' 'to another.', schema=fields.Object( {'sourceAuthor': USERNAME_SCHEMA, 'targetAuthor': USERNAME_SCHEMA} ), ) @authorization_required('update_users', 'dashboard') def transfer_bulk_ownership_by_username(self, request): source_author = lookup_author(author_username=request['sourceAuthor']) target_author = lookup_author(author_username=request['targetAuthor']) api_bulk_transfer_dashboard_ownership(source_author, target_author) return None, NO_CONTENT @ItemRoute.POST( '/official', title='Update Dashboard \'official\' flag', description='Marks a Dashboard as official or not.', schema=fields.Boolean( description='The updated value of the "isOfficial" flag for the ' 'dashboard.' ), ) @authorization_required('publish_resource', 'dashboard') def set_official(self, dashboard, is_official): self.manager.update(dashboard, {'is_official': is_official}) return None, NO_CONTENT @ItemRoute.POST( '/favorite', title='Update Dashboard \'favorite\' flag', description='Marks a Dashboard as a user favorite or not.', schema=fields.Boolean( description='The updated value of the "isFavorite" flag for the ' 'dashboard.' ), ) def set_favorite(self, dashboard, is_favorite): with AuthorizedOperation( 'view_resource', 'dashboard', dashboard.id ), Transaction() as transaction: metadata = get_or_create_metadata(transaction, dashboard.id) metadata.is_favorite = is_favorite transaction.add_or_update(metadata) return None, NO_CONTENT # NOTE(vedant): Why are we overriding the default potion route for GET <{}:id> and # PATCH <{}:id>? # # We want to only display the dashboard specification when an individual dashboard is # requested. To do this, we have to tailor the response schema to include the specification # field which would otherwise NOT be included in the default Schema specified in the `Schema` # subclass of `DashboardResource`. # # The rationale for only display the complete dashboard specification is that specifications # have a tendency to be very large and we don't want to send a lot of useless data over the # wire unless it is specifically asked for by the client. It is also very unlikely that a # client will ever be loading more than one dashboard specification at a given time. @Route.GET( lambda r: '/<{}:id>'.format(r.meta.id_converter), rel='self', attribute='instance', response_schema=DETAILED_DASHBOARD_SCHEMA, ) def read(self, id): with Transaction() as transaction: dashboard = super(DashboardResource, self).read(id) dashboard.total_views += 1 track_dashboard_access(dashboard.id) dashboard = transaction.add_or_update(dashboard, flush=True) return self._get_single_dashboard_with_metadata(id) @Route.PATCH( lambda r: '/<{}:id>'.format(r.meta.id_converter), rel='update', schema=fields.Inline('self', patchable=True), response_schema=DETAILED_DASHBOARD_SCHEMA, ) def update(self, properties, id): dashboard = super(DashboardResource, self).update(properties, id) track_dashboard_access(dashboard.id, edited=True) return self._get_single_dashboard_with_metadata(id) @ItemRoute.GET( '/history', title='Dashboard Update History', schema=Instances(), description='Gets dashboard history data', rel='getDashboardHistory', response_schema=DASHBOARD_CHANGES_SCHEMA, ) # pylint: disable=W0613 # Method signature requires where and sort params def get_history(self, dashboard, page, per_page, where, sort): records = [] with Transaction() as transaction: records = ( transaction.find_all_by_fields( HistoryRecord, {'object_id': dashboard.resource_id, 'object_type': self.meta.name}, ) .paginate(page, per_page) .items ) return records
from web.server.util.util import EMAIL_PATTERN, as_dictionary from web.server.errors import NotificationError # TODO(vedant) - Allow users to change their Dashboard Slugs via the Settings # Modal. Once this is done, we will restrict the ability to use whitespaces # in dashboard slugs. DASHBOARD_SLUG_PATTERN = r'(^[a-zA-Z0-9-_ ]*)$' # The schema for the `selections` object that is POSTed to the `visualization` API. This # essentially represents the selections generated by the Query Tool. We are not specifying # A detailed schema for this object as it is not really necessary at this stage since the # selections object is in flux and if the structure ever changes without this schema definition # being altered, any APIs that reference this schema will reject the client request since it # will not match the defined schema. SELECTIONS_SCHEMA = fields.Custom( fields.Any(), description='The selections object containing the query to be added to a dashboard.', ) # The schema for the `query_result_spec` object that is POSTed to the # `visualization` API. This represents the frontend configuration for a query # result (e.g. custom fields, filters, settings, etc.) We are not specifying a # detailed schema for this object yet, but we should. QUERY_RESULT_SPEC_SCHEMA = fields.Custom( fields.Any(), description='The query result spec object describing a query result and its configuration to be added to a dashboard.', ) ADD_QUERY_TO_DASHBOARD_SCHEMA = fields.Object( properties={ 'activeViewType': fields.String(
class Schema: queryUuid = fields.String(attribute='query_uuid', nullable=True) userId = fields.Integer(attribute='user_id') queryBlob = fields.Any(attribute='query_blob')
from flask_potion import fields from flask_potion.routes import ItemRoute, Route from flask_potion.schema import FieldSet from web.server.routes.views.user_query_session import store_query_and_generate_link from web.server.api.api_models import PrincipalResource from models.alchemy.user_query_session.model import UserQuerySession GET_BY_QUERY_UUID_RESPONSE_SCHEMA = FieldSet({ 'queryBlob': fields.Any(), 'queryUuid': fields.String(), 'username': fields.String(), }) class UserQuerySessionResource(PrincipalResource): '''Potion class for interacting with saved queries. ''' class Meta: model = UserQuerySession filters = {'queryUuid': True} id_attribute = 'query_uuid' id_field_class = fields.String() permissions = {'read': 'view_resource'} class Schema: queryUuid = fields.String(attribute='query_uuid', nullable=True) userId = fields.Integer(attribute='user_id') queryBlob = fields.Any(attribute='query_blob')