def test_object_pattern_schema(self): o = fields.Object(fields.Integer, pattern="[A-Z][0-9]+") self.assertEqual( { "type": "object", "additionalProperties": False, "patternProperties": { "[A-Z][0-9]+": { "type": "integer" } } }, o.response) o = fields.Object(pattern_properties={"[A-Z][0-9]+": fields.Integer}) self.assertEqual( { "type": "object", "additionalProperties": False, "patternProperties": { "[A-Z][0-9]+": { "type": "integer" } } }, o.response)
def test_attribute_mapped_no_pattern(self): o = fields.AttributeMapped( fields.Object({"foo": fields.Integer()}), mapping_attribute="key" ) self.assertEqual( [{'foo': 1, 'key': 'A3'}, {'foo': 1, 'key': 'B12'}], sorted( o.convert({"A3": {"foo": 1}, "B12": {"foo": 1}}), key=itemgetter("key") ), ) self.assertEqual( {"A3": {"foo": 1}, "B12": {"foo": 2}}, o.format([{'foo': 1, 'key': 'A3'}, {'foo': 2, 'key': 'B12'}]), ) self.assertEqual( { "type": "object", "additionalProperties": { "additionalProperties": False, "properties": {"foo": {"type": "integer"}}, "type": "object", }, }, o.response, )
def test_object_convert_pattern(self): o = fields.Object(fields.Integer, pattern="[A-Z][0-9]+") self.assertEqual({"A3": 1, "B12": 2}, o.convert({"A3": 1, "B12": 2})) with self.assertRaises(ValidationError): o.convert({"A2": "string"}) with self.assertRaises(ValidationError): o.convert({"Wrong": 1})
class FooResource(Resource): @Route.GET(lambda r: '/<{}:id>'.format(r.meta.id_converter), rel="self") def read(self, id): return {"id": id} read.response_schema = fields.Object({"id": fields.Integer()}) class Meta: name = 'foo' id_converter = 'int'
def test_object_convert(self): o = fields.Object(fields.Integer, nullable=False) self.assertEqual({"x": 123}, o.convert({"x": 123})) self.assertEqual(o.request, o.response) self.assertEqual( {"type": "object", "additionalProperties": {"type": "integer"}}, o.response ) with self.assertRaises(ValidationError): o.convert({"y": "string"}) with self.assertRaises(ValidationError): o.convert(None)
class Recipe(ModelResource): class Schema: name = fields.String() class Meta: model = 'recipe' manager = MemoryManager id_field_class = fields.Integer include_type = True ingredients = ItemAttributeRoute( fields.Array( fields.Object({ "ingredient": fields.String(), "amount": fields.String() })))
class DrinkResource(ModelResource): recipe = ItemAttributeRoute( fields.Array( fields.Object( properties={ "ingredient": fields.ToOne("ingredient"), "volume": fields.Number() }))) class Meta: name = "drink" model = name manager = MemoryManager id_field_class = fields.Integer include_type = True class Schema: name = fields.String()
def test_object_convert_pattern(self): o = fields.Object(fields.Integer, pattern="[A-Z][0-9]+") self.assertEqual({"A3": 1, "B12": 2}, o.convert({"A3": 1, "B12": 2})) self.assertEqual({ "type": "object", "additionalProperties": False, "patternProperties": { "[A-Z][0-9]+": {"type": "integer"} } }, o.response) with self.assertRaises(ValidationError): o.convert({"A2": "string"}) with self.assertRaises(ValidationError): o.convert({"Wrong": 1})
'A list of dimension values that the policy holder will be allowed to or prevented ' 'from querying on. To allow the user to query against all values, you would omit ' 'this field and instead set `allowAllValues` to true.', ) ALL_VALUES_SCHEMA = fields.Boolean( description= 'Set to true if the policy holder should be allowed to query across ALL values ' 'for the specified dimension.', default=False, nullable=True, ) DIMENSION_VALUES_SCHEMA = fields.Object({ 'excludeValues': SPECIFIC_VALUES_SCHEMA, 'includeValues': SPECIFIC_VALUES_SCHEMA, 'allValues': ALL_VALUES_SCHEMA, }) POLICY_FILTERS_SCHEMA = fields.Object( properties=DIMENSION_VALUES_SCHEMA, pattern_properties={ fields.String(title='dimensionType', description='The dimension to filter on'): DIMENSION_VALUES_SCHEMA }, pattern='|'.join(DIMENSIONS), attribute='policy_filters', description= 'A mapping of dimension names to the discrete dimension values that the ' 'policy holder is allowed to query data for. Example: For dimension '
def test_object_format_any(self): o = fields.Object(fields.Any) self.assertEqual({"x": 123}, o.format({"x": 123}))
ROLE_NAME_SCHEMA = fields.String( title='roleName', description= 'The string representation of a role name. (e.g. `dashboard_admin`)', ) # The schema for a mapping of resource names to role names # Structure should be # { <resourceName>: [<roleName>, <otherRoleName>], <otherResourceName>: [<roleName>], ... } RESOURCE_MAP_SCHEMA = fields.Object( properties=fields.List( ROLE_NAME_SCHEMA, description='A list of all the roles held for a given resource.', ), pattern_properties={ fields.String(title='resourceName', description='An individual resource.'): fields.List(ROLE_NAME_SCHEMA) }, nullable=False, description='A mapping of resource names to role names. ', ) # The schema representing the role assignments for an individual resource type # Structure should be # { # 'resources': { # <resourceName>: [<roleName>, <otherRoleName>], # <otherResourceName>: [<roleName>], ... # }, # 'sitewideRoles': [<roleName>, <otherRoleName>, ...]
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
# 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( description='The currently active view type when the query was saved', nullable=False, ), 'queryResultSpec': QUERY_RESULT_SPEC_SCHEMA, 'querySelections': SELECTIONS_SCHEMA, }, description='The request object to add a query to a dashboard', ) # The schema for the dashboard's specification. Because the specification is stored in the database # as a text value, we must serialize and deserialize as part of retrieval / update. # TODO(vedant) - We can actually define the entire Dashboard Schema here. # Determine if it's necessary. # HACK(vedant) - This is actually a hack. The `io=w` signals to Potion that this is a write-only # field when it is infact a readable field as well. The reason that we mark it as a write-only # field is to prevent it from being shown in the `GET` call to the API root. We only want this # field to be shown in the resource-specific `GET` call. The correct solution is to build this # behaviour into Flask-Potion (configurable fields depending on query view / resource view).
UPDATE_ROLE_PERMISSIONS_RESPONSE_SCHEMA = augment_standard_schema( ROLE_PERMISSIONS_FIELDS) ROLE_LIST_SCHEMA = fields.List( ROLE_NAME_SCHEMA, title='roles', description='A listing of roles held by a user or security group on an ' 'indvidual resource or all resources of a specific type.', ) DEFAULT_ROLE_SCHEMA = fields.Object({ 'roleName': ROLE_NAME_SCHEMA, 'applyToUnregistered': fields.Boolean( nullable=False, description= 'Indicates whether or not this role only applies to registered users. If ' 'set to `false` it only applies to registered users. If set to `false`, it ' 'applies to unregistered/anonymous users as well as long as public access ' 'is enabled.', ), }) USER_ROLES_MAPPING = fields.Object( properties=ROLE_LIST_SCHEMA, pattern_properties={ fields.String(title='username', description='The user\'s username'): ROLE_LIST_SCHEMA }, default=None, nullable=True,
Success, ) from web.server.potion.signals import after_user_role_change # The schema for an invite user request INVITE_OBJECT_FIELDS = { 'name': fields.String( description='The invited user\'s name.', pattern=INTERNATIONALIZED_ALPHANUMERIC_AND_DELIMITER, ), 'email': USERNAME_SCHEMA, } INVITE_OBJECT_SCHEMA = fields.Custom(fields.Object(INVITE_OBJECT_FIELDS), converter=lambda value: Invitee(**value)) class UserResource(PrincipalResource): '''The potion class for performing CRUD operations on the `User` class. ''' class Meta(object): title = 'Users API' description = ( 'The API through which CRUD operations can be performed on User(s).' ) model = User natural_key = 'username' permissions = {'read': 'yes'}