Ejemplo n.º 1
0
 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'],
     )
Ejemplo n.º 2
0
 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))
Ejemplo n.º 3
0
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}
Ejemplo n.º 4
0
 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
Ejemplo n.º 5
0
    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
Ejemplo n.º 6
0
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,
    )
Ejemplo n.º 7
0
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
Ejemplo n.º 8
0
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
Ejemplo n.º 9
0
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(
Ejemplo n.º 10
0
 class Schema:
     queryUuid = fields.String(attribute='query_uuid', nullable=True)
     userId = fields.Integer(attribute='user_id')
     queryBlob = fields.Any(attribute='query_blob')
Ejemplo n.º 11
0
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')