class DomainResource(ModelResource): projects = Relation("project") class Meta: model = Domain read_only_fields = (Domain.uuid.key, ) @ItemRoute.PATCH("/deactivate", rel="deactivate") @role_required(["admin"]) def deactivate(self, domain) -> fields.Boolean(): domain.is_active = False db.session.commit() return True @ItemRoute.PATCH("/activate", rel="activate") @role_required(["admin"]) def activate(self, domain) -> fields.Boolean(): domain.is_active = True db.session.commit() return True @Route.POST("", rel="create", schema=fields.Inline("self"), response_schema=fields.Inline("self")) @role_required(["admin"]) def create(self, properties): item = self.manager.create(properties) return item
class UserPermissionLink(ModelResource): class Meta: model = UserPermissionLinker @Route.POST("", rel="create", schema=fields.Inline("self"), response_schema=fields.Inline("self")) @role_required(["admin"]) def create(self, properties): return super().create(properties=properties) @Route.GET( lambda r: "/<{}:id>".format(r.meta.id_converter), rel="self", attribute="instance", response_schema=fields.Inline("self"), ) @role_required(["admin"]) def read(self, id): return super().read(id=id) @read.PATCH( rel="update", schema=fields.Inline("self", patchable=True), response_schema=fields.Inline("self", patchable=True), ) @role_required(["admin"]) def update(self, properties, id): item = self.manager.read(id) updated_item = self.manager.update(item, properties) return updated_item
class Schema: uuid = fields.UUID() user = fields.Inline('user') host = fields.Inline('host') start = fields.DateTimeString() end = fields.DateTimeString() duration = fields.Custom('{"type": "integer"}', io="r", formatter=lambda x: x.total_seconds())
class ProjectResource(ModelResource): class Meta: model = Project read_only_fields = (Project._uuid.key, ) @Route.POST("", rel="create", schema=fields.Inline("self"), response_schema=fields.Inline("self")) @role_required(["admin"]) def create(self, properties): return super().create(properties=properties) @Route.GET( lambda r: "/<{}:id>".format(r.meta.id_converter), rel="self", attribute="instance", response_schema=fields.Inline("self"), ) @role_required(["admin"]) def read(self, id): return super().read(id=id) @read.PATCH( rel="update", schema=fields.Inline("self", patchable=True), response_schema=fields.Inline("self", patchable=True), ) @role_required(["admin"]) def update(self, properties, id): item = self.manager.read(id) updated_item = self.manager.update(item, properties) return updated_item @ItemRoute.PATCH("/deactivate", rel="deactivate") @role_required(["admin"]) def deactivate(self, project) -> fields.Boolean(): project.is_active = False db.session.commit() return True @ItemRoute.PATCH("/activate", rel="activate") @role_required(["admin"]) def activate(self, project) -> fields.Boolean(): project.is_active = True db.session.commit() return True @ItemRoute.GET("/uuid", rel="uuid") @role_required(["admin"]) def uuid(self, project) -> fields.UUID(): return project.uuid
class Schema: province = fields.Inline('province') ward = fields.Inline('ward') district = fields.Inline('district') associate_group = fields.Inline('associate_group') province_id = fields.String() associate_group_id = fields.String() district_id = fields.String() ward_id = fields.String() _deleted_at = fields.DateString() _deleted_at._schema = c.DATETIME_SCHEMA
class Schema: owner_group = fields.Inline('group') owner_farmer = fields.Inline('farmer') certificate_start_date = fields.DateString() certificate_expiry_date = fields.DateString() certificate_expiry_date._schema = c.DATETIME_SCHEMA certificate_start_date._schema = c.DATETIME_SCHEMA owner_group_id = fields.String() owner_farmer_id = fields.String() _deleted_at = fields.DateString() _deleted_at._schema = c.DATETIME_SCHEMA
class Box(ModelResource): class Schema: description = fields.String() is_open = fields.Boolean(default=False) class Meta: model = 'box' manager = MemoryManager include_id = True include_type = True @ItemRoute.GET() def verbose_description(self, box): return box["description"] + " box" verbose_description.response_schema = fields.String() @ItemRoute.POST() def open(self, box): return self.manager.update(box, {"is_open": True}) @Route.GET(schema=FieldSet({'greeting': fields.String()})) def greet(self, greeting): return greeting + " box!" open.response_schema = fields.Inline('self', attribute='Test')
class LogResource(Resource): class Schema: level = fields.String(enum=['info', 'warning', 'error']) message = fields.String() class Meta: name = 'log' @Route.POST( '', rel="create", schema=fields.Inline('self'), response_schema=fields.Inline('self'), ) def create(self, properties): print('{level}: {message}'.format(**properties)) return properties
class UserResource(Resource): @Route.GET('/<int:id>', rel="self") def read(self, id): return {"name": "Foo", "age": 123} read.response_schema = fields.Inline("self") class Schema: name = fields.String() age = fields.PositiveInteger(nullable=True) class Meta: name = 'user'
def test_inline_schema(self): class FooResource(ModelResource): class Meta: name = "foo" class BarResource(ModelResource): class Meta: name = "bar" self.api.add_resource(FooResource) self.api.add_resource(BarResource) foo = fields.Inline(FooResource) foo.bind(BarResource) self.assertEqual({'$ref': '/foo/schema'}, foo.response)
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') # pylint: disable=E1101 @ItemRoute.GET('/by_query_uuid') # pylint: disable=R0201 def by_query_uuid(self, user_query_session): return { 'queryBlob': user_query_session.query_blob, 'queryUuid': user_query_session.query_uuid, 'username': user_query_session.user.username, } # pylint: disable=E1101 @Route.POST( '/generate_link', rel='generateLink', schema=fields.Inline('self'), response_schema=fields.String(), ) # pylint: disable=R0201 def generate_link(self, query_session): return store_query_and_generate_link(query_session)
def create(self, value: fields.Number()) -> fields.Inline('self'): return {"name": "foo", "value": value}
class UserResource(ModelResource): class Meta: model = User exclude_fields = [User._password_hash.key] read_only_fields = [User.uuid.key] @Route.GET("/profile/permissions") @auth_required def resolve_profile_permissions( self, project_id: fields.Integer(minimum=1) ) -> fields.List(fields.String()): permissions = (UserPermissionLinker.query.filter_by( project_id=project_id, user_id=current_identity.id).join( UserPermissionLinker.permission).with_entities( Permission.slug).all()) permissions = list(map(lambda x: x.slug, permissions)) return permissions @Route.GET("/profile") @auth_required def resolve_profile(self) -> fields.Inline("self"): return self.manager.read(current_identity.id) @ItemRoute.PATCH("/deactivate", rel="deactivate") @role_required(["admin"]) def deactivate(self, user) -> fields.Boolean(): user.is_active = False db.session.commit() return True @ItemRoute.PATCH("/activate", rel="activate") @role_required(["admin"]) def activate(self, user) -> fields.Boolean(): user.is_active = True db.session.commit() return True @ItemRoute.GET("/permissions") @role_required(["admin"]) def resolve_permissions( self, user, project_id: fields.Integer(minimum=1) ) -> fields.List(fields.String()): permissions = (UserPermissionLinker.query.filter_by( project_id=project_id, user_id=user.id).join( UserPermissionLinker.permission).with_entities( Permission.slug).all()) permissions = list(map(lambda x: x.slug, permissions)) return permissions @Route.POST("", rel="create", schema=fields.Inline("self"), response_schema=fields.Inline("self")) @role_required(["admin"]) def create(self, properties): return super().create(properties=properties) @Route.GET( lambda r: "/<{}:id>".format(r.meta.id_converter), rel="self", attribute="instance", response_schema=fields.Inline("self"), ) @role_required(["admin"]) def read(self, id): return super().read(id=id) @read.PATCH( rel="update", schema=fields.Inline("self", patchable=True), response_schema=fields.Inline("self", patchable=True), ) @role_required(["admin"]) def update(self, properties, id): item = self.manager.read(id) updated_item = self.manager.update(item, properties) return updated_item @ItemRoute.PATCH("/changePassword", rel="change_password") @role_required(["admin"]) def change_password( self, user, new_password: fields.String()) -> fields.Boolean(): user.password_hash = new_password db.session.commit() return True
def add_view(self, video) -> FieldSet({'count': fields.Integer(), 'view': fields.Inline(ViewResource)}): '''Create view and return view and count''' view = ViewResource().manager.create({ 'video_id': video.id }) return {'count': video.count, 'view': view}
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'} # Read Permissions are the defaults as defined in # `web.server.security.permissions.PERMISSION_DEFAULTS` # # 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` # Allow users to filter resources by name, username, status, and phone filters = { 'username': True, 'status': True, 'firstName': True, 'lastName': True, 'phoneNumber': True, } exclude_fields = ('password', 'reset_password_token') class Schema(object): username = USERNAME_SCHEMA roles = fields.Custom( ROLE_MAP_SCHEMA, description='The role(s) that the user currently possesses.', attribute='roles', formatter=role_list_as_map, default=[], ) firstName = fields.String( description='The first name of the user.', attribute='first_name', pattern=INTERNATIONALIZED_ALPHANUMERIC_AND_DELIMITER, ) lastName = fields.String( description='The last name of the user.', attribute='last_name', pattern=INTERNATIONALIZED_ALPHANUMERIC_AND_DELIMITER_OR_EMPTY, ) phoneNumber = fields.String( description='The phone number of the user.', attribute='phone_number') # Disabling this warning because Hyper-Schema is enforcing # that the value of this field MUST match one of the values # of the Enum. # pylint:disable=E1136 # pylint: disable=E1101 status = fields.Custom( fields.String(enum=USER_STATUSES), attribute='status_id', formatter=lambda status_id: UserStatusEnum(status_id).name.lower(), converter=lambda status_value: UserStatusEnum[status_value.upper()] .value, default=UserStatusEnum.ACTIVE.value, nullable=False, ) # The parameter is coming directly from the API which uses camelCase instead of # snake_case # pylint: disable=C0103 # pylint: disable=E1101 @ItemRoute.POST( '/password', title='Update User Password', description= 'Updates the user\'s password with the new value specified.', rel='updatePassword', schema=FieldSet({ 'newPassword': fields.String( min_length=10, max_length=255, description='The new password for the user.', ) }), ) def update_password(self, user, newPassword): # TODO(vedant) Refactor this into a separate module like # we do for the Groups API with AuthorizedOperation('change_password', 'user', user.id): hashed_password = current_app.user_manager.hash_password( newPassword) self.manager.update(user, {'password': hashed_password}) return None, NO_CONTENT @ItemRoute.POST( '/reset_password', title='Reset User Password', description= 'Resets the user\'s password and sends them a password reset email.', rel='resetPassword', schema=None, response_schema=None, ) def reset_password(self, user): with AuthorizedOperation('reset_password', 'user', user.id): # TODO(vedant) - Change the return value of `reset_password` to convey a value # with more resolution than True/False username = user.username # TODO(vedant) - This is for legacy users who signed up with usernames that do # not represent an e-mail address if not EMAIL_REGEX.match(username): message = 'User {username} does not have a valid e-mail address.'.format( username=username) return StandardResponse(message, BAD_REQUEST, False), BAD_REQUEST send_reset_password(username) message = ('User password has been reset and instructions ' 'e-mailed to {username}.'.format(username=username)) g.request_logger.info(message) return None, NO_CONTENT @ItemRoute.DELETE( '/force', rel='forceDestroy', title='Force Delete User', description= 'Force delete a User and ALL their created artifacts (Dashboards, etc.)', ) def force_delete(self, user): with AuthorizedOperation('delete_resource', 'user', user.id): before_delete.send(user) force_delete_user(user) after_delete.send(user) return None, NO_CONTENT @Route.POST( '/invite', title='Invite New User', description='', rel='inviteUser', schema=fields.List(INVITE_OBJECT_SCHEMA), response_schema=fields.List( fields.Inline('self'), description='A listing of all the invited users'), ) def invite_user(self, invitees): with AuthorizedOperation('invite_user', 'site', ROOT_SITE_RESOURCE_ID): invited_users = [invitee.email for invitee in invitees] try: pending_users = invite_users(invitees) emails = [user.username for user in pending_users] g.request_logger.info( 'Successfully invited the following users: \'%s\'.', emails) return pending_users except UserAlreadyInvited as e: g.request_logger.error( 'Attempt to invite the following users has failed: \'%s\'. ' 'The following users already have platform accounts: \'%s\'', invited_users, e.already_registered_users, ) raise # Flask-Potion requires that these be methods and NOT functions # pylint: disable=R0201 # pylint: disable=E1101 @ItemRoute.POST( '/roles', title='Add User Role', description='Adds a single role to a user.', schema=USER_ROLES_SCHEMA, response_schema=STANDARD_RESPONSE_SCHEMA, rel='addRole', ) def add_role_by_name(self, user, request): with AuthorizedOperation('edit_resource', 'user', user.id): # TODO(vedant) Refactor this into a separate module like # we do for the Groups API role_name = request['roleName'] resource_type = request['resourceType'] resource_name = request.get('resourceName') result = add_user_role_api(user, role_name, resource_type, resource_name, commit=True) success = False if isinstance(result, Success): success = True response_code = OK if success else BAD_REQUEST return ( StandardResponse(result['data']['message'], response_code, success), response_code, ) # Flask-Potion requires that these be methods and NOT functions # pylint: disable=R0201 @ItemRoute.DELETE( '/roles', title='Delete User Role', description='Deletes a single role from a user.', schema=USER_ROLES_SCHEMA, response_schema=STANDARD_RESPONSE_SCHEMA, rel='deleteRole', ) def delete_role_by_name(self, user, request): with AuthorizedOperation('edit_resource', 'user', user.id): # TODO(vedant) Refactor this into a separate module like # we do for the Groups API role_name = request['roleName'] resource_type = request['resourceType'] resource_name = request.get('resourceName') result = delete_user_role_api(user, role_name, resource_type, resource_name, commit=True) return StandardResponse(result['data']['message'], OK, True) @ItemRoute.PATCH( '/roles', title='Update User Roles', description= 'Updates all the roles for a user with the values specified.', schema=ROLE_MAP_SCHEMA, response_schema=fields.Inline('self'), rel='updateRoles', ) def update_roles(self, user, request): with AuthorizedOperation('edit_resource', 'user', user.id): roles = update_user_roles_from_map(user, request) message = ( 'Successfully updated the roles attached to user \'%s\'. ' 'New roles are now: \'%s\'' % (user.username, [user_role_as_dictionary(role) for role in roles])) g.request_logger.info(message) return self.manager.read(user.id) @Route.GET( '/default/roles', title='Get Default User Role(s)', description= 'Gets all role(s) that are possessed by all registered users and optionally, ' 'unregistered users.', rel='getDefaultRoles', ) @authorization_required('view_resource', 'user', is_api_request=True) def list_default_roles(self): return StandardResponse( 'Successfully retrieved a listing of all public roles. ', OK, True, roles=role_list_as_map(list_default_roles()), ) @Route.POST( '/default/roles', title='Add Default User Role', description= 'Adds a single role to a user that will apply to all registered users and ' 'optionally, unregistered users.', schema=DEFAULT_ROLES_SCHEMA, rel='addDefaultRole', ) @authorization_required('edit_resource', 'user', is_api_request=True) def add_default_role_by_name(self, request): role_name = request['roleName'] resource_type = request['resourceType'] resource_name = request.get('resourceName') (_, exists) = add_default_role(role_name, resource_type, resource_name) action = 'has been added' if exists else 'already exists' message = 'Role \'%s\' %s for %s' % ( role_name, action, get_resource_string(resource_name, resource_type), ) g.request_logger.info(message) response_code = OK if exists else CREATED return (StandardResponse(message, response_code, True), response_code) @Route.DELETE( '/default/roles', title='Delete Default User Role', description= 'Deletes a single role from a user that will apply to both anonymous ' '(unregistered) as well as registered users.', schema=DEFAULT_ROLES_SCHEMA, response_schema=STANDARD_RESPONSE_SCHEMA, rel='deletePublicRole', ) @authorization_required('delete_resource', 'user', is_api_request=True) def delete_default_role_by_name(self, request): role_name = request['roleName'] resource_type = request['resourceType'] resource_name = request.get('resourceName') (_, exists) = delete_default_role(role_name, resource_type, resource_name) action = 'has been deleted' if exists else 'does not exist' message = 'Role \'%s\' %s for %s' % ( role_name, action, get_resource_string(resource_name, resource_type), ) g.request_logger.info(message) return StandardResponse(message, OK, True)
def resolve_profile(self) -> fields.Inline("self"): return self.manager.read(current_identity.id)
class Schema: roles = fields.ToMany('role') associate_group = fields.Inline('associate_group') associate_group_id = fields.String() last_login_at = fields.DateTimeString() current_login_at = fields.DateTimeString()
def session_by_command(self, cmd: fields.String(), **kwargs) -> fields.List(fields.Inline('self')): return Session.query.join(Command).filter( Command.cmd.like('%{0}%'.format(cmd)))
class Schema: province = fields.Inline('province') id = fields.String() province_id = fields.String() _deleted_at = fields.DateString() _deleted_at._schema = c.DATETIME_SCHEMA
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
class Schema: group = fields.Inline('group') group_id = fields.String() _deleted_at = fields.DateString() _deleted_at._schema = c.DATETIME_SCHEMA
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 Schema: projects = fields.Array(fields.Inline('project'))