Esempio n. 1
0
 def __init__(self, name=None, category=None,
              endpoint=None, url=None,
              template='admin/index.html'):
     super(AdminIndexView, self).__init__(name or babel.lazy_gettext('Home'),
                                          category,
                                          endpoint or 'admin',
                                          url or '/admin',
                                          'static')
     self._template = template
Esempio n. 2
0
 def __init__(self, name=None, category=None,
              endpoint=None, url=None,
              template='admin/index.html',
              menu_class_name=None,
              menu_icon_type=None,
              menu_icon_value=None):
     super(AdminIndexView, self).__init__(name or babel.lazy_gettext('Home'),
                                          category,
                                          endpoint or 'admin',
                                          url or '/admin',
                                          'static',
                                          menu_class_name=menu_class_name,
                                          menu_icon_type=menu_icon_type,
                                          menu_icon_value=menu_icon_value)
     self._template = template
def test_column_label_translation():
    app, db, admin = setup()

    Model1, _ = create_models(db)

    app.config['BABEL_DEFAULT_LOCALE'] = 'es'
    Babel(app)

    label = lazy_gettext('Name')

    view = CustomModelView(Model1, db.session,
                           column_list=['test1', 'test3'],
                           column_labels=dict(test1=label),
                           column_filters=('test1',))
    admin.add_view(view)

    client = app.test_client()

    rv = client.get('/admin/model1/?flt1_0=test')
    eq_(rv.status_code, 200)
    ok_('{"Nombre":' in rv.data.decode('utf-8'))
Esempio n. 4
0
 def operation(self):
     return lazy_gettext('exact')
Esempio n. 5
0
class ModelView(BaseModelView):
    """
        MongoEngine model scaffolding.
    """

    column_filters = None
    """
        Collection of the column filters.

        Can contain either field names or instances of
        :class:`flask_admin.contrib.mongoengine.filters.BaseMongoEngineFilter`
        classes.

        Filters will be grouped by name when displayed in the drop-down.

        For example::

            class MyModelView(BaseModelView):
                column_filters = ('user', 'email')

        or::

            from flask_admin.contrib.mongoengine.filters import BooleanEqualFilter

            class MyModelView(BaseModelView):
                column_filters = (BooleanEqualFilter(column=User.name, name='Name'),)

        or::

            from flask_admin.contrib.mongoengine.filters import BaseMongoEngineFilter

            class FilterLastNameBrown(BaseMongoEngineFilter):
                def apply(self, query, value):
                    if value == '1':
                        return query.filter(self.column == "Brown")
                    else:
                        return query.filter(self.column != "Brown")

                def operation(self):
                    return 'is Brown'

            class MyModelView(BaseModelView):
                column_filters = [
                    FilterLastNameBrown(
                        column=User.last_name, name='Last Name',
                        options=(('1', 'Yes'), ('0', 'No'))
                    )
                ]
    """

    model_form_converter = CustomModelConverter
    """
        Model form conversion class. Use this to implement custom
        field conversion logic.

        Custom class should be derived from the
        `flask_admin.contrib.mongoengine.form.CustomModelConverter`.

        For example::

            class MyModelConverter(AdminModelConverter):
                pass


            class MyAdminView(ModelView):
                model_form_converter = MyModelConverter
    """

    object_id_converter = ObjectId
    """
        Mongodb ``_id`` value conversion function. Default is `bson.ObjectId`.
        Use this if you are using String, Binary and etc.

        For example::

            class MyModelView(BaseModelView):
                object_id_converter = int

        or::

            class MyModelView(BaseModelView):
                object_id_converter = str
    """

    filter_converter = FilterConverter()
    """
        Field to filter converter.

        Override this attribute to use a non-default converter.
    """

    column_type_formatters = DEFAULT_FORMATTERS
    """
        Customized type formatters for MongoEngine backend
    """

    allowed_search_types = (mongoengine.StringField, mongoengine.URLField,
                            mongoengine.EmailField)
    """
        List of allowed search field types.
    """

    form_subdocuments = None
    """
        Subdocument configuration options.

        This field accepts dictionary, where key is field name and value is either dictionary or instance of the
        `flask_admin.contrib.mongoengine.EmbeddedForm`.

        Consider following example::

            class Comment(db.EmbeddedDocument):
                name = db.StringField(max_length=20, required=True)
                value = db.StringField(max_length=20)

            class Post(db.Document):
                text = db.StringField(max_length=30)
                data = db.EmbeddedDocumentField(Comment)

            class MyAdmin(ModelView):
                form_subdocuments = {
                    'data': {
                        'form_columns': ('name',)
                    }
                }

        In this example, `Post` model has child `Comment` subdocument. When generating form for `Comment` embedded
        document, Flask-Admin will only create `name` field.

        It is also possible to use class-based embedded document configuration::

            class CommentEmbed(EmbeddedForm):
                form_columns = ('name',)

            class MyAdmin(ModelView):
                form_subdocuments = {
                    'data': CommentEmbed()
                }

        Arbitrary depth nesting is supported::

            class SomeEmbed(EmbeddedForm):
                form_excluded_columns = ('test',)

            class CommentEmbed(EmbeddedForm):
                form_columns = ('name',)
                form_subdocuments = {
                    'inner': SomeEmbed()
                }

            class MyAdmin(ModelView):
                form_subdocuments = {
                    'data': CommentEmbed()
                }

        There's also support for forms embedded into `ListField`. All you have
        to do is to create nested rule with `None` as a name. Even though it
        is slightly confusing, but that's how Flask-MongoEngine creates
        form fields embedded into ListField::

            class Comment(db.EmbeddedDocument):
                name = db.StringField(max_length=20, required=True)
                value = db.StringField(max_length=20)

            class Post(db.Document):
                text = db.StringField(max_length=30)
                data = db.ListField(db.EmbeddedDocumentField(Comment))

            class MyAdmin(ModelView):
                form_subdocuments = {
                    'data': {
                        'form_subdocuments': {
                            None: {
                                'form_columns': ('name',)
                            }
                        }

                    }
                }
    """
    def __init__(self,
                 model,
                 name=None,
                 category=None,
                 endpoint=None,
                 url=None,
                 static_folder=None,
                 menu_class_name=None,
                 menu_icon_type=None,
                 menu_icon_value=None):
        """
            Constructor

            :param model:
                Model class
            :param name:
                Display name
            :param category:
                Display category
            :param endpoint:
                Endpoint
            :param url:
                Custom URL
            :param menu_class_name:
                Optional class name for the menu item.
            :param menu_icon_type:
                Optional icon. Possible icon types:

                 - `flask_admin.consts.ICON_TYPE_GLYPH` - Bootstrap glyph icon
                 - `flask_admin.consts.ICON_TYPE_FONT_AWESOME` - Font Awesome icon
                 - `flask_admin.consts.ICON_TYPE_IMAGE` - Image relative to Flask static directory
                 - `flask_admin.consts.ICON_TYPE_IMAGE_URL` - Image with full URL

            :param menu_icon_value:
                Icon glyph name or URL, depending on `menu_icon_type` setting
        """
        self._search_fields = []

        super(ModelView, self).__init__(model,
                                        name,
                                        category,
                                        endpoint,
                                        url,
                                        static_folder,
                                        menu_class_name=menu_class_name,
                                        menu_icon_type=menu_icon_type,
                                        menu_icon_value=menu_icon_value)

        self._primary_key = self.scaffold_pk()

    def _refresh_cache(self):
        """
            Refresh cache.
        """
        # Process subdocuments
        if self.form_subdocuments is None:
            self.form_subdocuments = {}

        self._form_subdocuments = convert_subdocuments(self.form_subdocuments)

        # Cache other properties
        super(ModelView, self)._refresh_cache()

    def _process_ajax_references(self):
        """
            AJAX endpoint is exposed by top-level admin view class, but
            subdocuments might have AJAX references too.

            This method will recursively go over subdocument configuration
            and will precompute AJAX references for them ensuring that
            subdocuments can also use AJAX to populate their ReferenceFields.
        """
        references = super(ModelView, self)._process_ajax_references()
        return process_ajax_references(references, self)

    def _get_model_fields(self, model=None):
        """
            Inspect model and return list of model fields

            :param model:
                Model to inspect
        """
        if model is None:
            model = self.model

        return sorted(iteritems(model._fields),
                      key=lambda n: n[1].creation_counter)

    def scaffold_pk(self):
        # MongoEngine models have predefined 'id' as a key
        return 'id'

    def get_pk_value(self, model):
        """
            Return the primary key value from the model instance

            :param model:
                Model instance
        """
        return model.pk

    def scaffold_list_columns(self):
        """
            Scaffold list columns
        """
        columns = []

        for n, f in self._get_model_fields():
            # Verify type
            field_class = type(f)

            if (field_class == mongoengine.ListField and isinstance(
                    f.field, mongoengine.EmbeddedDocumentField)):
                continue

            if field_class == mongoengine.EmbeddedDocumentField:
                continue

            if self.column_display_pk or field_class != mongoengine.ObjectIdField:
                columns.append(n)

        return columns

    def scaffold_sortable_columns(self):
        """
            Return a dictionary of sortable columns (name, field)
        """
        columns = {}

        for n, f in self._get_model_fields():
            if type(f) in SORTABLE_FIELDS:
                if self.column_display_pk or type(
                        f) != mongoengine.ObjectIdField:
                    columns[n] = f

        return columns

    def init_search(self):
        """
            Init search
        """
        if self.column_searchable_list:
            for p in self.column_searchable_list:
                if isinstance(p, string_types):
                    p = self.model._fields.get(p)

                if p is None:
                    raise Exception('Invalid search field')

                field_type = type(p)

                # Check type
                if (field_type not in self.allowed_search_types):
                    raise Exception('Can only search on text columns. ' +
                                    'Failed to setup search for "%s"' % p)

                self._search_fields.append(p)

        return bool(self._search_fields)

    def scaffold_filters(self, name):
        """
            Return filter object(s) for the field

            :param name:
                Either field name or field instance
        """
        if isinstance(name, string_types):
            attr = self.model._fields.get(name)
        else:
            attr = name

        if attr is None:
            raise Exception('Failed to find field for filter: %s' % name)

        # Find name
        visible_name = None

        if not isinstance(name, string_types):
            visible_name = self.get_column_name(attr.name)

        if not visible_name:
            visible_name = self.get_column_name(name)

        # Convert filter
        type_name = type(attr).__name__
        flt = self.filter_converter.convert(type_name, attr, visible_name)

        return flt

    def is_valid_filter(self, filter):
        """
            Validate if the provided filter is a valid MongoEngine filter

            :param filter:
                Filter object
        """
        return isinstance(filter, BaseMongoEngineFilter)

    def scaffold_form(self):
        """
            Create form from the model.
        """
        form_class = get_form(self.model,
                              self.model_form_converter(self),
                              base_class=self.form_base_class,
                              only=self.form_columns,
                              exclude=self.form_excluded_columns,
                              field_args=self.form_args,
                              extra_fields=self.form_extra_fields)

        return form_class

    def scaffold_list_form(self, widget=None, validators=None):
        """
            Create form for the `index_view` using only the columns from
            `self.column_editable_list`.

            :param widget:
                WTForms widget class. Defaults to `XEditableWidget`.
            :param validators:
                `form_args` dict with only validators
                {'name': {'validators': [required()]}}
        """
        form_class = get_form(self.model,
                              self.model_form_converter(self),
                              base_class=self.form_base_class,
                              only=self.column_editable_list,
                              field_args=validators)

        return create_editable_list_form(self.form_base_class, form_class,
                                         widget)

    # AJAX foreignkey support
    def _create_ajax_loader(self, name, opts):
        return create_ajax_loader(self.model, name, name, opts)

    def get_query(self):
        """
        Returns the QuerySet for this view.  By default, it returns all the
        objects for the current model.
        """
        return self.model.objects

    def _search(self, query, search_term):
        # TODO: Unfortunately, MongoEngine contains bug which
        # prevents running complex Q queries and, as a result,
        # Flask-Admin does not support per-word searching like
        # in other backends
        op, term = parse_like_term(search_term)

        criteria = None

        for field in self._search_fields:
            flt = {'%s__%s' % (field.name, op): term}
            q = mongoengine.Q(**flt)

            if criteria is None:
                criteria = q
            else:
                criteria |= q

        return query.filter(criteria)

    def get_list(self,
                 page,
                 sort_column,
                 sort_desc,
                 search,
                 filters,
                 execute=True,
                 page_size=None):
        """
            Get list of objects from MongoEngine

            :param page:
                Page number
            :param sort_column:
                Sort column
            :param sort_desc:
                Sort descending
            :param search:
                Search criteria
            :param filters:
                List of applied filters
            :param execute:
                Run query immediately or not
            :param page_size:
                Number of results. Defaults to ModelView's page_size. Can be
                overriden to change the page_size limit. Removing the page_size
                limit requires setting page_size to 0 or False.
        """
        query = self.get_query()

        # Filters
        if self._filters:
            for flt, flt_name, value in filters:
                f = self._filters[flt]
                query = f.apply(query, f.clean(value))

        # Search
        if self._search_supported and search:
            query = self._search(query, search)

        # Get count
        count = query.count() if not self.simple_list_pager else None

        # Sorting
        if sort_column:
            query = query.order_by('%s%s' %
                                   ('-' if sort_desc else '', sort_column))
        else:
            order = self._get_default_order()

            if order:
                keys = [
                    '%s%s' % ('-' if desc else '', col)
                    for (col, desc) in order
                ]
                query = query.order_by(*keys)

        # Pagination
        if page_size is None:
            page_size = self.page_size

        if page_size:
            query = query.limit(page_size)

        if page and page_size:
            query = query.skip(page * page_size)

        if execute:
            query = query.all()

        return count, query

    def get_one(self, id):
        """
            Return a single model instance by its ID

            :param id:
                Model ID
        """
        try:
            return self.get_query().filter(pk=id).first()
        except mongoengine.ValidationError as ex:
            flash(
                gettext('Failed to get model. %(error)s',
                        error=format_error(ex)), 'error')
            return None

    def create_model(self, form):
        """
            Create model helper

            :param form:
                Form instance
        """
        try:
            model = self.model()
            form.populate_obj(model)
            self._on_model_change(form, model, True)
            model.save()
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext('Failed to create record. %(error)s',
                            error=format_error(ex)), 'error')
                log.exception('Failed to create record.')

            return False
        else:
            self.after_model_change(form, model, True)

        return model

    def update_model(self, form, model):
        """
            Update model helper

            :param form:
                Form instance
            :param model:
                Model instance to update
        """
        try:
            form.populate_obj(model)
            self._on_model_change(form, model, False)
            model.save()
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext('Failed to update record. %(error)s',
                            error=format_error(ex)), 'error')
                log.exception('Failed to update record.')

            return False
        else:
            self.after_model_change(form, model, False)

        return True

    def delete_model(self, model):
        """
            Delete model helper

            :param model:
                Model instance
        """
        try:
            self.on_model_delete(model)
            model.delete()
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext('Failed to delete record. %(error)s',
                            error=format_error(ex)), 'error')
                log.exception('Failed to delete record.')

            return False
        else:
            self.after_model_delete(model)

        return True

    # FileField access API
    @expose('/api/file/')
    def api_file_view(self):
        pk = request.args.get('id')
        coll = request.args.get('coll')
        db = request.args.get('db', 'default')

        if not pk or not coll or not db:
            abort(404)

        fs = gridfs.GridFS(get_db(db), coll)

        data = fs.get(self.object_id_converter(pk))
        if not data:
            abort(404)

        return Response(data.read(),
                        content_type=data.content_type,
                        headers={'Content-Length': data.length})

    # Default model actions
    def is_action_allowed(self, name):
        # Check delete action permission
        if name == 'delete' and not self.can_delete:
            return False

        return super(ModelView, self).is_action_allowed(name)

    @action('delete', lazy_gettext('Delete'),
            lazy_gettext('Are you sure you want to delete selected records?'))
    def action_delete(self, ids):
        try:
            count = 0

            all_ids = [self.object_id_converter(pk) for pk in ids]
            for obj in self.get_query().in_bulk(all_ids).values():
                count += self.delete_model(obj)

            flash(
                ngettext('Record was successfully deleted.',
                         '%(count)s records were successfully deleted.',
                         count,
                         count=count), 'success')
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext('Failed to delete records. %(error)s',
                            error=str(ex)), 'error')
Esempio n. 6
0
class HstoreForm(BaseForm):
    """ Form used in InlineFormField/InlineHstoreList for HSTORE columns """
    key = StringField(lazy_gettext('Key'))
    value = StringField(lazy_gettext('Value'))
Esempio n. 7
0
 def operation(self):
     return lazy_gettext('smaller than')
Esempio n. 8
0
 def operation(self):
     return lazy_gettext('equals')
Esempio n. 9
0
class ProductView(ModelView):
    # page_size = 10
    form_excluded_columns = ('sku', 'created', 'object_', 'url')
    column_exclude_list = ('url', 'description', 'object_')
    form_overrides = dict(image=ImageUploadField)

    # form_args = {
    #   'image': dict(
    #     thumbnail_size=(500, 500, True),
    #     url_relative_path=f'{current_app.config.get("S3_RELATIVE_URL_PATH")}',
    #     namegen=lambda x: int(f'{x.year}{x.month}{x.day}{x.hour}{x.minute}{x.second}'),
    #     storage_type='s3',
    #     bucket_name=current_app.config.get('S3_BUCKET_NAME'),
    #     access_key_id=current_app.config.get('S3_ACCESS_KEY_ID'),
    #     access_key_secret=current_app.config.get('S3_SECRET_ACCESS_KEY'),
    #     acl='public-read',
    #     allowed_extensions=('png', 'jpg'),
    #   )
    # }

    def create_model(self, form):
        try:
            getDate = lambda x: f'{x.year}{x.month}{x.day}{x.hour}{x.minute}{x.second}'
            if request.files["image"]:
                s3.upload_to_aws(
                    request.files["image"],
                    current_app.config.get('S3_BUCKET_NAME'),
                    'products/' + getDate(datetime.utcnow()) + '.png')

                # # Create Stripe product
                # product = stripe.Product.create(
                #   name=form.name.data,
                #   type='good',
                #   attributes=['name'],
                #   description=form.description.data,
                #   images=[file_upload],
                # )

                # # Create SKU for Stripe product
                # sku = stripe.SKU.create(
                #   product=product.id,
                #   attributes={'name': product.name},
                #   price=int(form.price.data * 100),
                #   currency='usd',
                #   image=product.images[0],
                #   inventory={'type': 'infinite'}
                # )

                # # Create database product
                # p = Product(
                #   id_=product.id,
                #   sku=sku.id,
                #   name=product.name,
                #   image=product.images[0],
                #   price=form.price.data,
                #   active=product.active,
                #   created=datetime.fromtimestamp(product.created),
                #   description=product.description,
                #   object_=product.type,
                #   url=product.url
                # )
                # db.session.add(p)
                # db.session.commit()
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext('Failed to create record. %(error)s',
                            error=str(ex)), 'error')
                log.exception('Failed to create record.')
            self.session.rollback()
            return False
        return True

    def update_model(self, form, model):
        try:
            file = request.files["image"]
            file.filename = getDate(datetime.utcnow()) + '.png'
            file_upload = upload_file_to_s3(
                file, current_app.config.get('S3_BUCKET_NAME'))

            # Update Stripe SKU information
            sku = stripe.SKU.modify(model.sku,
                                    image=file_upload,
                                    price=int(form.price.data * 100))

            # Update Stripe Product information
            product = stripe.Product.modify(
                model.id_,
                name=form.name.data,
                images=[file_upload],
                description=form.description.data,
                active=form.active.data,
            )

            # Update database Product model
            model.name = product.name
            model.image = file_upload
            model.price = form.price.data
            model.active = product.active
            model.description = product.description
            db.session.commit()

        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext('Failed to update record. %(error)s',
                            error=str(ex)), 'error')
                log.exception('Failed to update record.')
            self.session.rollback()
            return False
        return True

    def delete_model(self, model):
        try:
            # Delete SKU from Stripe (must be done before deleting product)
            stripe.SKU.delete(model.sku)

            # Delete product from Stripe
            stripe.Product.delete(model.id_)

            # Delete from database
            db.session.delete(model)
            db.session.commit()

        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext('Failed to delete record. %(error)s',
                            error=str(ex)), 'error')
                log.exception('Failed to delete record.')
            self.session.rollback()
            return False
        return True

    @action('delete', lazy_gettext('Delete'),
            lazy_gettext('Are you sure you want to delete selected records?'))
    def action_delete(self, ids):
        try:
            count = 0
            for i in Product.query.all():
                count += 1
                db.session.delete(i)
            db.session.commit()
            flash(
                ngettext('Record was successfully deleted.',
                         '%(count)s records were successfully deleted.',
                         count,
                         count=count), 'success')
        except Exception as ex:
            if not self.handle_view_exception(ex):
                raise
            flash(
                gettext('Failed to delete records. %(error)s', error=str(ex)),
                'error')
Esempio n. 10
0
class FileAdmin(BaseView, ActionsMixin):
    """
        Simple file-management interface.

        :param path:
            Path to the directory which will be managed
        :param base_url:
            Optional base URL for the directory. Will be used to generate
            static links to the files. If not defined, a route will be created
            to serve uploaded files.

        Sample usage::

            import os.path as op

            from flask_admin import Admin
            from flask_admin.contrib.fileadmin import FileAdmin

            admin = Admin()

            path = op.join(op.dirname(__file__), 'static')
            admin.add_view(FileAdmin(path, '/static/', name='Static Files'))
    """

    can_upload = True
    """
        Is file upload allowed.
    """

    can_download = True
    """
        Is file download allowed.
    """

    can_delete = True
    """
        Is file deletion allowed.
    """

    can_delete_dirs = True
    """
        Is recursive directory deletion is allowed.
    """

    can_mkdir = True
    """
        Is directory creation allowed.
    """

    can_rename = True
    """
        Is file and directory renaming allowed.
    """

    allowed_extensions = None
    """
        List of allowed extensions for uploads, in lower case.

        Example::

            class MyAdmin(FileAdmin):
                allowed_extensions = ('swf', 'jpg', 'gif', 'png')
    """

    editable_extensions = tuple()
    """
        List of editable extensions, in lower case.

        Example::

            class MyAdmin(FileAdmin):
                editable_extensions = ('md', 'html', 'txt')
    """

    list_template = 'admin/file/list.html'
    """
        File list template
    """

    upload_template = 'admin/file/form.html'
    """
        File upload template
    """

    mkdir_template = 'admin/file/form.html'
    """
        Directory creation (mkdir) template
    """

    rename_template = 'admin/file/rename.html'
    """
        Rename template
    """

    edit_template = 'admin/file/edit.html'
    """
        Edit template
    """

    form_base_class = form.BaseForm
    """
        Base form class. Will be used to create the upload, rename, edit, and delete form.

        Allows enabling CSRF validation and useful if you want to have custom
        contructor or override some fields.

        Example::

            class MyBaseForm(Form):
                def do_something(self):
                    pass

            class MyAdmin(FileAdmin):
                form_base_class = MyBaseForm

    """

    def __init__(self, base_path, base_url=None,
                 name=None, category=None, endpoint=None, url=None,
                 verify_path=True, menu_class_name=None, menu_icon_type=None, menu_icon_value=None):
        """
            Constructor.

            :param base_path:
                Base file storage location
            :param base_url:
                Base URL for the files
            :param name:
                Name of this view. If not provided, will default to the class name.
            :param category:
                View category
            :param endpoint:
                Endpoint name for the view
            :param url:
                URL for view
            :param verify_path:
                Verify if path exists. If set to `True` and path does not exist
                will raise an exception.
        """
        self.base_path = as_unicode(base_path)
        self.base_url = base_url

        self.init_actions()

        self._on_windows = platform.system() == 'Windows'

        # Convert allowed_extensions to set for quick validation
        if (self.allowed_extensions and
            not isinstance(self.allowed_extensions, set)):
            self.allowed_extensions = set(self.allowed_extensions)

        # Convert editable_extensions to set for quick validation
        if (self.editable_extensions and
            not isinstance(self.editable_extensions, set)):
            self.editable_extensions = set(self.editable_extensions)

        # Check if path exists
        if not op.exists(base_path):
            raise IOError('FileAdmin path "%s" does not exist or is not accessible' % base_path)

        super(FileAdmin, self).__init__(name, category, endpoint, url,
                                        menu_class_name=menu_class_name, menu_icon_type=menu_icon_type,
                                        menu_icon_value=menu_icon_value)

    def is_accessible_path(self, path):
        """
            Verify if the provided path is accessible for the current user.

            Override to customize behavior.

            :param path:
                Relative path to the root
        """
        return True

    def get_base_path(self):
        """
            Return base path. Override to customize behavior (per-user
            directories, etc)
        """
        return op.normpath(self.base_path)

    def get_base_url(self):
        """
            Return base URL. Override to customize behavior (per-user
            directories, etc)
        """
        return self.base_url

    def get_upload_form(self):
        """
            Upload form class for file upload view.

            Override to implement customized behavior.
        """
        class UploadForm(self.form_base_class):
            """
                File upload form. Works with FileAdmin instance to check if it
                is allowed to upload file with given extension.
            """
            upload = fields.FileField(lazy_gettext('File to upload'))

            def __init__(self, *args, **kwargs):
                super(UploadForm, self).__init__(*args, **kwargs)
                self.admin = kwargs['admin']

            def validate_upload(self, field):
                if not self.upload.data:
                    raise validators.ValidationError(gettext('File required.'))

                filename = self.upload.data.filename

                if not self.admin.is_file_allowed(filename):
                    raise validators.ValidationError(gettext('Invalid file type.'))

        return UploadForm

    def get_edit_form(self):
        """
            Create form class for file editing view.

            Override to implement customized behavior.
        """
        class EditForm(self.form_base_class):
            content = fields.TextAreaField(lazy_gettext('Content'),
                                           (validators.required(),))

        return EditForm

    def get_name_form(self):
        """
            Create form class for renaming and mkdir views.

            Override to implement customized behavior.
        """
        def validate_name(self, field):
            regexp = re.compile(r'^(?!^(PRN|AUX|CLOCK\$|NUL|CON|COM\d|LPT\d|\..*)(\..+)?$)[^\x00-\x1f\\?*:\";|/]+$')
            if not regexp.match(field.data):
                raise validators.ValidationError(gettext('Invalid name'))

        class NameForm(self.form_base_class):
            """
                Form with a filename input field.

                Validates if provided name is valid for *nix and Windows systems.
            """
            name = fields.StringField(lazy_gettext('Name'),
                                      validators=[validators.Required(),
                                                  validate_name])
            path = fields.HiddenField()

        return NameForm

    def get_delete_form(self):
        """
            Create form class for model delete view.

            Override to implement customized behavior.
        """
        class DeleteForm(self.form_base_class):
            path = fields.HiddenField(validators=[validators.Required()])

        return DeleteForm

    def upload_form(self):
        """
            Instantiate file upload form and return it.

            Override to implement custom behavior.
        """
        upload_form_class = self.get_upload_form()
        if request.form:
            # Workaround for allowing both CSRF token + FileField to be submitted
            # https://bitbucket.org/danjac/flask-wtf/issue/12/fieldlist-filefield-does-not-follow
            formdata = request.form.copy() # as request.form is immutable
            formdata.update(request.files)

            # admin=self allows the form to use self.is_file_allowed
            return upload_form_class(formdata, admin=self)
        elif request.files:
            return upload_form_class(request.files, admin=self)
        else:
            return upload_form_class(admin=self)

    def name_form(self):
        """
            Instantiate form used in rename and mkdir then return it.

            Override to implement custom behavior.
        """
        name_form_class = self.get_name_form()
        if request.form:
            return name_form_class(request.form)
        elif request.args:
            return name_form_class(request.args)
        else:
            return name_form_class()

    def edit_form(self):
        """
            Instantiate file editing form and return it.

            Override to implement custom behavior.
        """
        edit_form_class = self.get_edit_form()
        if request.form:
            return edit_form_class(request.form)
        else:
            return edit_form_class()

    def delete_form(self):
        """
            Instantiate file delete form and return it.

            Override to implement custom behavior.
        """
        delete_form_class = self.get_delete_form()
        if request.form:
            return delete_form_class(request.form)
        else:
            return delete_form_class()

    def is_file_allowed(self, filename):
        """
            Verify if file can be uploaded.

            Override to customize behavior.

            :param filename:
                Source file name
        """
        ext = op.splitext(filename)[1].lower()

        if ext.startswith('.'):
            ext = ext[1:]

        if self.allowed_extensions and ext not in self.allowed_extensions:
            return False

        return True

    def is_file_editable(self, filename):
        """
            Determine if the file can be edited.

            Override to customize behavior.

            :param filename:
                Source file name
        """
        ext = op.splitext(filename)[1].lower()

        if ext.startswith('.'):
            ext = ext[1:]

        if not self.editable_extensions or ext not in self.editable_extensions:
            return False

        return True

    def is_in_folder(self, base_path, directory):
        """
            Verify that `directory` is in `base_path` folder

            :param base_path:
                Base directory path
            :param directory:
                Directory path to check
        """
        return op.normpath(directory).startswith(base_path)

    def save_file(self, path, file_data):
        """
            Save uploaded file to the disk

            :param path:
                Path to save to
            :param file_data:
                Werkzeug `FileStorage` object
        """
        file_data.save(path)

    def validate_form(self, form):
        """
            Validate the form on submit.

            :param form:
                Form to validate
        """
        return helpers.validate_form_on_submit(form)

    def _get_dir_url(self, endpoint, path=None, **kwargs):
        """
            Return prettified URL

            :param endpoint:
                Endpoint name
            :param path:
                Directory path
            :param kwargs:
                Additional arguments
        """
        if not path:
            return self.get_url(endpoint)
        else:
            if self._on_windows:
                path = path.replace('\\', '/')

            kwargs['path'] = path

            return self.get_url(endpoint, **kwargs)

    def _get_file_url(self, path):
        """
            Return static file url

            :param path:
                Static file path
        """
        if self.is_file_editable(path):
            route = '.edit'
        else:
            route = '.download'

        return self.get_url(route, path=path)

    def _normalize_path(self, path):
        """
            Verify and normalize path.

            If the path is not relative to the base directory, will raise a 404 exception.

            If the path does not exist, this will also raise a 404 exception.
        """
        base_path = self.get_base_path()

        if path is None:
            directory = base_path
            path = ''
        else:
            path = op.normpath(path)
            directory = op.normpath(op.join(base_path, path))

            if not self.is_in_folder(base_path, directory):
                abort(404)

        if not op.exists(directory):
            abort(404)

        return base_path, directory, path

    def is_action_allowed(self, name):
        if name == 'delete' and not self.can_delete:
            return False
        elif name == 'edit' and len(self.editable_extensions) == 0:
            return False

        return True

    def on_rename(self, full_path, dir_base, filename):
        """
            Perform some actions after a file or directory has been renamed.

            Called from rename method

            By default do nothing.
        """
        pass

    def on_edit_file(self, full_path, path):
        """
            Perform some actions after a file has been successfully changed.

            Called from edit method

            By default do nothing.
        """
        pass

    def on_file_upload(self, directory, path, filename):
        """
            Perform some actions after a file has been successfully uploaded.

            Called from upload method

            By default do nothing.
        """
        pass

    def on_mkdir(self, parent_dir, dir_name):
        """
            Perform some actions after a directory has successfully been created.

            Called from mkdir method

            By default do nothing.
        """
        pass

    def on_directory_delete(self, full_path, dir_name):
        """
            Perform some actions after a directory has successfully been deleted.

            Called from delete method

            By default do nothing.
        """
        pass

    def on_file_delete(self, full_path, filename):
        """
            Perform some actions after a file has successfully been deleted.

            Called from delete method

            By default do nothing.
        """
        pass

    def _save_form_files(self, directory, path, form):
        filename = op.join(directory,
                           secure_filename(form.upload.data.filename))

        if op.exists(filename):
            flash(gettext('File "%(name)s" already exists.', name=filename),
                  'error')
        else:
            self.save_file(filename, form.upload.data)
            self.on_file_upload(directory, path, filename)

    @expose('/')
    @expose('/b/<path:path>')
    def index(self, path=None):
        """
            Index view method

            :param path:
                Optional directory path. If not provided, will use the base directory
        """
        if self.can_delete:
            delete_form = self.delete_form()
        else:
            delete_form = None

        # Get path and verify if it is valid
        base_path, directory, path = self._normalize_path(path)

        if not self.is_accessible_path(path):
            flash(gettext('Permission denied.'), 'error')
            return redirect(self._get_dir_url('.index'))

        # Get directory listing
        items = []

        # Parent directory
        if directory != base_path:
            parent_path = op.normpath(op.join(path, '..'))
            if parent_path == '.':
                parent_path = None

            items.append(('..', parent_path, True, 0, 0))

        for f in os.listdir(directory):
            fp = op.join(directory, f)
            rel_path = op.join(path, f)

            if self.is_accessible_path(rel_path):
                items.append((f, rel_path, op.isdir(fp), op.getsize(fp), op.getmtime(fp)))

        # Sort by name
        items.sort(key=itemgetter(0))

        # Sort by type
        items.sort(key=itemgetter(2), reverse=True)

        # Sort by modified date
        items.sort(key=lambda values: (values[0], values[1], values[2], values[3], datetime.fromtimestamp(values[4])), reverse=True)

        # Generate breadcrumbs
        accumulator = []
        breadcrumbs = []
        for n in path.split(os.sep):
            accumulator.append(n)
            breadcrumbs.append((n, op.join(*accumulator)))

        # Actions
        actions, actions_confirmation = self.get_actions_list()

        return self.render(self.list_template,
                           dir_path=path,
                           breadcrumbs=breadcrumbs,
                           get_dir_url=self._get_dir_url,
                           get_file_url=self._get_file_url,
                           items=items,
                           actions=actions,
                           actions_confirmation=actions_confirmation,
                           delete_form=delete_form)

    @expose('/upload/', methods=('GET', 'POST'))
    @expose('/upload/<path:path>', methods=('GET', 'POST'))
    def upload(self, path=None):
        """
            Upload view method

            :param path:
                Optional directory path. If not provided, will use the base directory
        """
        # Get path and verify if it is valid
        base_path, directory, path = self._normalize_path(path)

        if not self.can_upload:
            flash(gettext('File uploading is disabled.'), 'error')
            return redirect(self._get_dir_url('.index', path))

        if not self.is_accessible_path(path):
            flash(gettext('Permission denied.'), 'error')
            return redirect(self._get_dir_url('.index'))

        form = self.upload_form()
        if self.validate_form(form):
            try:
                self._save_form_files(directory, path, form)
                return redirect(self._get_dir_url('.index', path))
            except Exception as ex:
                flash(gettext('Failed to save file: %(error)s', error=ex), 'error')

        return self.render(self.upload_template, form=form)

    @expose('/download/<path:path>')
    def download(self, path=None):
        """
            Download view method.

            :param path:
                File path.
        """
        if not self.can_download:
            abort(404)

        base_path, directory, path = self._normalize_path(path)

        # backward compatibility with base_url
        base_url = self.get_base_url()
        if base_url:
            base_url = urljoin(self.get_url('.index'), base_url)
            return redirect(urljoin(base_url, path))

        return send_file(directory)

    @expose('/mkdir/', methods=('GET', 'POST'))
    @expose('/mkdir/<path:path>', methods=('GET', 'POST'))
    def mkdir(self, path=None):
        """
            Directory creation view method

            :param path:
                Optional directory path. If not provided, will use the base directory
        """
        # Get path and verify if it is valid
        base_path, directory, path = self._normalize_path(path)

        dir_url = self._get_dir_url('.index', path)

        if not self.can_mkdir:
            flash(gettext('Directory creation is disabled.'), 'error')
            return redirect(dir_url)

        if not self.is_accessible_path(path):
            flash(gettext('Permission denied.'), 'error')
            return redirect(self._get_dir_url('.index'))

        form = self.name_form()

        if self.validate_form(form):
            try:
                os.mkdir(op.join(directory, form.name.data))
                self.on_mkdir(directory, form.name.data)
                return redirect(dir_url)
            except Exception as ex:
                flash(gettext('Failed to create directory: %(error)s', error=ex), 'error')
        else:
            helpers.flash_errors(form, message='Failed to create directory: %(error)s')

        return self.render(self.mkdir_template,
                           form=form,
                           dir_url=dir_url)

    @expose('/delete/', methods=('POST',))
    def delete(self):
        """
            Delete view method
        """
        form = self.delete_form()

        path = form.path.data
        if path:
            return_url = self._get_dir_url('.index', op.dirname(path))
        else:
            return_url = self.get_url('.index')

        if self.validate_form(form):
            # Get path and verify if it is valid
            base_path, full_path, path = self._normalize_path(path)

            if not self.can_delete:
                flash(gettext('Deletion is disabled.'), 'error')
                return redirect(return_url)

            if not self.is_accessible_path(path):
                flash(gettext('Permission denied.'), 'error')
                return redirect(self._get_dir_url('.index'))

            if op.isdir(full_path):
                if not self.can_delete_dirs:
                    flash(gettext('Directory deletion is disabled.'), 'error')
                    return redirect(return_url)

                try:
                    shutil.rmtree(full_path)
                    self.on_directory_delete(full_path, path)
                    flash(gettext('Directory "%(path)s" was successfully deleted.', path=path))
                except Exception as ex:
                    flash(gettext('Failed to delete directory: %(error)s', error=ex), 'error')
            else:
                try:
                    os.remove(full_path)
                    self.on_file_delete(full_path, path)
                    flash(gettext('File "%(name)s" was successfully deleted.', name=path))
                except Exception as ex:
                    flash(gettext('Failed to delete file: %(name)s', name=ex), 'error')
        else:
            helpers.flash_errors(form, message='Failed to delete file. %(error)s')

        return redirect(return_url)

    @expose('/rename/', methods=('GET', 'POST'))
    def rename(self):
        """
            Rename view method
        """
        form = self.name_form()

        path = form.path.data
        if path:
            base_path, full_path, path = self._normalize_path(path)

            return_url = self._get_dir_url('.index', op.dirname(path))
        else:
            return redirect(self.get_url('.index'))

        if not self.can_rename:
            flash(gettext('Renaming is disabled.'), 'error')
            return redirect(return_url)

        if not self.is_accessible_path(path):
            flash(gettext('Permission denied.'), 'error')
            return redirect(self._get_dir_url('.index'))

        if not op.exists(full_path):
            flash(gettext('Path does not exist.'), 'error')
            return redirect(return_url)

        if self.validate_form(form):
            try:
                dir_base = op.dirname(full_path)
                filename = secure_filename(form.name.data)

                os.rename(full_path, op.join(dir_base, filename))
                self.on_rename(full_path, dir_base, filename)
                flash(gettext('Successfully renamed "%(src)s" to "%(dst)s"',
                      src=op.basename(path),
                      dst=filename))
            except Exception as ex:
                flash(gettext('Failed to rename: %(error)s', error=ex), 'error')

            return redirect(return_url)
        else:
            helpers.flash_errors(form, message='Failed to rename: %(error)s')

        return self.render(self.rename_template,
                           form=form,
                           path=op.dirname(path),
                           name=op.basename(path),
                           dir_url=return_url)

    @expose('/edit/', methods=('GET', 'POST'))
    def edit(self):
        """
            Edit view method
        """
        next_url = None

        path = request.args.getlist('path')
        if not path:
            return redirect(self.get_url('.index'))

        if len(path) > 1:
            next_url = self.get_url('.edit', path=path[1:])

        path = path[0]

        base_path, full_path, path = self._normalize_path(path)

        if not self.is_accessible_path(path) or not self.is_file_editable(path):
            flash(gettext('Permission denied.'), 'error')
            return redirect(self._get_dir_url('.index'))

        dir_url = self._get_dir_url('.index', os.path.dirname(path))
        next_url = next_url or dir_url

        form = self.edit_form()
        error = False

        if self.validate_form(form):
            form.process(request.form, content='')
            if form.validate():
                try:
                    with open(full_path, 'w') as f:
                        f.write(request.form['content'])
                except IOError:
                    flash(gettext("Error saving changes to %(name)s.", name=path), 'error')
                    error = True
                else:
                    self.on_edit_file(full_path, path)
                    flash(gettext("Changes to %(name)s saved successfully.", name=path))
                    return redirect(next_url)
        else:
            helpers.flash_errors(form, message='Failed to edit file. %(error)s')

            try:
                with open(full_path, 'rb') as f:
                    content = f.read()
            except IOError:
                flash(gettext("Error reading %(name)s.", name=path), 'error')
                error = True
            except:
                flash(gettext("Unexpected error while reading from %(name)s", name=path), 'error')
                error = True
            else:
                try:
                    content = content.decode('utf8')
                except UnicodeDecodeError:
                    flash(gettext("Cannot edit %(name)s.", name=path), 'error')
                    error = True
                except:
                    flash(gettext("Unexpected error while reading from %(name)s", name=path), 'error')
                    error = True
                else:
                    form.content.data = content

        return self.render(self.edit_template, dir_url=dir_url, path=path,
                           form=form, error=error)

    @expose('/action/', methods=('POST',))
    def action_view(self):
        return self.handle_action()

    # Actions
    @action('delete',
            lazy_gettext('Delete'),
            lazy_gettext('Are you sure you want to delete these files?'))
    def action_delete(self, items):
        if not self.can_delete:
            flash(gettext('File deletion is disabled.'), 'error')
            return

        for path in items:
            base_path, full_path, path = self._normalize_path(path)

            if self.is_accessible_path(path):
                try:
                    os.remove(full_path)
                    flash(gettext('File "%(name)s" was successfully deleted.', name=path))
                except Exception as ex:
                    flash(gettext('Failed to delete file: %(name)s', name=ex), 'error')

    @action('edit', lazy_gettext('Edit'))
    def action_edit(self, items):
        return redirect(self.get_url('.edit', path=items))
Esempio n. 11
0
 def __init__(self, name, options=None, data_type=None):
     super(BaseBooleanFilter,
           self).__init__(name, (('1', lazy_gettext(u'Yes')),
                                 ('0', lazy_gettext(u'No'))), data_type)
Esempio n. 12
0
class ModelView(BaseModelView):
    """LeanCloud Model"""

    column_filters = None
    details_modal = True
    create_modal = True
    edit_modal = True
    form_image_width = 150
    form_image_height = 150

    def __init__(self,
                 coll,
                 name=None,
                 category=None,
                 endpoint=None,
                 url=None,
                 menu_class_name=None,
                 menu_icon_type=None,
                 menu_icon_value=None):
        super(ModelView, self).__init__(None,
                                        name,
                                        category,
                                        endpoint,
                                        url,
                                        menu_class_name=menu_class_name,
                                        menu_icon_type=menu_icon_type,
                                        menu_icon_value=menu_icon_value)
        self.coll = coll

    def get_pk_value(self, model):
        return model.id

    def scaffold_list_columns(self):
        raise NotImplementedError()

    def scaffold_sortable_columns(self):
        return []

    def init_search(self):
        return False

    def scaffold_filters(self, name):
        raise NotImplementedError()

    def scaffold_form(self):
        raise NotImplementedError()

    def _get_field_value(self, model, name):
        value = model.get(name)
        if isinstance(value, File):
            if value._type.startswith('image/'):
                return Markup(
                    "<a href='%s'><img src='%s'/></a>" %
                    (value.url,
                     value.get_thumbnail_url(width=self.form_image_width,
                                             height=self.form_image_height)))
            else:
                return Markup("<a href='%s'>%s</a>" % (value.url, value.name))
        return value

    def get_list(self,
                 page,
                 sort_field,
                 sort_desc,
                 search,
                 filters,
                 page_size=None):
        query = self.coll.query()
        count = query.count()

        if page_size is None:
            page_size = self.page_size
        query.skip(page * page_size)
        query.ascending('create_at')
        query.limit(page_size)
        results = query.find()

        return count, results

    def get_one(self, id):
        return self.coll.query().get(id)

    def edit_form(self, obj):
        cols = {}
        for col in set(self.column_list + self.column_details_list):
            cols[col] = obj.get(col)
        return self._edit_form_class(get_form_data(), **cols)

    def create_model(self, form):
        model = self.coll.Class()
        model.set(form.data)
        self._on_model_change(form, model, True)
        model.save()
        return model

    def update_model(self, form, model):
        model.set(form.data)
        self._on_model_change(form, model, False)
        model.save()
        return True

    def delete_model(self, model):
        model.destroy()
        return True

    def is_valid_filter(self, filter):
        return True

    @action('delete', lazy_gettext('Delete'),
            lazy_gettext('Are you sure to delete selected row?'))
    def action_delete(self, ids):
        count = 0
        for pk in ids:
            self.get_one(pk).destroy()
            count += 1
        flash(
            ngettext('Record was successfully deleted.',
                     '%(count)s records were successfully deleted.',
                     count,
                     count=count))
Esempio n. 13
0
 def operation(self):
     return lazy_gettext('starts with')
Esempio n. 14
0
                except:
                    flash(gettext("Unexpected error while reading from %(name)s", name=path), 'error')
                    error = True
                else:
                    form.content.data = content

        return self.render(self.edit_template, dir_url=dir_url, path=path,
                        form=form, error=error)

    @expose('/action/', methods=('POST',))
    def action_view(self):
        return self.handle_action()

    # Actions
    @action('delete',
            lazy_gettext('Delete'),
            lazy_gettext('Are you sure you want to delete these files?'))
    def action_delete(self, items):
        for path in items:
            base_path, full_path, path = self._normalize_path(path)

            try:
                os.remove(full_path)
                flash(gettext('File "%(name)s" was successfully deleted.', name=path))
            except Exception, ex:
                flash(gettext('Failed to delete file: %(name)s', name=ex), 'error')

    @action('edit', lazy_gettext('Edit'))
    def action_edit(self, items):
        return redirect(url_for('.edit', path=items))
Esempio n. 15
0
 def operation(self):
     return lazy_gettext("contains")
Esempio n. 16
0
 def operation(self):
     return lazy_gettext("between")
Esempio n. 17
0
def _l(*args, **kwargs):
    return lazy_gettext(*args, **kwargs)
Esempio n. 18
0
class CouponView(ModelView):
    column_exclude_list = ('object_')
    form_excluded_columns = ('object_', 'created')

    def on_form_prefill(self, form, id):
        form.duration.render_kw = {'readonly': True}
        form.duration_in_months.render_kw = {'readonly': True}
        form.percent_off.render_kw = {'readonly': True}

    def create_model(self, form):
        try:
            coupon = stripe.Coupon.create(
                name=form.name.data,
                duration=form.duration.data,
                duration_in_months=form.duration_in_months.data,
                percent_off=form.percent_off.data)

            c = Coupon(id_=coupon.id,
                       name=coupon.name,
                       duration=coupon.duration,
                       duration_in_months=coupon.duration_in_months,
                       percent_off=coupon.percent_off,
                       created=datetime.fromtimestamp(coupon.created),
                       object_=coupon.object)
            db.session.add(c)
            db.session.commit()
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext('Failed to create record. %(error)s',
                            error=str(ex)), 'error')
                log.exception('Failed to create record.')
            self.session.rollback()
            return False
        return self.render('admin/model/create.html', form=form)

    def update_model(self, form, model):
        try:
            # Update Stripe Coupon information
            coupon = stripe.Coupon.modify(model.id_, name=form.name.data)

            # Update database Coupon model
            model.name = coupon.name
            db.session.commit()

        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext('Failed to update record. %(error)s',
                            error=str(ex)), 'error')
                log.exception('Failed to update record.')
            self.session.rollback()
            return False
        return True

    def delete_model(self, model):
        try:
            stripe.Coupon.delete(model.name)

            # Delete from database
            db.session.delete(model)
            db.session.commit()

        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext('Failed to delete record. %(error)s',
                            error=str(ex)), 'error')
                log.exception('Failed to delete record.')
            self.session.rollback()
            return False
        return True

    @action('delete', lazy_gettext('Delete'),
            lazy_gettext('Are you sure you want to delete selected records?'))
    def action_delete(self, ids):
        try:
            count = 0
            for i in Coupon.query.all():
                count += 1
                db.session.delete(i)
            db.session.commit()
            flash(
                ngettext('Record was successfully deleted.',
                         '%(count)s records were successfully deleted.',
                         count,
                         count=count), 'success')
        except Exception as ex:
            if not self.handle_view_exception(ex):
                raise
        flash(gettext('Failed to delete records. %(error)s', error=str(ex)),
              'error')
Esempio n. 19
0
 def operation(self):
     return lazy_gettext('not in list')
Esempio n. 20
0
class ModelView(BaseModelView):
    """
        SQLAlchemy model view

        Usage sample::

            admin = Admin()
            admin.add_view(ModelView(User, db.session))
    """

    column_auto_select_related = ObsoleteAttr('column_auto_select_related',
                                              'auto_select_related', True)
    """
        Enable automatic detection of displayed foreign keys in this view
        and perform automatic joined loading for related models to improve
        query performance.

        Please note that detection is not recursive: if `__unicode__` method
        of related model uses another model to generate string representation, it
        will still make separate database call.
    """

    column_select_related_list = ObsoleteAttr('column_select_related',
                                              'list_select_related', None)
    """
        List of parameters for SQLAlchemy `subqueryload`. Overrides `column_auto_select_related`
        property.

        For example::

            class PostAdmin(ModelView):
                column_select_related_list = ('user', 'city')

        You can also use properties::

            class PostAdmin(ModelView):
                column_select_related_list = (Post.user, Post.city)

        Please refer to the `subqueryload` on list of possible values.
    """

    column_display_all_relations = ObsoleteAttr('column_display_all_relations',
                                                'list_display_all_relations',
                                                False)
    """
        Controls if list view should display all relations, not only many-to-one.
    """

    column_searchable_list = ObsoleteAttr('column_searchable_list',
                                          'searchable_columns', None)
    """
        Collection of the searchable columns.

        Example::

            class MyModelView(ModelView):
                column_searchable_list = ('name', 'email')

        You can also pass columns::

            class MyModelView(ModelView):
                column_searchable_list = (User.name, User.email)

        The following search rules apply:

        - If you enter *ZZZ* in the UI search field, it will generate *ILIKE '%ZZZ%'*
          statement against searchable columns.

        - If you enter multiple words, each word will be searched separately, but
          only rows that contain all words will be displayed. For example, searching
          for 'abc def' will find all rows that contain 'abc' and 'def' in one or
          more columns.

        - If you prefix your search term with ^, it will find all rows
          that start with ^. So, if you entered *^ZZZ*, *ILIKE 'ZZZ%'* will be used.

        - If you prefix your search term with =, it will perform an exact match.
          For example, if you entered *=ZZZ*, the statement *ILIKE 'ZZZ'* will be used.
    """

    column_filters = None
    """
        Collection of the column filters.

        Can contain either field names or instances of :class:`flask_admin.contrib.sqla.filters.BaseFilter` classes.

        For example::

            class MyModelView(BaseModelView):
                column_filters = ('user', 'email')

        or::

            class MyModelView(BaseModelView):
                column_filters = (BooleanEqualFilter(User.name, 'Name'))
    """

    model_form_converter = form.AdminModelConverter
    """
        Model form conversion class. Use this to implement custom field conversion logic.

        For example::

            class MyModelConverter(AdminModelConverter):
                pass


            class MyAdminView(ModelView):
                model_form_converter = MyModelConverter
    """

    inline_model_form_converter = form.InlineModelConverter
    """
        Inline model conversion class. If you need some kind of post-processing for inline
        forms, you can customize behavior by doing something like this::

            class MyInlineModelConverter(AdminModelConverter):
                def post_process(self, form_class, info):
                    form_class.value = wtf.StringField('value')
                    return form_class

            class MyAdminView(ModelView):
                inline_model_form_converter = MyInlineModelConverter
    """

    filter_converter = sqla_filters.FilterConverter()
    """
        Field to filter converter.

        Override this attribute to use non-default converter.
    """

    fast_mass_delete = False
    """
        If set to `False` and user deletes more than one model using built in action,
        all models will be read from the database and then deleted one by one
        giving SQLAlchemy a chance to manually cleanup any dependencies (many-to-many
        relationships, etc).

        If set to `True`, will run a `DELETE` statement which is somewhat faster,
        but may leave corrupted data if you forget to configure `DELETE
        CASCADE` for your model.
    """

    inline_models = None
    """
        Inline related-model editing for models with parent-child relations.

        Accepts enumerable with one of the following possible values:

        1. Child model class::

            class MyModelView(ModelView):
                inline_models = (Post,)

        2. Child model class and additional options::

            class MyModelView(ModelView):
                inline_models = [(Post, dict(form_columns=['title']))]

        3. Django-like ``InlineFormAdmin`` class instance::

            class MyInlineModelForm(InlineFormAdmin):
                form_columns = ('title', 'date')

            class MyModelView(ModelView):
                inline_models = (MyInlineModelForm(MyInlineModel),)

        You can customize the generated field name by:

        1. Using the `form_name` property as a key to the options dictionary:

            class MyModelView(ModelView):
                inline_models = ((Post, dict(form_label='Hello')))

        2. Using forward relation name and `column_labels` property:

            class Model1(Base):
                pass

            class Model2(Base):
                # ...
                model1 = relation(Model1, backref='models')

            class MyModel1View(Base):
                inline_models = (Model2,)
                column_labels = {'models': 'Hello'}
    """

    column_type_formatters = DEFAULT_FORMATTERS

    form_choices = None
    """
        Map choices to form fields

        Example::

            class MyModelView(BaseModelView):
                form_choices = {'my_form_field': [
                    ('db_value', 'display_value'),
                ]
    """

    form_optional_types = (Boolean, )
    """
        List of field types that should be optional if column is not nullable.

        Example::

            class MyModelView(BaseModelView):
                form_optional_types = (Boolean, Unicode)
    """
    def __init__(self,
                 model,
                 session,
                 name=None,
                 category=None,
                 endpoint=None,
                 url=None,
                 static_folder=None,
                 menu_class_name=None,
                 menu_icon_type=None,
                 menu_icon_value=None):
        """
            Constructor.

            :param model:
                Model class
            :param session:
                SQLAlchemy session
            :param name:
                View name. If not set, defaults to the model name
            :param category:
                Category name
            :param endpoint:
                Endpoint name. If not set, defaults to the model name
            :param url:
                Base URL. If not set, defaults to '/admin/' + endpoint
            :param menu_class_name:
                Optional class name for the menu item.
            :param menu_icon_type:
                Optional icon. Possible icon types:

                 - `flask_admin.consts.ICON_TYPE_GLYPH` - Bootstrap glyph icon
                 - `flask_admin.consts.ICON_TYPE_FONT_AWESOME` - Font Awesome icon
                 - `flask_admin.consts.ICON_TYPE_IMAGE` - Image relative to Flask static directory
                 - `flask_admin.consts.ICON_TYPE_IMAGE_URL` - Image with full URL
            :param menu_icon_value:
                Icon glyph name or URL, depending on `menu_icon_type` setting
        """
        self.session = session

        self._search_fields = None
        self._search_joins = []

        self._filter_joins = dict()

        self._sortable_joins = dict()

        if self.form_choices is None:
            self.form_choices = {}

        super(ModelView, self).__init__(model,
                                        name,
                                        category,
                                        endpoint,
                                        url,
                                        static_folder,
                                        menu_class_name=menu_class_name,
                                        menu_icon_type=menu_icon_type,
                                        menu_icon_value=menu_icon_value)

        # Primary key
        self._primary_key = self.scaffold_pk()

        if self._primary_key is None:
            raise Exception('Model %s does not have primary key.' %
                            self.model.__name__)

        # Configuration
        if not self.column_select_related_list:
            self._auto_joins = self.scaffold_auto_joins()
        else:
            self._auto_joins = self.column_select_related_list

    # Internal API
    def _get_model_iterator(self, model=None):
        """
            Return property iterator for the model
        """
        if model is None:
            model = self.model

        return model._sa_class_manager.mapper.iterate_properties

    def _get_columns_for_field(self, field):
        if (not field or not hasattr(field, 'property')
                or not hasattr(field.property, 'columns')
                or not field.property.columns):
            raise Exception(
                'Invalid field %s: does not contains any columns.' % field)

        return field.property.columns

    def _get_field_with_path(self, name):
        join_tables = []

        if isinstance(name, string_types):
            model = self.model

            for attribute in name.split('.'):
                value = getattr(model, attribute)

                if (hasattr(value, 'property')
                        and hasattr(value.property, 'direction')):
                    model = value.property.mapper.class_
                    table = model.__table__

                    if self._need_join(table):
                        join_tables.append(table)

                attr = value
        else:
            attr = name

            # determine joins if Table.column (relation object) is given
            if isinstance(name, InstrumentedAttribute):
                columns = self._get_columns_for_field(name)

                if len(columns) > 1:
                    raise Exception('Can only handle one column for %s' % name)

                column = columns[0]

                if self._need_join(column.table):
                    join_tables.append(column.table)

        return join_tables, attr

    def _need_join(self, table):
        return table not in self.model._sa_class_manager.mapper.tables

    # Scaffolding
    def scaffold_pk(self):
        """
            Return the primary key name(s) from a model
            If model has single primary key, will return a string and tuple otherwise
        """
        return tools.get_primary_key(self.model)

    def get_pk_value(self, model):
        """
            Return the primary key value from a model object.
            If there are multiple primary keys, they're encoded into string representation.
        """
        if isinstance(self._primary_key, tuple):
            return tools.iterencode(
                getattr(model, attr) for attr in self._primary_key)
        else:
            return tools.escape(getattr(model, self._primary_key))

    def scaffold_list_columns(self):
        """
            Return a list of columns from the model.
        """
        columns = []

        for p in self._get_model_iterator():
            if hasattr(p, 'direction'):
                if self.column_display_all_relations or p.direction.name == 'MANYTOONE':
                    columns.append(p.key)
            elif hasattr(p, 'columns'):
                if len(p.columns) > 1:
                    filtered = tools.filter_foreign_columns(
                        self.model.__table__, p.columns)

                    if len(filtered) > 1:
                        warnings.warn(
                            'Can not convert multiple-column properties (%s.%s)'
                            % (self.model, p.key))
                        continue

                    column = filtered[0]
                else:
                    column = p.columns[0]

                if column.foreign_keys:
                    continue

                if not self.column_display_pk and column.primary_key:
                    continue

                columns.append(p.key)

        return columns

    def scaffold_sortable_columns(self):
        """
            Return a dictionary of sortable columns.
            Key is column name, value is sort column/field.
        """
        columns = dict()

        for p in self._get_model_iterator():
            if hasattr(p, 'columns'):
                # Sanity check
                if len(p.columns) > 1:
                    # Multi-column properties are not supported
                    continue

                column = p.columns[0]

                # Can't sort on primary or foreign keys by default
                if column.foreign_keys:
                    continue

                if not self.column_display_pk and column.primary_key:
                    continue

                columns[p.key] = column

        return columns

    def get_sortable_columns(self):
        """
            Returns a dictionary of the sortable columns. Key is a model
            field name and value is sort column (for example - attribute).

            If `column_sortable_list` is set, will use it. Otherwise, will call
            `scaffold_sortable_columns` to get them from the model.
        """
        self._sortable_joins = dict()

        if self.column_sortable_list is None:
            return self.scaffold_sortable_columns()
        else:
            result = dict()

            for c in self.column_sortable_list:
                if isinstance(c, tuple):
                    join_tables, column = self._get_field_with_path(c[1])
                    column_name = c[0]
                elif isinstance(c, InstrumentedAttribute):
                    join_tables, column = self._get_field_with_path(c)
                    column_name = str(c)
                else:
                    join_tables, column = self._get_field_with_path(c)
                    column_name = c

                result[column_name] = column

                if join_tables:
                    self._sortable_joins[column_name] = join_tables

            return result

    def init_search(self):
        """
            Initialize search. Returns `True` if search is supported for this
            view.

            For SQLAlchemy, this will initialize internal fields: list of
            column objects used for filtering, etc.
        """
        if self.column_searchable_list:
            self._search_fields = []
            self._search_joins = []

            joins = set()

            for p in self.column_searchable_list:
                join_tables, attr = self._get_field_with_path(p)

                if not attr:
                    raise Exception(
                        'Failed to find field for search field: %s' % p)

                for column in self._get_columns_for_field(attr):
                    column_type = type(column.type).__name__

                    self._search_fields.append(column)

                    # Store joins, avoid duplicates
                    for table in join_tables:
                        if table.name not in joins:
                            self._search_joins.append(table)
                            joins.add(table.name)

        return bool(self.column_searchable_list)

    def scaffold_filters(self, name):
        """
            Return list of enabled filters
        """

        join_tables, attr = self._get_field_with_path(name)

        if attr is None:
            raise Exception('Failed to find field for filter: %s' % name)

        # Figure out filters for related column
        if hasattr(attr, 'property') and hasattr(attr.property, 'direction'):
            filters = []

            for p in self._get_model_iterator(attr.property.mapper.class_):
                if hasattr(p, 'columns'):
                    # TODO: Check for multiple columns
                    column = p.columns[0]

                    if column.foreign_keys or column.primary_key:
                        continue

                    visible_name = '%s / %s' % (self.get_column_name(
                        attr.prop.table.name), self.get_column_name(p.key))

                    type_name = type(column.type).__name__
                    flt = self.filter_converter.convert(
                        type_name, column, visible_name)

                    if flt:
                        table = column.table

                        if join_tables:
                            self._filter_joins[table.name] = join_tables
                        elif self._need_join(table):
                            self._filter_joins[table.name] = [table]
                        filters.extend(flt)

            return filters
        else:
            columns = self._get_columns_for_field(attr)

            if len(columns) > 1:
                raise Exception(
                    'Can not filter more than on one column for %s' % name)

            column = columns[0]

            if self._need_join(
                    column.table) and name not in self.column_labels:
                visible_name = '%s / %s' % (self.get_column_name(
                    column.table.name), self.get_column_name(column.name))
            else:
                if not isinstance(name, string_types):
                    visible_name = self.get_column_name(name.property.key)
                else:
                    visible_name = self.get_column_name(name)

            type_name = type(column.type).__name__

            if join_tables:
                self._filter_joins[column.table.name] = join_tables

            flt = self.filter_converter.convert(
                type_name,
                column,
                visible_name,
                options=self.column_choices.get(name),
            )

            if flt and not join_tables and self._need_join(column.table):
                self._filter_joins[column.table.name] = [column.table]

            return flt

    def handle_filter(self, filter):
        if isinstance(filter, sqla_filters.BaseSQLAFilter):
            column = filter.column

            if self._need_join(column.table):
                self._filter_joins[column.table.name] = [column.table]

        return filter

    def scaffold_form(self):
        """
            Create form from the model.
        """
        converter = self.model_form_converter(self.session, self)
        form_class = form.get_form(self.model,
                                   converter,
                                   base_class=self.form_base_class,
                                   only=self.form_columns,
                                   exclude=self.form_excluded_columns,
                                   field_args=self.form_args,
                                   extra_fields=self.form_extra_fields)

        if self.inline_models:
            form_class = self.scaffold_inline_form_models(form_class)

        return form_class

    def scaffold_list_form(self,
                           custom_fieldlist=ListEditableFieldList,
                           validators=None):
        """
            Create form for the `index_view` using only the columns from
            `self.column_editable_list`.

            :param validators:
                `form_args` dict with only validators
                {'name': {'validators': [required()]}}
            :param custom_fieldlist:
                A WTForm FieldList class. By default, `ListEditableFieldList`.
        """
        converter = self.model_form_converter(self.session, self)
        form_class = form.get_form(self.model,
                                   converter,
                                   base_class=self.form_base_class,
                                   only=self.column_editable_list,
                                   field_args=validators)

        return wrap_fields_in_fieldlist(self.form_base_class, form_class,
                                        custom_fieldlist)

    def scaffold_inline_form_models(self, form_class):
        """
            Contribute inline models to the form

            :param form_class:
                Form class
        """
        inline_converter = self.inline_model_form_converter(
            self.session, self, self.model_form_converter)

        for m in self.inline_models:
            form_class = inline_converter.contribute(self.model, form_class, m)

        return form_class

    def scaffold_auto_joins(self):
        """
            Return a list of joined tables by going through the
            displayed columns.
        """
        if not self.column_auto_select_related:
            return []

        relations = set()

        for p in self._get_model_iterator():
            if hasattr(p, 'direction'):
                # Check if it is pointing to same model
                if p.mapper.class_ == self.model:
                    continue

                if p.direction.name in ['MANYTOONE', 'MANYTOMANY']:
                    relations.add(p.key)

        joined = []

        for prop, name in self._list_columns:
            if prop in relations:
                joined.append(getattr(self.model, prop))

        return joined

    # AJAX foreignkey support
    def _create_ajax_loader(self, name, options):
        return create_ajax_loader(self.model, self.session, name, name,
                                  options)

    # Database-related API
    def get_query(self):
        """
            Return a query for the model type.

            If you override this method, don't forget to override `get_count_query` as well.

            This method can be used to set a "persistent filter" on an index_view.

            Example::

                class MyView(ModelView):
                    def get_query(self):
                        return super(MyView, self).get_query().filter(User.username == current_user.username)
        """
        return self.session.query(self.model)

    def get_count_query(self):
        """
            Return a the count query for the model type

            A query(self.model).count() approach produces an excessive
            subquery, so query(func.count('*')) should be used instead.

            See #45a2723 commit message for details.
        """
        return self.session.query(func.count('*')).select_from(self.model)

    def _order_by(self, query, joins, sort_joins, sort_field, sort_desc):
        """
            Apply order_by to the query

            :param query:
                Query
            :param joins:
                Joins set
            :param sort_field:
                Sort field
            :param sort_desc:
                Ascending or descending
        """
        # TODO: Preprocessing for joins
        # Handle joins
        if sort_joins:
            for table in sort_joins:
                if table.name not in joins:
                    query = query.outerjoin(table)

                    joins.add(table.name)

        if sort_field is not None:
            if sort_desc:
                query = query.order_by(desc(sort_field))
            else:
                query = query.order_by(sort_field)

        return query, joins

    def _get_default_order(self):
        order = super(ModelView, self)._get_default_order()

        if order is not None:
            field, direction = order

            join_tables, attr = self._get_field_with_path(field)

            return join_tables, attr, direction

        return None

    def get_list(self,
                 page,
                 sort_column,
                 sort_desc,
                 search,
                 filters,
                 execute=True):
        """
            Return models from the database.

            :param page:
                Page number
            :param sort_column:
                Sort column name
            :param sort_desc:
                Descending or ascending sort
            :param search:
                Search query
            :param execute:
                Execute query immediately? Default is `True`
            :param filters:
                List of filter tuples
        """

        # Will contain names of joined tables to avoid duplicate joins
        joins = set()

        query = self.get_query()
        count_query = self.get_count_query()

        # Ignore eager-loaded relations (prevent unnecessary joins)
        # TODO: Separate join detection for query and count query?
        if hasattr(query, '_join_entities'):
            for entity in query._join_entities:
                for table in entity.tables:
                    joins.add(table.name)

        # Apply search criteria
        if self._search_supported and search:
            # Apply search-related joins
            if self._search_joins:
                for table in self._search_joins:
                    if table.name not in joins:
                        query = query.outerjoin(table)
                        count_query = count_query.outerjoin(table)

                        joins.add(table.name)

            # Apply terms
            terms = search.split(' ')

            for term in terms:
                if not term:
                    continue

                stmt = tools.parse_like_term(term)
                filter_stmt = [c.ilike(stmt) for c in self._search_fields]
                query = query.filter(or_(*filter_stmt))
                count_query = count_query.filter(or_(*filter_stmt))

        # Apply filters
        if filters and self._filters:
            for idx, flt_name, value in filters:
                flt = self._filters[idx]

                # Figure out joins
                if isinstance(flt, sqla_filters.BaseSQLAFilter):
                    tbl = flt.column.table.name

                    join_tables = self._filter_joins.get(tbl, [])

                    for table in join_tables:
                        if table.name not in joins:
                            query = query.join(table)
                            count_query = count_query.join(table)
                            joins.add(table.name)

                # turn into python format with .clean() and apply filter
                query = flt.apply(query, flt.clean(value))
                count_query = flt.apply(count_query, flt.clean(value))

        # Calculate number of rows
        count = count_query.scalar()

        # Auto join
        for j in self._auto_joins:
            query = query.options(joinedload(j))

        # Sorting
        if sort_column is not None:
            if sort_column in self._sortable_columns:
                sort_field = self._sortable_columns[sort_column]
                sort_joins = self._sortable_joins.get(sort_column)

                query, joins = self._order_by(query, joins, sort_joins,
                                              sort_field, sort_desc)
        else:
            order = self._get_default_order()

            if order:
                sort_joins, sort_field, sort_desc = order

                query, joins = self._order_by(query, joins, sort_joins,
                                              sort_field, sort_desc)

        # Pagination
        if page is not None:
            query = query.offset(page * self.page_size)

        query = query.limit(self.page_size)

        # Execute if needed
        if execute:
            query = query.all()

        return count, query

    def get_one(self, id):
        """
            Return a single model by its id.

            :param id:
                Model id
        """
        return self.session.query(self.model).get(tools.iterdecode(id))

    # Error handler
    def handle_view_exception(self, exc):
        if isinstance(exc, IntegrityError):
            flash(
                gettext('Integrity error. %(message)s',
                        message=text_type(exc)), 'error')
            return True

        return super(ModelView, self).handle_view_exception(exc)

    # Model handlers
    def create_model(self, form):
        """
            Create model from form.

            :param form:
                Form instance
        """
        try:
            model = self.model()
            form.populate_obj(model)
            self.session.add(model)
            self._on_model_change(form, model, True)
            self.session.commit()
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext('Failed to create record. %(error)s',
                            error=str(ex)), 'error')
                log.exception('Failed to create record.')

            self.session.rollback()

            return False
        else:
            self.after_model_change(form, model, True)

        return True

    def update_model(self, form, model):
        """
            Update model from form.

            :param form:
                Form instance
            :param model:
                Model instance
        """
        try:
            form.populate_obj(model)
            self._on_model_change(form, model, False)
            self.session.commit()
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext('Failed to update record. %(error)s',
                            error=str(ex)), 'error')
                log.exception('Failed to update record.')

            self.session.rollback()

            return False
        else:
            self.after_model_change(form, model, False)

        return True

    def delete_model(self, model):
        """
            Delete model.

            :param model:
                Model to delete
        """
        try:
            self.on_model_delete(model)
            self.session.flush()
            self.session.delete(model)
            self.session.commit()
            return True
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext('Failed to delete record. %(error)s',
                            error=str(ex)), 'error')
                log.exception('Failed to delete record.')

            self.session.rollback()

            return False

    # Default model actions
    def is_action_allowed(self, name):
        # Check delete action permission
        if name == 'delete' and not self.can_delete:
            return False

        return super(ModelView, self).is_action_allowed(name)

    @action('delete', lazy_gettext('Delete'),
            lazy_gettext('Are you sure you want to delete selected records?'))
    def action_delete(self, ids):
        try:
            query = get_query_for_ids(self.get_query(), self.model, ids)

            if self.fast_mass_delete:
                count = query.delete(synchronize_session=False)
            else:
                count = 0

                for m in query.all():
                    if self.delete_model(m):
                        count += 1

            self.session.commit()

            flash(
                ngettext('Record was successfully deleted.',
                         '%(count)s records were successfully deleted.',
                         count,
                         count=count))
        except Exception as ex:
            if not self.handle_view_exception(ex):
                raise

            flash(
                gettext('Failed to delete records. %(error)s', error=str(ex)),
                'error')
Esempio n. 21
0
 def operation(self):
     return lazy_gettext('not contains')
Esempio n. 22
0
 def operation(self):
     return lazy_gettext('phrase')
Esempio n. 23
0
class WidgetSettingView(ModelView):
    """Widget Setting admin view."""

    can_create = True
    can_edit = True
    can_delete = True
    can_view_details = True
    column_formatters_detail = ObsoleteAttr('column_formatters',
                                            'list_formatters', dict())
    column_type_formatters_detail = dict(typefmt.EXPORT_FORMATTERS)

    def search_placeholder(self):
        """Return search placeholder."""
        return 'Search'

    @staticmethod
    def get_label_display_to_list(widget_id):
        """Helper to get label to display to list.

        Arguments:
            widget_id {int} -- id of widget item

        Return: label to display to list

        """
        register_language = get_register_language()
        multi_lang_data = WidgetMultiLangData.get_by_widget_id(widget_id)

        for lang in register_language:
            for data in multi_lang_data:
                if lang.get('lang_code') == data.lang_code:
                    return data.label

        unregister_language = get_unregister_language()
        for lang in unregister_language:
            for data in multi_lang_data:
                if lang.get('lang_code') == data.lang_code:
                    return data.label
        return None

    # Views
    @expose('/')
    def index_view(self):
        """List view."""
        if self.can_delete:
            delete_form = self.delete_form()
        else:
            delete_form = None

        # Grab parameters from URL
        view_args = self._get_list_extra_args()

        # Map column index to column name
        sort_column = self._get_column_by_idx(view_args.sort)
        if sort_column is not None:
            sort_column = sort_column[0]

        # Get page size
        page_size = view_args.page_size or self.page_size

        # Get count and data
        count, data = self.get_list(view_args.page,
                                    sort_column,
                                    view_args.sort_desc,
                                    view_args.search,
                                    view_args.filters,
                                    page_size=page_size)

        list_forms = {}
        if self.column_editable_list:
            for row in data:
                list_forms[self.get_pk_value(row)] = self.list_form(obj=row)

        # Calculate number of pages
        if count is not None and page_size:
            num_pages = int(ceil(count / float(page_size)))
        elif not page_size:
            num_pages = 0  # hide pager for unlimited page_size
        else:
            num_pages = None  # use simple pager

        # Various URL generation helpers
        def pager_url(p):
            # Do not add page number if it is first page
            if p == 0:
                p = None

            return self._get_list_url(view_args.clone(page=p))

        def sort_url(column, invert=False, desc=None):
            if not desc and invert and not view_args.sort_desc:
                desc = 1

            return self._get_list_url(
                view_args.clone(sort=column, sort_desc=desc))

        def page_size_url(s):
            if not s:
                s = self.page_size

            return self._get_list_url(view_args.clone(page_size=s))

        # Actions
        actions, actions_confirmation = self.get_actions_list()
        if actions:
            action_form = self.action_form()
        else:
            action_form = None

        clear_search_url = self._get_list_url(
            view_args.clone(page=0,
                            sort=view_args.sort,
                            sort_desc=view_args.sort_desc,
                            search=None,
                            filters=None))

        list_data = list()
        for widget_item in data:
            obj = copy.deepcopy(widget_item)
            label = WidgetSettingView.get_label_display_to_list(
                widget_item.widget_id)
            obj.label = label
            list_data.append(obj)

        return self.render(
            self.list_template,
            data=list_data,
            list_forms=list_forms,
            delete_form=delete_form,
            action_form=action_form,

            # List
            list_columns=self._list_columns,
            sortable_columns=self._sortable_columns,
            editable_columns=self.column_editable_list,
            list_row_actions=self.get_list_row_actions(),

            # Pagination
            count=count,
            pager_url=pager_url,
            num_pages=num_pages,
            can_set_page_size=self.can_set_page_size,
            page_size_url=page_size_url,
            page=view_args.page,
            page_size=page_size,
            default_page_size=self.page_size,

            # Sorting
            sort_column=view_args.sort,
            sort_desc=view_args.sort_desc,
            sort_url=sort_url,

            # Search
            search_supported=self._search_supported,
            clear_search_url=clear_search_url,
            search=view_args.search,
            search_placeholder=self.search_placeholder(),

            # Filters
            filters=self._filters,
            filter_groups=self._get_filter_groups(),
            active_filters=view_args.filters,
            filter_args=self._get_filters(view_args.filters),

            # Actions
            actions=actions,
            actions_confirmation=actions_confirmation,

            # Misc
            enumerate=enumerate,
            get_pk_value=self.get_pk_value,
            get_value=self.get_list_value,
            return_url=self._get_list_url(view_args),
        )

    @expose('/new/', methods=('GET', 'POST'))
    def create_view(self):
        return_url = get_redirect_target() or self.get_url('.index_view')

        if not self.can_create:
            return redirect(return_url)
        return self.render(config.WEKO_GRIDLAYOUT_ADMIN_CREATE_WIDGET_SETTINGS,
                           return_url=return_url)

    @expose('/edit/', methods=('GET', 'POST'))
    def edit_view(self):
        """Define Api for edit view.

        Returns:
            HTML page -- Html page for edit view

        """
        return_url = get_redirect_target() or self.get_url('.index_view')

        if not self.can_edit:
            return redirect(return_url)

        id_list = helpers.get_mdict_item_or_list(request.args, 'id')

        widget_data = convert_widget_data_to_dict(self.get_one(id_list))
        multi_lang_data = WidgetMultiLangData.get_by_widget_id(id_list)
        converted_data = convert_data_to_design_pack(widget_data,
                                                     multi_lang_data)
        model = convert_data_to_edit_pack(converted_data)
        if model is None:
            flash(gettext('Record does not exist.'), 'error')
            return redirect(return_url)

        return self.render(config.WEKO_GRIDLAYOUT_ADMIN_EDIT_WIDGET_SETTINGS,
                           model=json.dumps(model),
                           return_url=return_url)

    @contextfunction
    def get_detail_value(self, context, model, name):
        """Returns the value to be displayed in the detail view.

        :param context:
            :py:class:`jinja2.runtime.Context`
            :param model: Model instance
            :param name: Field name
        """
        data_settings = model.settings
        data_settings = json.loads(data_settings) \
            if isinstance(data_settings, str) else data_settings
        data_settings_model = namedtuple(
            "Settings", data_settings.keys())(*data_settings.values())
        if name == "label_color" or name == "frame_border" \
            or name == "frame_border_color" or name == "text_color" \
                or name == "background_color":
            return super()._get_list_value(
                context,
                data_settings_model,
                name,
                self.column_formatters_detail,
                self.column_type_formatters_detail,
            )
        else:
            return super()._get_list_value(
                context,
                model,
                name,
                self.column_formatters_detail,
                self.column_type_formatters_detail,
            )

    @expose('/details/')
    def details_view(self):
        """Details model view."""
        return_url = get_redirect_target() or self.get_url('.index_view')

        if not self.can_view_details:
            return redirect(return_url)

        widget_item_id = helpers.get_mdict_item_or_list(request.args, 'id')
        if widget_item_id is None:
            return redirect(return_url)

        model = self.get_one(widget_item_id)
        label = WidgetSettingView.get_label_display_to_list(model.widget_id)
        model.label = label

        if model is None:
            flash(gettext('Record does not exist.'), 'error')
            return redirect(return_url)

        if self.details_modal and request.args.get('modal'):
            template = self.details_modal_template
        else:
            template = self.details_template

        return self.render(template,
                           model=model,
                           details_columns=self._details_columns,
                           get_value=self.get_detail_value,
                           return_url=return_url)

    @action('delete', lazy_gettext('Delete'),
            lazy_gettext('Are you sure you want to delete selected records?'))
    def action_delete(self, ids):
        try:
            query = tools.get_query_for_ids(self.get_query(), self.model, ids)

            if self.fast_mass_delete:
                count = query.delete(synchronize_session=False)
            else:
                count = 0
                for m in query.all():
                    if self.delete_model(m, self.session):
                        count += 1

            self.session.commit()

            flash(
                ngettext('Record was successfully deleted.',
                         '%(count)s records were successfully deleted.',
                         count,
                         count=count), 'success')
        except Exception as ex:
            if not self.handle_view_exception(ex):
                raise

            flash(
                gettext('Failed to delete records. %(error)s', error=str(ex)),
                'error')

    def get_query(self):
        return self.session.query(
            self.model).filter(self.model.is_deleted == 'False')

    def get_count_query(self):
        return self.session.query(
            func.count('*')).filter(self.model.is_deleted == 'False')

    def delete_model(self, model, session=None):
        """Delete model.

        :param
        model: Model to delete
        session: session to delete
        """
        if not self.on_model_delete(model):
            flash(
                _(
                    "Cannot delete widget (ID: %(widget_id)s, "
                    "because it's setting in Widget Design.",
                    widget_id=model.widget_id), 'error')
            return False
        try:
            if session:
                WidgetItemServices.delete_multi_item_by_id(
                    model.widget_id, session)
                return True
            else:
                self.session.flush()
                WidgetItemServices.delete_by_id(model.widget_id)
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(_('Failed to delete record. %(error)s', error=str(ex)),
                      'error')
                current_app.logger.error('Failed to delete record: ', ex)

            self.session.rollback()

            return False
        else:
            self.after_model_delete(model)

        return True

    def on_model_delete(self, model):
        """Define action before delete model.

        Arguments:
            model {widget_item} -- [item to be deleted]

        Returns:
            [false] -- [it is being used in widget design]
            [true] -- [it isn't being used in widget design]

        """
        if WidgetDesignServices.validate_admin_widget_item_setting(
                model.widget_id):
            return False
        return True

    column_list = (
        'widget_id',
        'repository_id',
        'widget_type',
        'label',
        'is_enabled',
    )

    column_searchable_list = ('repository_id', 'widget_type', 'is_enabled')

    column_details_list = (
        'repository_id',
        'widget_type',
        'label',
        'label_color',
        'frame_border',
        'frame_border_color',
        'text_color',
        'background_color',
        'browsing_role',
    )

    form_extra_fields = {
        'repo_selected': StringField('Repository Selector'),
    }

    column_labels = dict(
        widget_id=_('ID'),
        repository_id=_('Repository'),
        widget_type=_('Widget Type'),
        label=_('Label'),
        is_enabled=_('Enable'),
        frame_border=_('Has Frame Border'),
    )
Esempio n. 24
0
class ModelView(BaseModelView):
    """
        MongoEngine model scaffolding.
    """

    column_filters = None
    """
        Collection of the column filters.

        Should contain instances of
        :class:`flask_admin.contrib.pymongo.filters.BasePyMongoFilter` classes.

        Filters will be grouped by name when displayed in the drop-down.

        For example::

            from flask_admin.contrib.pymongo.filters import BooleanEqualFilter

            class MyModelView(BaseModelView):
                column_filters = (BooleanEqualFilter(column=User.name, name='Name'),)

        or::

            from flask_admin.contrib.pymongo.filters import BasePyMongoFilter

            class FilterLastNameBrown(BasePyMongoFilter):
                def apply(self, query, value):
                    if value == '1':
                        return query.filter(self.column == "Brown")
                    else:
                        return query.filter(self.column != "Brown")

                def operation(self):
                    return 'is Brown'

            class MyModelView(BaseModelView):
                column_filters = [
                    FilterLastNameBrown(
                        column=User.last_name, name='Last Name',
                        options=(('1', 'Yes'), ('0', 'No'))
                    )
                ]
    """

    def __init__(self, coll,
                 name=None, category=None, endpoint=None, url=None,
                 menu_class_name=None, menu_icon_type=None, menu_icon_value=None):
        """
            Constructor

            :param coll:
                MongoDB collection object
            :param name:
                Display name
            :param category:
                Display category
            :param endpoint:
                Endpoint
            :param url:
                Custom URL
            :param menu_class_name:
                Optional class name for the menu item.
            :param menu_icon_type:
                Optional icon. Possible icon types:

                 - `flask_admin.consts.ICON_TYPE_GLYPH` - Bootstrap glyph icon
                 - `flask_admin.consts.ICON_TYPE_FONT_AWESOME` - Font Awesome icon
                 - `flask_admin.consts.ICON_TYPE_IMAGE` - Image relative to Flask static directory
                 - `flask_admin.consts.ICON_TYPE_IMAGE_URL` - Image with full URL
            :param menu_icon_value:
                Icon glyph name or URL, depending on `menu_icon_type` setting
        """
        self._search_fields = []

        if name is None:
            name = self._prettify_name(coll.name)

        if endpoint is None:
            endpoint = ('%sview' % coll.name).lower()

        super(ModelView, self).__init__(None, name, category, endpoint, url,
                                        menu_class_name=menu_class_name,
                                        menu_icon_type=menu_icon_type,
                                        menu_icon_value=menu_icon_value)

        self.coll = coll

    def scaffold_pk(self):
        return '_id'

    def get_pk_value(self, model):
        """
            Return primary key value from the model instance

            :param model:
                Model instance
        """
        return model.get('_id')

    def scaffold_list_columns(self):
        """
            Scaffold list columns
        """
        raise NotImplementedError()

    def scaffold_sortable_columns(self):
        """
            Return sortable columns dictionary (name, field)
        """
        return []

    def init_search(self):
        """
            Init search
        """
        if self.column_searchable_list:
            for p in self.column_searchable_list:
                if not isinstance(p, string_types):
                    raise ValueError('Expected string')

                # TODO: Validation?

                self._search_fields.append(p)

        return bool(self._search_fields)

    def scaffold_filters(self, attr):
        """
            Return filter object(s) for the field

            :param name:
                Either field name or field instance
        """
        raise NotImplementedError()

    def is_valid_filter(self, filter):
        """
            Validate if it is valid MongoEngine filter

            :param filter:
                Filter object
        """
        return isinstance(filter, BasePyMongoFilter)

    def scaffold_form(self):
        raise NotImplementedError()

    def _get_field_value(self, model, name):
        """
            Get unformatted field value from the model
        """
        return model.get(name)

    def _search(self, query, search_term):
        values = search_term.split(' ')

        queries = []

        # Construct inner querie
        for value in values:
            if not value:
                continue

            regex = parse_like_term(value)

            stmt = []
            for field in self._search_fields:
                stmt.append({field: {'$regex': regex}})

            if stmt:
                if len(stmt) == 1:
                    queries.append(stmt[0])
                else:
                    queries.append({'$or': stmt})

        # Construct final query
        if queries:
            if len(queries) == 1:
                final = queries[0]
            else:
                final = {'$and': queries}

            if query:
                query = {'$and': [query, final]}
            else:
                query = final

        return query

    def get_list(self, page, sort_column, sort_desc, search, filters,
                 execute=True, page_size=None):
        """
            Get list of objects from MongoEngine

            :param page:
                Page number
            :param sort_column:
                Sort column
            :param sort_desc:
                Sort descending
            :param search:
                Search criteria
            :param filters:
                List of applied fiters
            :param execute:
                Run query immediately or not
            :param page_size:
                Number of results. Defaults to ModelView's page_size. Can be
                overriden to change the page_size limit. Removing the page_size
                limit requires setting page_size to 0 or False.
        """
        query = {}

        # Filters
        if self._filters:
            data = []

            for flt, flt_name, value in filters:
                f = self._filters[flt]
                data = f.apply(data, f.clean(value))

            if data:
                if len(data) == 1:
                    query = data[0]
                else:
                    query['$and'] = data

        # Search
        if self._search_supported and search:
            query = self._search(query, search)

        # Get count
        count = self.coll.find(query).count() if not self.simple_list_pager else None

        # Sorting
        sort_by = None

        if sort_column:
            sort_by = [(sort_column, pymongo.DESCENDING if sort_desc else pymongo.ASCENDING)]
        else:
            order = self._get_default_order()

            if order:
                sort_by = [(col, pymongo.DESCENDING if desc else pymongo.ASCENDING)
                           for (col, desc) in order]

        # Pagination
        if page_size is None:
            page_size = self.page_size

        skip = 0

        if page and page_size:
            skip = page * page_size

        results = self.coll.find(query, sort=sort_by, skip=skip, limit=page_size)

        if execute:
            results = list(results)

        return count, results

    def _get_valid_id(self, id):
        try:
            return ObjectId(id)
        except InvalidId:
            return id

    def get_one(self, id):
        """
            Return single model instance by ID

            :param id:
                Model ID
        """
        return self.coll.find_one({'_id': self._get_valid_id(id)})

    def edit_form(self, obj):
        """
            Create edit form from the MongoDB document
        """
        return self._edit_form_class(get_form_data(), **obj)

    def create_model(self, form):
        """
            Create model helper

            :param form:
                Form instance
        """
        try:
            model = form.data
            self._on_model_change(form, model, True)
            self.coll.insert(model)
        except Exception as ex:
            flash(gettext('Failed to create record. %(error)s', error=str(ex)),
                  'error')
            log.exception('Failed to create record.')
            return False
        else:
            self.after_model_change(form, model, True)

        return model

    def update_model(self, form, model):
        """
            Update model helper

            :param form:
                Form instance
            :param model:
                Model instance to update
        """
        try:
            model.update(form.data)
            self._on_model_change(form, model, False)

            pk = self.get_pk_value(model)
            self.coll.update({'_id': pk}, model)
        except Exception as ex:
            flash(gettext('Failed to update record. %(error)s', error=str(ex)),
                  'error')
            log.exception('Failed to update record.')
            return False
        else:
            self.after_model_change(form, model, False)

        return True

    def delete_model(self, model):
        """
            Delete model helper

            :param model:
                Model instance
        """
        try:
            pk = self.get_pk_value(model)

            if not pk:
                raise ValueError('Document does not have _id')

            self.on_model_delete(model)
            self.coll.remove({'_id': pk})
        except Exception as ex:
            flash(gettext('Failed to delete record. %(error)s', error=str(ex)),
                  'error')
            log.exception('Failed to delete record.')
            return False
        else:
            self.after_model_delete(model)

        return True

    # Default model actions
    def is_action_allowed(self, name):
        # Check delete action permission
        if name == 'delete' and not self.can_delete:
            return False

        return super(ModelView, self).is_action_allowed(name)

    @action('delete',
            lazy_gettext('Delete'),
            lazy_gettext('Are you sure you want to delete selected records?'))
    def action_delete(self, ids):
        try:
            count = 0

            # TODO: Optimize me
            for pk in ids:
                if self.delete_model(self.get_one(pk)):
                    count += 1

            flash(ngettext('Record was successfully deleted.',
                           '%(count)s records were successfully deleted.',
                           count,
                           count=count), 'success')
        except Exception as ex:
            flash(gettext('Failed to delete records. %(error)s', error=str(ex)), 'error')
Esempio n. 25
0
class ModelView(BaseModelView):
    column_filters = None
    """
        Collection of the column filters.

        Can contain either field names or instances of
        :class:`flask_admin.contrib.peewee.filters.BasePeeweeFilter` classes.

        Filters will be grouped by name when displayed in the drop-down.

        For example::

            class MyModelView(BaseModelView):
                column_filters = ('user', 'email')

        or::

            from flask_admin.contrib.peewee.filters import BooleanEqualFilter

            class MyModelView(BaseModelView):
                column_filters = (BooleanEqualFilter(column=User.name, name='Name'),)

        or::

            from flask_admin.contrib.peewee.filters import BasePeeweeFilter

            class FilterLastNameBrown(BasePeeweeFilter):
                def apply(self, query, value):
                    if value == '1':
                        return query.filter(self.column == "Brown")
                    else:
                        return query.filter(self.column != "Brown")

                def operation(self):
                    return 'is Brown'

            class MyModelView(BaseModelView):
                column_filters = [
                    FilterLastNameBrown(
                        column=User.last_name, name='Last Name',
                        options=(('1', 'Yes'), ('0', 'No'))
                    )
                ]
    """

    model_form_converter = CustomModelConverter
    """
        Model form conversion class. Use this to implement custom field conversion logic.

        For example::

            class MyModelConverter(AdminModelConverter):
                pass


            class MyAdminView(ModelView):
                model_form_converter = MyModelConverter
    """

    inline_model_form_converter = InlineModelConverter
    """
        Inline model conversion class. If you need some kind of post-processing for inline
        forms, you can customize behavior by doing something like this::

            class MyInlineModelConverter(AdminModelConverter):
                def post_process(self, form_class, info):
                    form_class.value = TextField('value')
                    return form_class

            class MyAdminView(ModelView):
                inline_model_form_converter = MyInlineModelConverter
    """

    filter_converter = filters.FilterConverter()
    """
        Field to filter converter.

        Override this attribute to use non-default converter.
    """

    fast_mass_delete = False
    """
        If set to `False` and user deletes more than one model using actions,
        all models will be read from the database and then deleted one by one
        giving Peewee chance to manually cleanup any dependencies (many-to-many
        relationships, etc).

        If set to True, will run DELETE statement which is somewhat faster, but
        might leave corrupted data if you forget to configure DELETE CASCADE
        for your model.
    """

    inline_models = None
    """
        Inline related-model editing for models with parent to child relation.

        Accept enumerable with one of the values:

        1. Child model class::

            class MyModelView(ModelView):
                inline_models = (Post,)

        2. Child model class and additional options::

            class MyModelView(ModelView):
                inline_models = [(Post, dict(form_columns=['title']))]

        3. Django-like ``InlineFormAdmin`` class instance::

            from flask_admin.model.form import InlineFormAdmin

            class MyInlineModelForm(InlineFormAdmin):
                form_columns = ('title', 'date')

            class MyModelView(ModelView):
                inline_models = (MyInlineModelForm(MyInlineModel),)

        You can customize generated field name by:

        1. Using `form_name` property as option:

            class MyModelView(ModelView):
                inline_models = ((Post, dict(form_label='Hello')))

        2. Using field's related_name:

            class Model1(Base):
                # ...
                pass

            class Model2(Base):
                # ...
                model1 = ForeignKeyField(related_name="model_twos")

            class MyModel1View(Base):
                inline_models = (Model2,)
                column_labels = {'model_ones': 'Hello'}
    """
    def __init__(
        self,
        model,
        name=None,
        category=None,
        endpoint=None,
        url=None,
        static_folder=None,
        menu_class_name=None,
        menu_icon_type=None,
        menu_icon_value=None,
    ):
        self._search_fields = []

        super(ModelView, self).__init__(
            model,
            name,
            category,
            endpoint,
            url,
            static_folder,
            menu_class_name=menu_class_name,
            menu_icon_type=menu_icon_type,
            menu_icon_value=menu_icon_value,
        )

        self._primary_key = self.scaffold_pk()

    def _get_model_fields(self, model=None):
        if model is None:
            model = self.model

        return ((field.name, field) for field in get_meta_fields(model))

    def scaffold_pk(self):
        return get_primary_key(self.model)

    def get_pk_value(self, model):
        return getattr(model, self._primary_key)

    def scaffold_list_columns(self):
        columns = []

        for n, f in self._get_model_fields():
            # Verify type
            field_class = type(f)

            if field_class == ForeignKeyField:
                columns.append(n)
            elif self.column_display_pk or field_class != PrimaryKeyField:
                columns.append(n)

        return columns

    def scaffold_sortable_columns(self):
        columns = dict()

        for n, f in self._get_model_fields():
            if self.column_display_pk or type(f) != PrimaryKeyField:
                columns[n] = f

        return columns

    def init_search(self):
        if self.column_searchable_list:
            for p in self.column_searchable_list:
                if isinstance(p, string_types):
                    p = getattr(self.model, p)

                # Check type
                if not isinstance(p, (CharField, TextField)):
                    raise Exception("Can only search on text columns. " +
                                    'Failed to setup search for "%s"' % p)

                self._search_fields.append(p)

        return bool(self._search_fields)

    def scaffold_filters(self, name):
        if isinstance(name, string_types):
            attr = getattr(self.model, name, None)
        else:
            attr = name

        if attr is None:
            raise Exception("Failed to find field for filter: %s" % name)

        # Check if field is in different model
        try:
            if attr.model_class != self.model:
                visible_name = "%s / %s" % (
                    self.get_column_name(attr.model_class.__name__),
                    self.get_column_name(attr.name),
                )
            else:
                if not isinstance(name, string_types):
                    visible_name = self.get_column_name(attr.name)
                else:
                    visible_name = self.get_column_name(name)
        except AttributeError:
            if attr.model != self.model:
                visible_name = "%s / %s" % (
                    self.get_column_name(attr.model.__name__),
                    self.get_column_name(attr.name),
                )
            else:
                if not isinstance(name, string_types):
                    visible_name = self.get_column_name(attr.name)
                else:
                    visible_name = self.get_column_name(name)

        type_name = type(attr).__name__
        flt = self.filter_converter.convert(type_name, attr, visible_name)

        return flt

    def is_valid_filter(self, filter):
        return isinstance(filter, filters.BasePeeweeFilter)

    def scaffold_form(self):
        form_class = get_form(
            self.model,
            self.model_form_converter(self),
            base_class=self.form_base_class,
            only=self.form_columns,
            exclude=self.form_excluded_columns,
            field_args=self.form_args,
            # Allow child to specify pk, so inline_models
            # can be ModelViews. But don't auto-generate
            # pk field if form_columns is empty -- allow
            # default behaviour in that case.
            allow_pk=bool(self.form_columns),
            extra_fields=self.form_extra_fields,
        )

        if self.inline_models:
            form_class = self.scaffold_inline_form_models(form_class)

        return form_class

    def scaffold_list_form(self, widget=None, validators=None):
        """
            Create form for the `index_view` using only the columns from
            `self.column_editable_list`.

            :param widget:
                WTForms widget class. Defaults to `XEditableWidget`.
            :param validators:
                `form_args` dict with only validators
                {'name': {'validators': [required()]}}
        """
        form_class = get_form(
            self.model,
            self.model_form_converter(self),
            base_class=self.form_base_class,
            only=self.column_editable_list,
            field_args=validators,
        )

        return create_editable_list_form(self.form_base_class, form_class,
                                         widget)

    def scaffold_inline_form_models(self, form_class):
        converter = self.model_form_converter(self)
        inline_converter = self.inline_model_form_converter(self)

        for m in self.inline_models:
            form_class = inline_converter.contribute(converter, self.model,
                                                     form_class, m)

        return form_class

    # AJAX foreignkey support
    def _create_ajax_loader(self, name, options):
        return create_ajax_loader(self.model, name, name, options)

    def _handle_join(self, query, field, joins):
        try:
            if field.model_class != self.model:
                model_name = field.model_class.__name__

                if model_name not in joins:
                    query = query.join(field.model_class, JOIN.LEFT_OUTER)
                    joins.add(model_name)
        except AttributeError:
            if field.model != self.model:
                model_name = field.model.__name__

                if model_name not in joins:
                    query = query.join(field.model, JOIN.LEFT_OUTER)
                    joins.add(model_name)

        return query

    def _order_by(self, query, joins, sort_field, sort_desc):
        if isinstance(sort_field, string_types):
            field = getattr(self.model, sort_field)
            query = query.order_by(field.desc() if sort_desc else field.asc())
        elif isinstance(sort_field, Field):
            try:
                if sort_field.model_class != self.model:
                    query = self._handle_join(query, sort_field, joins)
            except AttributeError:
                if sort_field.model != self.model:
                    query = self._handle_join(query, sort_field, joins)

            query = query.order_by(
                sort_field.desc() if sort_desc else sort_field.asc())

        return query, joins

    def get_query(self):
        return self.model.select()

    def get_list(
        self,
        page,
        sort_column,
        sort_desc,
        search,
        filters,
        execute=True,
        page_size=None,
    ):
        """
            Return records from the database.

            :param page:
                Page number
            :param sort_column:
                Sort column name
            :param sort_desc:
                Descending or ascending sort
            :param search:
                Search query
            :param filters:
                List of filter tuples
            :param execute:
                Execute query immediately? Default is `True`
            :param page_size:
                Number of results. Defaults to ModelView's page_size. Can be
                overriden to change the page_size limit. Removing the page_size
                limit requires setting page_size to 0 or False.
        """

        query = self.get_query()

        joins = set()

        # Search
        if self._search_supported and search:
            values = search.split(" ")

            for value in values:
                if not value:
                    continue

                term = parse_like_term(value)

                stmt = None
                for field in self._search_fields:
                    query = self._handle_join(query, field, joins)

                    q = field**term

                    if stmt is None:
                        stmt = q
                    else:
                        stmt |= q

                query = query.where(stmt)

        # Filters
        if self._filters:
            for flt, flt_name, value in filters:
                f = self._filters[flt]

                query = self._handle_join(query, f.column, joins)
                query = f.apply(query, f.clean(value))

        # Get count
        count = query.count() if not self.simple_list_pager else None

        # Apply sorting
        if sort_column is not None:
            sort_field = self._sortable_columns[sort_column]

            query, joins = self._order_by(query, joins, sort_field, sort_desc)
        else:
            order = self._get_default_order()

            if order:
                query, joins = self._order_by(query, joins, order[0], order[1])

        # Pagination
        if page_size is None:
            page_size = self.page_size

        if page_size:
            query = query.limit(page_size)

        if page and page_size:
            query = query.offset(page * page_size)

        if execute:
            query = list(query.execute())

        return count, query

    def get_one(self, id):
        return self.model.get(**{self._primary_key: id})

    def create_model(self, form):
        try:
            model = self.model()
            form.populate_obj(model)
            self._on_model_change(form, model, True)
            model.save()

            # For peewee have to save inline forms after model was saved
            save_inline(form, model)
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext("Failed to create record. %(error)s",
                            error=str(ex)),
                    "error",
                )
                log.exception("Failed to create record.")

            return False
        else:
            self.after_model_change(form, model, True)

        return model

    def update_model(self, form, model):
        try:
            form.populate_obj(model)
            self._on_model_change(form, model, False)
            model.save()

            # For peewee have to save inline forms after model was saved
            save_inline(form, model)
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext("Failed to update record. %(error)s",
                            error=str(ex)),
                    "error",
                )
                log.exception("Failed to update record.")

            return False
        else:
            self.after_model_change(form, model, False)

        return True

    def delete_model(self, model):
        try:
            self.on_model_delete(model)
            model.delete_instance(recursive=True)
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext("Failed to delete record. %(error)s",
                            error=str(ex)),
                    "error",
                )
                log.exception("Failed to delete record.")

            return False
        else:
            self.after_model_delete(model)

        return True

    # Default model actions
    def is_action_allowed(self, name):
        # Check delete action permission
        if name == "delete" and not self.can_delete:
            return False

        return super(ModelView, self).is_action_allowed(name)

    @action(
        "delete",
        lazy_gettext("Delete"),
        lazy_gettext("Are you sure you want to delete selected records?"),
    )
    def action_delete(self, ids):
        try:
            model_pk = getattr(self.model, self._primary_key)

            if self.fast_mass_delete:
                count = self.model.delete().where(model_pk << ids).execute()
            else:
                count = 0

                query = self.model.select().filter(model_pk << ids)

                for m in query:
                    self.on_model_delete(m)
                    m.delete_instance(recursive=True)
                    count += 1

            flash(
                ngettext(
                    "Record was successfully deleted.",
                    "%(count)s records were successfully deleted.",
                    count,
                    count=count,
                ),
                "success",
            )
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext("Failed to delete records. %(error)s",
                            error=str(ex)),
                    "error",
                )
Esempio n. 26
0
class ModelView(BaseModelView):
    """
        SQLAlchemy model view

        Usage sample::

            admin = Admin()
            admin.add_view(ModelView(User, db.session))
    """

    column_auto_select_related = ObsoleteAttr('column_auto_select_related',
                                              'auto_select_related', True)
    """
        Enable automatic detection of displayed foreign keys in this view
        and perform automatic joined loading for related models to improve
        query performance.

        Please note that detection is not recursive: if `__unicode__` method
        of related model uses another model to generate string representation, it
        will still make separate database call.
    """

    column_select_related_list = ObsoleteAttr('column_select_related',
                                              'list_select_related', None)
    """
        List of parameters for SQLAlchemy `subqueryload`. Overrides `column_auto_select_related`
        property.

        For example::

            class PostAdmin(ModelView):
                column_select_related_list = ('user', 'city')

        You can also use properties::

            class PostAdmin(ModelView):
                column_select_related_list = (Post.user, Post.city)

        Please refer to the `subqueryload` on list of possible values.
    """

    column_display_all_relations = ObsoleteAttr('column_display_all_relations',
                                                'list_display_all_relations',
                                                False)
    """
        Controls if list view should display all relations, not only many-to-one.
    """

    column_searchable_list = ObsoleteAttr('column_searchable_list',
                                          'searchable_columns', None)
    """
        Collection of the searchable columns.

        Example::

            class MyModelView(ModelView):
                column_searchable_list = ('name', 'email')

        You can also pass columns::

            class MyModelView(ModelView):
                column_searchable_list = (User.name, User.email)

        The following search rules apply:

        - If you enter ``ZZZ`` in the UI search field, it will generate ``ILIKE '%ZZZ%'``
          statement against searchable columns.

        - If you enter multiple words, each word will be searched separately, but
          only rows that contain all words will be displayed. For example, searching
          for ``abc def`` will find all rows that contain ``abc`` and ``def`` in one or
          more columns.

        - If you prefix your search term with ``^``, it will find all rows
          that start with ``^``. So, if you entered ``^ZZZ`` then ``ILIKE 'ZZZ%'`` will be used.

        - If you prefix your search term with ``=``, it will perform an exact match.
          For example, if you entered ``=ZZZ``, the statement ``ILIKE 'ZZZ'`` will be used.
    """

    column_filters = None
    """
        Collection of the column filters.

        Can contain either field names or instances of
        :class:`flask_admin.contrib.sqla.filters.BaseSQLAFilter` classes.

        Filters will be grouped by name when displayed in the drop-down.

        For example::

            class MyModelView(BaseModelView):
                column_filters = ('user', 'email')

        or::

            from flask_admin.contrib.sqla.filters import BooleanEqualFilter

            class MyModelView(BaseModelView):
                column_filters = (BooleanEqualFilter(column=User.name, name='Name'),)

        or::

            from flask_admin.contrib.sqla.filters import BaseSQLAFilter

            class FilterLastNameBrown(BaseSQLAFilter):
                def apply(self, query, value, alias=None):
                    if value == '1':
                        return query.filter(self.column == "Brown")
                    else:
                        return query.filter(self.column != "Brown")

                def operation(self):
                    return 'is Brown'

            class MyModelView(BaseModelView):
                column_filters = [
                    FilterLastNameBrown(
                        User.last_name, 'Last Name', options=(('1', 'Yes'), ('0', 'No'))
                    )
                ]
    """

    model_form_converter = form.AdminModelConverter
    """
        Model form conversion class. Use this to implement custom field conversion logic.

        For example::

            class MyModelConverter(AdminModelConverter):
                pass


            class MyAdminView(ModelView):
                model_form_converter = MyModelConverter
    """

    inline_model_form_converter = form.InlineModelConverter
    """
        Inline model conversion class. If you need some kind of post-processing for inline
        forms, you can customize behavior by doing something like this::

            class MyInlineModelConverter(InlineModelConverter):
                def post_process(self, form_class, info):
                    form_class.value = wtf.StringField('value')
                    return form_class

            class MyAdminView(ModelView):
                inline_model_form_converter = MyInlineModelConverter
    """

    filter_converter = sqla_filters.FilterConverter()
    """
        Field to filter converter.

        Override this attribute to use non-default converter.
    """

    fast_mass_delete = False
    """
        If set to `False` and user deletes more than one model using built in action,
        all models will be read from the database and then deleted one by one
        giving SQLAlchemy a chance to manually cleanup any dependencies (many-to-many
        relationships, etc).

        If set to `True`, will run a ``DELETE`` statement which is somewhat faster,
        but may leave corrupted data if you forget to configure ``DELETE
        CASCADE`` for your model.
    """

    inline_models = None
    """
        Inline related-model editing for models with parent-child relations.

        Accepts enumerable with one of the following possible values:

        1. Child model class::

            class MyModelView(ModelView):
                inline_models = (Post,)

        2. Child model class and additional options::

            class MyModelView(ModelView):
                inline_models = [(Post, dict(form_columns=['title']))]

        3. Django-like ``InlineFormAdmin`` class instance::

            from flask_admin.model.form import InlineFormAdmin

            class MyInlineModelForm(InlineFormAdmin):
                form_columns = ('title', 'date')

            class MyModelView(ModelView):
                inline_models = (MyInlineModelForm(MyInlineModel),)

        You can customize the generated field name by:

        1. Using the `form_name` property as a key to the options dictionary::

            class MyModelView(ModelView):
                inline_models = ((Post, dict(form_label='Hello')))

        2. Using forward relation name and `column_labels` property::

            class Model1(Base):
                pass

            class Model2(Base):
                # ...
                model1 = relation(Model1, backref='models')

            class MyModel1View(Base):
                inline_models = (Model2,)
                column_labels = {'models': 'Hello'}
    """

    column_type_formatters = DEFAULT_FORMATTERS

    form_choices = None
    """
        Map choices to form fields

        Example::

            class MyModelView(BaseModelView):
                form_choices = {'my_form_field': [
                    ('db_value', 'display_value'),
                ]}
    """

    form_optional_types = (Boolean, )
    """
        List of field types that should be optional if column is not nullable.

        Example::

            class MyModelView(BaseModelView):
                form_optional_types = (Boolean, Unicode)
    """

    ignore_hidden = True
    """
       Ignore field that starts with "_"

       Example::

           class MyModelView(BaseModelView):
               ignore_hidden = False
    """
    def __init__(self,
                 model,
                 session,
                 name=None,
                 category=None,
                 endpoint=None,
                 url=None,
                 static_folder=None,
                 menu_class_name=None,
                 menu_icon_type=None,
                 menu_icon_value=None):
        """
            Constructor.

            :param model:
                Model class
            :param session:
                SQLAlchemy session
            :param name:
                View name. If not set, defaults to the model name
            :param category:
                Category name
            :param endpoint:
                Endpoint name. If not set, defaults to the model name
            :param url:
                Base URL. If not set, defaults to '/admin/' + endpoint
            :param menu_class_name:
                Optional class name for the menu item.
            :param menu_icon_type:
                Optional icon. Possible icon types:

                 - `flask_admin.consts.ICON_TYPE_GLYPH` - Bootstrap glyph icon
                 - `flask_admin.consts.ICON_TYPE_FONT_AWESOME` - Font Awesome icon
                 - `flask_admin.consts.ICON_TYPE_IMAGE` - Image relative to Flask static directory
                 - `flask_admin.consts.ICON_TYPE_IMAGE_URL` - Image with full URL
            :param menu_icon_value:
                Icon glyph name or URL, depending on `menu_icon_type` setting
        """
        self.session = session

        self._search_fields = None

        self._filter_joins = dict()

        self._sortable_joins = dict()

        if self.form_choices is None:
            self.form_choices = {}

        super(ModelView, self).__init__(model,
                                        name,
                                        category,
                                        endpoint,
                                        url,
                                        static_folder,
                                        menu_class_name=menu_class_name,
                                        menu_icon_type=menu_icon_type,
                                        menu_icon_value=menu_icon_value)

        self._manager = manager_of_class(self.model)

        # Primary key
        self._primary_key = self.scaffold_pk()

        if self._primary_key is None:
            raise Exception('Model %s does not have primary key.' %
                            self.model.__name__)

        # Configuration
        if not self.column_select_related_list:
            self._auto_joins = self.scaffold_auto_joins()
        else:
            self._auto_joins = self.column_select_related_list

    # Internal API
    def _get_model_iterator(self, model=None):
        """
            Return property iterator for the model
        """
        if model is None:
            model = self.model

        return model._sa_class_manager.mapper.iterate_properties

    def _apply_path_joins(self, query, joins, path, inner_join=True):
        """
            Apply join path to the query.

            :param query:
                Query to add joins to
            :param joins:
                List of current joins. Used to avoid joining on same relationship more than once
            :param path:
                Path to be joined
            :param fn:
                Join function
        """
        last = None

        if path:
            for item in path:
                key = (inner_join, item)
                alias = joins.get(key)

                if key not in joins:
                    if not isinstance(item, Table):
                        alias = aliased(item.property.mapper.class_)

                    fn = query.join if inner_join else query.outerjoin

                    if last is None:
                        query = fn(item) if alias is None else fn(alias, item)
                    else:
                        prop = getattr(last, item.key)
                        query = fn(prop) if alias is None else fn(alias, prop)

                    joins[key] = alias

                last = alias

        return query, joins, last

    # Scaffolding
    def scaffold_pk(self):
        """
            Return the primary key name(s) from a model
            If model has single primary key, will return a string and tuple otherwise
        """
        return tools.get_primary_key(self.model)

    def get_pk_value(self, model):
        """
            Return the primary key value from a model object.
            If there are multiple primary keys, they're encoded into string representation.
        """
        if isinstance(self._primary_key, tuple):
            return tools.iterencode(
                getattr(model, attr) for attr in self._primary_key)
        else:
            return tools.escape(getattr(model, self._primary_key))

    def scaffold_list_columns(self):
        """
            Return a list of columns from the model.
        """
        columns = []

        for p in self._get_model_iterator():
            if hasattr(p, 'direction'):
                if self.column_display_all_relations or p.direction.name == 'MANYTOONE':
                    columns.append(p.key)
            elif hasattr(p, 'columns'):
                if len(p.columns) > 1:
                    filtered = tools.filter_foreign_columns(
                        self.model.__table__, p.columns)

                    if len(filtered) == 0:
                        continue
                    elif len(filtered) > 1:
                        warnings.warn(
                            'Can not convert multiple-column properties (%s.%s)'
                            % (self.model, p.key))
                        continue

                    column = filtered[0]
                else:
                    column = p.columns[0]

                if column.foreign_keys:
                    continue

                if not self.column_display_pk and column.primary_key:
                    continue

                columns.append(p.key)

        return columns

    def scaffold_sortable_columns(self):
        """
            Return a dictionary of sortable columns.
            Key is column name, value is sort column/field.
        """
        columns = dict()

        for p in self._get_model_iterator():
            if hasattr(p, 'columns'):
                # Sanity check
                if len(p.columns) > 1:
                    # Multi-column properties are not supported
                    continue

                column = p.columns[0]

                # Can't sort on primary or foreign keys by default
                if column.foreign_keys:
                    continue

                if not self.column_display_pk and column.primary_key:
                    continue

                columns[p.key] = column

        return columns

    def get_sortable_columns(self):
        """
            Returns a dictionary of the sortable columns. Key is a model
            field name and value is sort column (for example - attribute).

            If `column_sortable_list` is set, will use it. Otherwise, will call
            `scaffold_sortable_columns` to get them from the model.
        """
        self._sortable_joins = dict()

        if self.column_sortable_list is None:
            return self.scaffold_sortable_columns()
        else:
            result = dict()

            for c in self.column_sortable_list:
                if isinstance(c, tuple):
                    if isinstance(c[1], tuple):
                        column, path = [], []
                        for item in c[1]:
                            column_item, path_item = tools.get_field_with_path(
                                self.model, item)
                            column.append(column_item)
                            path.append(path_item)
                        column_name = c[0]
                    else:
                        column, path = tools.get_field_with_path(
                            self.model, c[1])
                        column_name = c[0]
                else:
                    column, path = tools.get_field_with_path(self.model, c)
                    column_name = text_type(c)

                if path and (hasattr(path[0], 'property')
                             or isinstance(path[0], list)):
                    self._sortable_joins[column_name] = path
                elif path:
                    raise Exception("For sorting columns in a related table, "
                                    "column_sortable_list requires a string "
                                    "like '<relation name>.<column name>'. "
                                    "Failed on: {0}".format(c))
                else:
                    # column is in same table, use only model attribute name
                    if getattr(column, 'key', None) is not None:
                        column_name = column.key

                # column_name must match column_name used in `get_list_columns`
                result[column_name] = column

            return result

    def get_column_names(self, only_columns, excluded_columns):
        """
            Returns a list of tuples with the model field name and formatted
            field name.

            Overridden to handle special columns like InstrumentedAttribute.

            :param only_columns:
                List of columns to include in the results. If not set,
                `scaffold_list_columns` will generate the list from the model.
            :param excluded_columns:
                List of columns to exclude from the results.
        """
        if excluded_columns:
            only_columns = [
                c for c in only_columns if c not in excluded_columns
            ]

        formatted_columns = []
        for c in only_columns:
            try:
                column, path = tools.get_field_with_path(self.model, c)

                if path:
                    # column is a relation (InstrumentedAttribute), use full path
                    column_name = text_type(c)
                else:
                    # column is in same table, use only model attribute name
                    if getattr(column, 'key', None) is not None:
                        column_name = column.key
                    else:
                        column_name = text_type(c)
            except AttributeError:
                # TODO: See ticket #1299 - allow virtual columns. Probably figure out
                # better way to handle it. For now just assume if column was not found - it
                # is virtual and there's column formatter for it.
                column_name = text_type(c)

            visible_name = self.get_column_name(column_name)

            # column_name must match column_name in `get_sortable_columns`
            formatted_columns.append((column_name, visible_name))

        return formatted_columns

    def init_search(self):
        """
            Initialize search. Returns `True` if search is supported for this
            view.

            For SQLAlchemy, this will initialize internal fields: list of
            column objects used for filtering, etc.
        """
        if self.column_searchable_list:
            self._search_fields = []

            for name in self.column_searchable_list:
                attr, joins = tools.get_field_with_path(self.model, name)

                if not attr:
                    raise Exception(
                        'Failed to find field for search field: %s' % name)

                if tools.is_hybrid_property(self.model, name):
                    column = attr
                    if isinstance(name, string_types):
                        column.key = name.split('.')[-1]
                    self._search_fields.append((column, joins))
                else:
                    for column in tools.get_columns_for_field(attr):
                        self._search_fields.append((column, joins))

        return bool(self.column_searchable_list)

    def search_placeholder(self):
        """
            Return search placeholder.

            For example, if set column_labels and column_searchable_list:

            class MyModelView(BaseModelView):
                column_labels = dict(name='Name', last_name='Last Name')
                column_searchable_list = ('name', 'last_name')

            placeholder is: "Name, Last Name"
        """
        if not self.column_searchable_list:
            return None

        placeholders = []

        for searchable in self.column_searchable_list:
            if isinstance(searchable, InstrumentedAttribute):
                placeholders.append(
                    self.column_labels.get(searchable.key, searchable.key))
            else:
                placeholders.append(
                    self.column_labels.get(searchable, searchable))

        return u', '.join(placeholders)

    def scaffold_filters(self, name):
        """
            Return list of enabled filters
        """

        attr, joins = tools.get_field_with_path(self.model, name)

        if attr is None:
            raise Exception('Failed to find field for filter: %s' % name)

        # Figure out filters for related column
        if is_relationship(attr):
            filters = []

            for p in self._get_model_iterator(attr.property.mapper.class_):
                if hasattr(p, 'columns'):
                    # TODO: Check for multiple columns
                    column = p.columns[0]

                    if column.foreign_keys or column.primary_key:
                        continue

                    visible_name = '%s / %s' % (self.get_column_name(
                        attr.prop.target.name), self.get_column_name(p.key))

                    type_name = type(column.type).__name__
                    flt = self.filter_converter.convert(
                        type_name, column, visible_name)

                    if flt:
                        table = column.table

                        if joins:
                            self._filter_joins[column] = joins
                        elif tools.need_join(self.model, table):
                            self._filter_joins[column] = [table]

                        filters.extend(flt)

            return filters
        else:
            is_hybrid_property = tools.is_hybrid_property(self.model, name)
            if is_hybrid_property:
                column = attr
                if isinstance(name, string_types):
                    column.key = name.split('.')[-1]
            else:
                columns = tools.get_columns_for_field(attr)

                if len(columns) > 1:
                    raise Exception(
                        'Can not filter more than on one column for %s' % name)

                column = columns[0]

            # If filter related to relation column (represented by
            # relation_name.target_column) we collect here relation name
            joined_column_name = None
            if isinstance(name, string_types) and '.' in name:
                joined_column_name = name.split('.')[0]

            # Join not needed for hybrid properties
            if (not is_hybrid_property
                    and tools.need_join(self.model, column.table)
                    and name not in self.column_labels):
                if joined_column_name:
                    visible_name = '%s / %s / %s' % (
                        joined_column_name,
                        self.get_column_name(column.table.name),
                        self.get_column_name(column.name))
                else:
                    visible_name = '%s / %s' % (self.get_column_name(
                        column.table.name), self.get_column_name(column.name))
            else:
                if not isinstance(name, string_types):
                    visible_name = self.get_column_name(name.property.key)
                else:
                    if self.column_labels and name in self.column_labels:
                        visible_name = self.column_labels[name]
                    else:
                        visible_name = self.get_column_name(name)
                        visible_name = visible_name.replace('.', ' / ')

            type_name = type(column.type).__name__

            flt = self.filter_converter.convert(
                type_name,
                column,
                visible_name,
                options=self.column_choices.get(name),
            )

            key_name = column
            # In case of filter related to relation column filter key
            # must be named with relation name (to prevent following same
            # target column to replace previous)
            if joined_column_name:
                key_name = "{0}.{1}".format(joined_column_name, column)
                for f in flt:
                    f.key_name = key_name

            if joins:
                self._filter_joins[key_name] = joins
            elif not is_hybrid_property and tools.need_join(
                    self.model, column.table):
                self._filter_joins[key_name] = [column.table]

            return flt

    def handle_filter(self, filter):
        if isinstance(filter, sqla_filters.BaseSQLAFilter):
            column = filter.column

            # hybrid_property joins are not supported yet
            if (isinstance(column, InstrumentedAttribute)
                    and tools.need_join(self.model, column.table)):
                self._filter_joins[column] = [column.table]

        return filter

    def scaffold_form(self):
        """
            Create form from the model.
        """
        converter = self.model_form_converter(self.session, self)
        form_class = form.get_form(self.model,
                                   converter,
                                   base_class=self.form_base_class,
                                   only=self.form_columns,
                                   exclude=self.form_excluded_columns,
                                   field_args=self.form_args,
                                   ignore_hidden=self.ignore_hidden,
                                   extra_fields=self.form_extra_fields)

        if self.inline_models:
            form_class = self.scaffold_inline_form_models(form_class)

        return form_class

    def scaffold_list_form(self, widget=None, validators=None):
        """
            Create form for the `index_view` using only the columns from
            `self.column_editable_list`.

            :param widget:
                WTForms widget class. Defaults to `XEditableWidget`.
            :param validators:
                `form_args` dict with only validators
                {'name': {'validators': [required()]}}
        """
        converter = self.model_form_converter(self.session, self)
        form_class = form.get_form(self.model,
                                   converter,
                                   base_class=self.form_base_class,
                                   only=self.column_editable_list,
                                   field_args=validators)

        return create_editable_list_form(self.form_base_class, form_class,
                                         widget)

    def scaffold_inline_form_models(self, form_class):
        """
            Contribute inline models to the form

            :param form_class:
                Form class
        """
        inline_converter = self.inline_model_form_converter(
            self.session, self, self.model_form_converter)

        for m in self.inline_models:
            form_class = inline_converter.contribute(self.model, form_class, m)

        return form_class

    def scaffold_auto_joins(self):
        """
            Return a list of joined tables by going through the
            displayed columns.
        """
        if not self.column_auto_select_related:
            return []

        relations = set()

        for p in self._get_model_iterator():
            if hasattr(p, 'direction'):
                # Check if it is pointing to same model
                if p.mapper.class_ == self.model:
                    continue

                # Check if it is pointing to a differnet bind
                source_bind = getattr(self.model, '__bind_key__', None)
                target_bind = getattr(p.mapper.class_, '__bind_key__', None)
                if source_bind != target_bind:
                    continue

                if p.direction.name in ['MANYTOONE', 'MANYTOMANY']:
                    relations.add(p.key)

        joined = []

        for prop, name in self._list_columns:
            if prop in relations:
                joined.append(getattr(self.model, prop))

        return joined

    # AJAX foreignkey support
    def _create_ajax_loader(self, name, options):
        return create_ajax_loader(self.model, self.session, name, name,
                                  options)

    # Database-related API
    def get_query(self):
        """
            Return a query for the model type.

            This method can be used to set a "persistent filter" on an index_view.

            Example::

                class MyView(ModelView):
                    def get_query(self):
                        return super(MyView, self).get_query().filter(User.username == current_user.username)


            If you override this method, don't forget to also override `get_count_query`, for displaying the correct
            item count in the list view, and `get_one`, which is used when retrieving records for the edit view.
        """
        return self.session.query(self.model)

    def get_count_query(self):
        """
            Return a the count query for the model type

            A ``query(self.model).count()`` approach produces an excessive
            subquery, so ``query(func.count('*'))`` should be used instead.

            See commit ``#45a2723`` for details.
        """
        return self.session.query(func.count('*')).select_from(self.model)

    def _order_by(self, query, joins, sort_joins, sort_field, sort_desc):
        """
            Apply order_by to the query

            :param query:
                Query
            :pram joins:
                Current joins
            :param sort_joins:
                Sort joins (properties or tables)
            :param sort_field:
                Sort field
            :param sort_desc:
                Ascending or descending
        """
        if sort_field is not None:
            # Handle joins
            query, joins, alias = self._apply_path_joins(query,
                                                         joins,
                                                         sort_joins,
                                                         inner_join=False)

            column = sort_field if alias is None else getattr(
                alias, sort_field.key)

            if sort_desc:
                query = query.order_by(desc(column))
            else:
                query = query.order_by(column)

        return query, joins

    def _get_default_order(self):
        order = super(ModelView, self)._get_default_order()
        for field, direction in (order or []):
            attr, joins = tools.get_field_with_path(self.model, field)
            yield attr, joins, direction

    def _apply_sorting(self, query, joins, sort_column, sort_desc):
        if sort_column is not None:
            if sort_column in self._sortable_columns:
                sort_field = self._sortable_columns[sort_column]
                sort_joins = self._sortable_joins.get(sort_column)

                if isinstance(sort_field, list):
                    for field_item, join_item in zip(sort_field, sort_joins):
                        query, joins = self._order_by(query, joins, join_item,
                                                      field_item, sort_desc)
                else:
                    query, joins = self._order_by(query, joins, sort_joins,
                                                  sort_field, sort_desc)
        else:
            order = self._get_default_order()
            for sort_field, sort_joins, sort_desc in order:
                query, joins = self._order_by(query, joins, sort_joins,
                                              sort_field, sort_desc)

        return query, joins

    def _apply_search(self, query, count_query, joins, count_joins, search):
        """
            Apply search to a query.
        """
        terms = search.split(' ')

        for term in terms:
            if not term:
                continue

            stmt = tools.parse_like_term(term)

            filter_stmt = []
            count_filter_stmt = []

            for field, path in self._search_fields:
                query, joins, alias = self._apply_path_joins(query,
                                                             joins,
                                                             path,
                                                             inner_join=False)

                count_alias = None

                if count_query is not None:
                    count_query, count_joins, count_alias = self._apply_path_joins(
                        count_query, count_joins, path, inner_join=False)

                column = field if alias is None else getattr(alias, field.key)
                filter_stmt.append(cast(column, Unicode).ilike(stmt))

                if count_filter_stmt is not None:
                    column = field if count_alias is None else getattr(
                        count_alias, field.key)
                    count_filter_stmt.append(cast(column, Unicode).ilike(stmt))

            query = query.filter(or_(*filter_stmt))

            if count_query is not None:
                count_query = count_query.filter(or_(*count_filter_stmt))

        return query, count_query, joins, count_joins

    def _apply_filters(self, query, count_query, joins, count_joins, filters):
        for idx, flt_name, value in filters:
            flt = self._filters[idx]

            alias = None
            count_alias = None

            # Figure out joins
            if isinstance(flt, sqla_filters.BaseSQLAFilter):
                # If no key_name is specified, use filter column as filter key
                filter_key = flt.key_name or flt.column
                path = self._filter_joins.get(filter_key, [])

                query, joins, alias = self._apply_path_joins(query,
                                                             joins,
                                                             path,
                                                             inner_join=False)

                if count_query is not None:
                    count_query, count_joins, count_alias = self._apply_path_joins(
                        count_query, count_joins, path, inner_join=False)

            # Clean value .clean() and apply the filter
            clean_value = flt.clean(value)

            try:
                query = flt.apply(query, clean_value, alias)
            except TypeError:
                spec = inspect.getargspec(flt.apply)

                if len(spec.args) == 3:
                    warnings.warn('Please update your custom filter %s to '
                                  'include additional `alias` parameter.' %
                                  repr(flt))
                else:
                    raise

                query = flt.apply(query, clean_value)

            if count_query is not None:
                try:
                    count_query = flt.apply(count_query, clean_value,
                                            count_alias)
                except TypeError:
                    count_query = flt.apply(count_query, clean_value)

        return query, count_query, joins, count_joins

    def _apply_pagination(self, query, page, page_size):
        if page_size is None:
            page_size = self.page_size

        if page_size:
            query = query.limit(page_size)

        if page and page_size:
            query = query.offset(page * page_size)

        return query

    def get_list(self,
                 page,
                 sort_column,
                 sort_desc,
                 search,
                 filters,
                 execute=True,
                 page_size=None):
        """
            Return records from the database.

            :param page:
                Page number
            :param sort_column:
                Sort column name
            :param sort_desc:
                Descending or ascending sort
            :param search:
                Search query
            :param execute:
                Execute query immediately? Default is `True`
            :param filters:
                List of filter tuples
            :param page_size:
                Number of results. Defaults to ModelView's page_size. Can be
                overriden to change the page_size limit. Removing the page_size
                limit requires setting page_size to 0 or False.
        """

        # Will contain join paths with optional aliased object
        joins = {}
        count_joins = {}

        query = self.get_query()
        count_query = self.get_count_query(
        ) if not self.simple_list_pager else None

        # Ignore eager-loaded relations (prevent unnecessary joins)
        # TODO: Separate join detection for query and count query?
        if hasattr(query, '_join_entities'):
            for entity in query._join_entities:
                for table in entity.tables:
                    joins[table] = None

        # Apply search criteria
        if self._search_supported and search:
            query, count_query, joins, count_joins = self._apply_search(
                query, count_query, joins, count_joins, search)

        # Apply filters
        if filters and self._filters:
            query, count_query, joins, count_joins = self._apply_filters(
                query, count_query, joins, count_joins, filters)

        # Calculate number of rows if necessary
        count = count_query.scalar() if count_query else None

        # Auto join
        for j in self._auto_joins:
            query = query.options(joinedload(j))

        # Sorting
        query, joins = self._apply_sorting(query, joins, sort_column,
                                           sort_desc)

        # Pagination
        query = self._apply_pagination(query, page, page_size)

        # Execute if needed
        if execute:
            query = query.all()

        return count, query

    def get_one(self, id):
        """
            Return a single model by its id.

            Example::

                def get_one(self, id):
                    query = self.get_query()
                    return query.filter(self.model.id == id).one()

            Also see `get_query` for how to filter the list view.

            :param id:
                Model id
        """
        return self.session.query(self.model).get(tools.iterdecode(id))

    # Error handler
    def handle_view_exception(self, exc):
        if isinstance(exc, IntegrityError):
            if current_app.config.get(
                    'ADMIN_RAISE_ON_INTEGRITY_ERROR',
                    current_app.config.get('ADMIN_RAISE_ON_VIEW_EXCEPTION')):
                raise
            else:
                flash(
                    gettext('Integrity error. %(message)s',
                            message=text_type(exc)), 'error')
            return True

        return super(ModelView, self).handle_view_exception(exc)

    # Model handlers
    def create_model(self, form):
        """
            Create model from form.

            :param form:
                Form instance
        """
        try:
            model = self._manager.new_instance()
            # TODO: We need a better way to create model instances and stay compatible with
            # SQLAlchemy __init__() behavior
            state = instance_state(model)
            self._manager.dispatch.init(state, [], {})
            form.populate_obj(model)
            self.session.add(model)
            self._on_model_change(form, model, True)
            self.session.commit()
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext('Failed to create record. %(error)s',
                            error=str(ex)), 'error')
                log.exception('Failed to create record.')

            self.session.rollback()

            return False
        else:
            self.after_model_change(form, model, True)

        return model

    def update_model(self, form, model):
        """
            Update model from form.

            :param form:
                Form instance
            :param model:
                Model instance
        """
        try:
            form.populate_obj(model)
            self._on_model_change(form, model, False)
            self.session.commit()
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext('Failed to update record. %(error)s',
                            error=str(ex)), 'error')
                log.exception('Failed to update record.')

            self.session.rollback()

            return False
        else:
            self.after_model_change(form, model, False)

        return True

    def delete_model(self, model):
        """
            Delete model.

            :param model:
                Model to delete
        """
        try:
            self.on_model_delete(model)
            self.session.flush()
            self.session.delete(model)
            self.session.commit()
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(
                    gettext('Failed to delete record. %(error)s',
                            error=str(ex)), 'error')
                log.exception('Failed to delete record.')

            self.session.rollback()

            return False
        else:
            self.after_model_delete(model)

        return True

    # Default model actions
    def is_action_allowed(self, name):
        # Check delete action permission
        if name == 'delete' and not self.can_delete:
            return False

        return super(ModelView, self).is_action_allowed(name)

    @action('delete', lazy_gettext('Delete'),
            lazy_gettext('Are you sure you want to delete selected records?'))
    def action_delete(self, ids):
        try:
            query = tools.get_query_for_ids(self.get_query(), self.model, ids)

            if self.fast_mass_delete:
                count = query.delete(synchronize_session=False)
            else:
                count = 0

                for m in query.all():
                    if self.delete_model(m):
                        count += 1

            self.session.commit()

            flash(
                ngettext('Record was successfully deleted.',
                         '%(count)s records were successfully deleted.',
                         count,
                         count=count), 'success')
        except Exception as ex:
            if not self.handle_view_exception(ex):
                raise

            flash(
                gettext('Failed to delete records. %(error)s', error=str(ex)),
                'error')
Esempio n. 27
0
class BaseFileAdmin(BaseView, ActionsMixin):

    can_upload = True
    """
        Is file upload allowed.
    """

    can_download = True
    """
        Is file download allowed.
    """

    can_delete = True
    """
        Is file deletion allowed.
    """

    can_delete_dirs = True
    """
        Is recursive directory deletion is allowed.
    """

    can_mkdir = True
    """
        Is directory creation allowed.
    """

    can_rename = True
    """
        Is file and directory renaming allowed.
    """

    allowed_extensions = None
    """
        List of allowed extensions for uploads, in lower case.

        Example::

            class MyAdmin(FileAdmin):
                allowed_extensions = ('swf', 'jpg', 'gif', 'png')
    """

    editable_extensions = tuple()
    """
        List of editable extensions, in lower case.

        Example::

            class MyAdmin(FileAdmin):
                editable_extensions = ('md', 'html', 'txt')
    """

    list_template = 'admin/file/list.html'
    """
        File list template
    """

    upload_template = 'admin/file/form.html'
    """
        File upload template
    """

    upload_modal_template = 'admin/file/modals/form.html'
    """
        File upload template for modal dialog
    """

    mkdir_template = 'admin/file/form.html'
    """
        Directory creation (mkdir) template
    """

    mkdir_modal_template = 'admin/file/modals/form.html'
    """
        Directory creation (mkdir) template for modal dialog
    """

    rename_template = 'admin/file/form.html'
    """
        Rename template
    """

    rename_modal_template = 'admin/file/modals/form.html'
    """
        Rename template for modal dialog
    """

    edit_template = 'admin/file/form.html'
    """
        Edit template
    """

    edit_modal_template = 'admin/file/modals/form.html'
    """
        Edit template for modal dialog
    """

    form_base_class = form.BaseForm
    """
        Base form class. Will be used to create the upload, rename, edit, and delete form.

        Allows enabling CSRF validation and useful if you want to have custom
        constructor or override some fields.

        Example::

            class MyBaseForm(Form):
                def do_something(self):
                    pass

            class MyAdmin(FileAdmin):
                form_base_class = MyBaseForm

    """

    # Modals
    rename_modal = False
    """Setting this to true will display the rename view as a modal dialog."""

    upload_modal = False
    """Setting this to true will display the upload view as a modal dialog."""

    mkdir_modal = False
    """Setting this to true will display the mkdir view as a modal dialog."""

    edit_modal = False
    """Setting this to true will display the edit view as a modal dialog."""

    # List view
    possible_columns = 'name', 'rel_path', 'is_dir', 'size', 'date'
    """A list of possible columns to display."""

    column_list = 'name', 'size', 'date'
    """A list of columns to display."""

    column_sortable_list = column_list
    """A list of sortable columns."""

    default_sort_column = None
    """The default sort column."""

    default_desc = 0
    """The default desc value."""

    column_labels = dict((column, column.capitalize()) for column in column_list)
    """A dict from column names to their labels."""

    date_format = '%Y-%m-%d %H:%M:%S'
    """Date column display format."""

    def __init__(self, base_url=None, name=None, category=None, endpoint=None,
                 url=None, verify_path=True, menu_class_name=None,
                 menu_icon_type=None, menu_icon_value=None, storage=None):
        """
            Constructor.

            :param base_url:
                Base URL for the files
            :param name:
                Name of this view. If not provided, will default to the class name.
            :param category:
                View category
            :param endpoint:
                Endpoint name for the view
            :param url:
                URL for view
            :param verify_path:
                Verify if path exists. If set to `True` and path does not exist
                will raise an exception.
            :param storage:
                The storage backend that the `BaseFileAdmin` will use to operate on the files.
        """
        self.base_url = base_url
        self.storage = storage

        self.init_actions()

        self._on_windows = platform.system() == 'Windows'

        # Convert allowed_extensions to set for quick validation
        if (self.allowed_extensions and
                not isinstance(self.allowed_extensions, set)):
            self.allowed_extensions = set(self.allowed_extensions)

        # Convert editable_extensions to set for quick validation
        if (self.editable_extensions and
                not isinstance(self.editable_extensions, set)):
            self.editable_extensions = set(self.editable_extensions)

        super(BaseFileAdmin, self).__init__(name, category, endpoint, url,
                                            menu_class_name=menu_class_name,
                                            menu_icon_type=menu_icon_type,
                                            menu_icon_value=menu_icon_value)

    def is_accessible_path(self, path):
        """
            Verify if the provided path is accessible for the current user.

            Override to customize behavior.

            :param path:
                Relative path to the root
        """
        return True

    def get_base_path(self):
        """
            Return base path. Override to customize behavior (per-user
            directories, etc)
        """
        return self.storage.get_base_path()

    def get_base_url(self):
        """
            Return base URL. Override to customize behavior (per-user
            directories, etc)
        """
        return self.base_url

    def get_upload_form(self):
        """
            Upload form class for file upload view.

            Override to implement customized behavior.
        """
        class UploadForm(self.form_base_class):
            """
                File upload form. Works with FileAdmin instance to check if it
                is allowed to upload file with given extension.
            """
            upload = fields.FileField(lazy_gettext('File to upload'))

            def __init__(self, *args, **kwargs):
                super(UploadForm, self).__init__(*args, **kwargs)
                self.admin = kwargs['admin']

            def validate_upload(self, field):
                if not self.upload.data:
                    raise validators.ValidationError(gettext('File required.'))

                filename = self.upload.data.filename

                if not self.admin.is_file_allowed(filename):
                    raise validators.ValidationError(gettext('Invalid file type.'))

        return UploadForm

    def get_edit_form(self):
        """
            Create form class for file editing view.

            Override to implement customized behavior.
        """
        class EditForm(self.form_base_class):
            content = fields.TextAreaField(lazy_gettext('Content'),
                                           (validators.required(),))

        return EditForm

    def get_name_form(self):
        """
            Create form class for renaming and mkdir views.

            Override to implement customized behavior.
        """
        def validate_name(self, field):
            regexp = re.compile(r'^(?!^(PRN|AUX|CLOCK\$|NUL|CON|COM\d|LPT\d|\..*)(\..+)?$)[^\x00-\x1f\\?*:\";|/]+$')
            if not regexp.match(field.data):
                raise validators.ValidationError(gettext('Invalid name'))

        class NameForm(self.form_base_class):
            """
                Form with a filename input field.

                Validates if provided name is valid for *nix and Windows systems.
            """
            name = fields.StringField(lazy_gettext('Name'),
                                      validators=[validators.Required(),
                                                  validate_name])
            path = fields.HiddenField()

        return NameForm

    def get_delete_form(self):
        """
            Create form class for model delete view.

            Override to implement customized behavior.
        """
        class DeleteForm(self.form_base_class):
            path = fields.HiddenField(validators=[validators.Required()])

        return DeleteForm

    def get_action_form(self):
        """
            Create form class for model action.

            Override to implement customized behavior.
        """
        class ActionForm(self.form_base_class):
            action = fields.HiddenField()
            url = fields.HiddenField()
            # rowid is retrieved using getlist, for backward compatibility

        return ActionForm

    def upload_form(self):
        """
            Instantiate file upload form and return it.

            Override to implement custom behavior.
        """
        upload_form_class = self.get_upload_form()
        if request.form:
            # Workaround for allowing both CSRF token + FileField to be submitted
            # https://bitbucket.org/danjac/flask-wtf/issue/12/fieldlist-filefield-does-not-follow
            formdata = request.form.copy()  # as request.form is immutable
            formdata.update(request.files)

            # admin=self allows the form to use self.is_file_allowed
            return upload_form_class(formdata, admin=self)
        elif request.files:
            return upload_form_class(request.files, admin=self)
        else:
            return upload_form_class(admin=self)

    def name_form(self):
        """
            Instantiate form used in rename and mkdir then return it.

            Override to implement custom behavior.
        """
        name_form_class = self.get_name_form()
        if request.form:
            return name_form_class(request.form)
        elif request.args:
            return name_form_class(request.args)
        else:
            return name_form_class()

    def edit_form(self):
        """
            Instantiate file editing form and return it.

            Override to implement custom behavior.
        """
        edit_form_class = self.get_edit_form()
        if request.form:
            return edit_form_class(request.form)
        else:
            return edit_form_class()

    def delete_form(self):
        """
            Instantiate file delete form and return it.

            Override to implement custom behavior.
        """
        delete_form_class = self.get_delete_form()
        if request.form:
            return delete_form_class(request.form)
        else:
            return delete_form_class()

    def action_form(self):
        """
            Instantiate action form and return it.

            Override to implement custom behavior.
        """
        action_form_class = self.get_action_form()
        if request.form:
            return action_form_class(request.form)
        else:
            return action_form_class()

    def is_file_allowed(self, filename):
        """
            Verify if file can be uploaded.

            Override to customize behavior.

            :param filename:
                Source file name
        """
        ext = op.splitext(filename)[1].lower()

        if ext.startswith('.'):
            ext = ext[1:]

        if self.allowed_extensions and ext not in self.allowed_extensions:
            return False

        return True

    def is_file_editable(self, filename):
        """
            Determine if the file can be edited.

            Override to customize behavior.

            :param filename:
                Source file name
        """
        ext = op.splitext(filename)[1].lower()

        if ext.startswith('.'):
            ext = ext[1:]

        if not self.editable_extensions or ext not in self.editable_extensions:
            return False

        return True

    def is_in_folder(self, base_path, directory):
        """
            Verify that `directory` is in `base_path` folder

            :param base_path:
                Base directory path
            :param directory:
                Directory path to check
        """
        return op.normpath(directory).startswith(base_path)

    def save_file(self, path, file_data):
        """
            Save uploaded file to the storage

            :param path:
                Path to save to
            :param file_data:
                Werkzeug `FileStorage` object
        """
        self.storage.save_file(path, file_data)

    def validate_form(self, form):
        """
            Validate the form on submit.

            :param form:
                Form to validate
        """
        return helpers.validate_form_on_submit(form)

    def _get_dir_url(self, endpoint, path=None, **kwargs):
        """
            Return prettified URL

            :param endpoint:
                Endpoint name
            :param path:
                Directory path
            :param kwargs:
                Additional arguments
        """
        if not path:
            return self.get_url(endpoint, **kwargs)
        else:
            if self._on_windows:
                path = path.replace('\\', '/')

            kwargs['path'] = path

            return self.get_url(endpoint, **kwargs)

    def _get_file_url(self, path, **kwargs):
        """
            Return static file url

            :param path:
                Static file path
        """
        if self.is_file_editable(path):
            route = '.edit'
        else:
            route = '.download'

        return self.get_url(route, path=path, **kwargs)

    def _normalize_path(self, path):
        """
            Verify and normalize path.

            If the path is not relative to the base directory, will raise a 404 exception.

            If the path does not exist, this will also raise a 404 exception.
        """
        base_path = self.get_base_path()
        if path is None:
            directory = base_path
            path = ''
        else:
            path = op.normpath(path)
            if base_path:
                directory = self._separator.join([base_path, path])
            else:
                directory = path

            directory = op.normpath(directory)

            if not self.is_in_folder(base_path, directory):
                abort(404)

        if not self.storage.path_exists(directory):
            abort(404)

        return base_path, directory, path

    def is_action_allowed(self, name):
        if name == 'delete' and not self.can_delete:
            return False
        elif name == 'edit' and len(self.editable_extensions) == 0:
            return False

        return True

    def on_rename(self, full_path, dir_base, filename):
        """
            Perform some actions after a file or directory has been renamed.

            Called from rename method

            By default do nothing.
        """
        pass

    def on_edit_file(self, full_path, path):
        """
            Perform some actions after a file has been successfully changed.

            Called from edit method

            By default do nothing.
        """
        pass

    def on_file_upload(self, directory, path, filename):
        """
            Perform some actions after a file has been successfully uploaded.

            Called from upload method

            By default do nothing.
        """
        pass

    def on_mkdir(self, parent_dir, dir_name):
        """
            Perform some actions after a directory has successfully been created.

            Called from mkdir method

            By default do nothing.
        """
        pass

    def before_directory_delete(self, full_path, dir_name):
        """
            Perform some actions before a directory has successfully been deleted.

            Called from delete method

            By default do nothing.
        """
        pass

    def before_file_delete(self, full_path, filename):
        """
            Perform some actions before a file has successfully been deleted.

            Called from delete method

            By default do nothing.
        """
        pass

    def on_directory_delete(self, full_path, dir_name):
        """
            Perform some actions after a directory has successfully been deleted.

            Called from delete method

            By default do nothing.
        """
        pass

    def on_file_delete(self, full_path, filename):
        """
            Perform some actions after a file has successfully been deleted.

            Called from delete method

            By default do nothing.
        """
        pass

    def is_column_visible(self, column):
        """
        Determines if the given column is visible.
        :param column: The column to query.
        :return: Whether the column is visible.
        """
        return column in self.column_list

    def is_column_sortable(self, column):
        """
        Determines if the given column is sortable.
        :param column: The column to query.
        :return: Whether the column is sortable.
        """
        return column in self.column_sortable_list

    def column_label(self, column):
        """
        Gets the column's label.
        :param column: The column to query.
        :return: The column's label.
        """
        return self.column_labels[column]

    def timestamp_format(self, timestamp):
        """
        Formats the timestamp to a date format.
        :param timestamp: The timestamp to format.
        :return: A formatted date.
        """
        return datetime.fromtimestamp(timestamp).strftime(self.date_format)

    def _save_form_files(self, directory, path, form):
        filename = self._separator.join([directory, secure_filename(form.upload.data.filename)])

        if self.storage.path_exists(filename):
            secure_name = self._separator.join([path, secure_filename(form.upload.data.filename)])
            raise Exception(gettext('File "%(name)s" already exists.',
                                    name=secure_name))
        else:
            self.save_file(filename, form.upload.data)
            self.on_file_upload(directory, path, filename)

    @property
    def _separator(self):
        return self.storage.separator

    def _get_breadcrumbs(self, path):
        """
            Returns a list of tuples with each tuple containing the folder and
            the tree up to that folder when traversing down the `path`
        """
        accumulator = []
        breadcrumbs = []
        for n in path.split(self._separator):
            accumulator.append(n)
            breadcrumbs.append((n, self._separator.join(accumulator)))
        return breadcrumbs

    @expose('/old_index')
    @expose('/old_b/<path:path>')
    def index(self, path=None):
        warnings.warn('deprecated: use index_view instead.', DeprecationWarning)
        return redirect(self.get_url('.index_view', path=path))

    @expose('/')
    @expose('/b/<path:path>')
    def index_view(self, path=None):
        """
            Index view method

            :param path:
                Optional directory path. If not provided, will use the base directory
        """
        if self.can_delete:
            delete_form = self.delete_form()
        else:
            delete_form = None

        # Get path and verify if it is valid
        base_path, directory, path = self._normalize_path(path)
        if not self.is_accessible_path(path):
            flash(gettext('Permission denied.'), 'error')
            return redirect(self._get_dir_url('.index_view'))

        # Get directory listing
        items = []

        # Parent directory
        if directory != base_path:
            parent_path = op.normpath(self._separator.join([path, '..']))
            if parent_path == '.':
                parent_path = None

            items.append(('..', parent_path, True, 0, 0))

        for item in self.storage.get_files(path, directory):
            file_name, rel_path, is_dir, size, last_modified = item
            if self.is_accessible_path(rel_path):
                items.append(item)

        sort_column = request.args.get('sort', None, type=str)
        sort_desc = request.args.get('desc', 0, type=int)

        if sort_column is None:
            # Sort by name
            items.sort(key=itemgetter(0))
            # Sort by type
            items.sort(key=itemgetter(2), reverse=True)
            # Sort by modified date
            items.sort(key=lambda x: (x[0], x[1], x[2], x[3], datetime.fromtimestamp(x[4])), reverse=True)
        else:
            column_index = self.possible_columns.index(sort_column)
            items.sort(key=itemgetter(column_index), reverse=sort_desc)

        # Generate breadcrumbs
        breadcrumbs = self._get_breadcrumbs(path)

        # Actions
        actions, actions_confirmation = self.get_actions_list()
        if actions:
            action_form = self.action_form()
        else:
            action_form = None

        def sort_url(column, invert=False):
            desc = None

            if invert and not sort_desc:
                desc = 1

            return self.get_url('.index_view', sort=column, desc=desc)

        return self.render(self.list_template,
                           dir_path=path,
                           breadcrumbs=breadcrumbs,
                           get_dir_url=self._get_dir_url,
                           get_file_url=self._get_file_url,
                           items=items,
                           actions=actions,
                           actions_confirmation=actions_confirmation,
                           action_form=action_form,
                           delete_form=delete_form,
                           sort_column=sort_column,
                           sort_desc=sort_desc,
                           sort_url=sort_url,
                           timestamp_format=self.timestamp_format)

    @expose('/upload/', methods=('GET', 'POST'))
    @expose('/upload/<path:path>', methods=('GET', 'POST'))
    def upload(self, path=None):
        """
            Upload view method

            :param path:
                Optional directory path. If not provided, will use the base directory
        """
        # Get path and verify if it is valid
        base_path, directory, path = self._normalize_path(path)

        if not self.can_upload:
            flash(gettext('File uploading is disabled.'), 'error')
            return redirect(self._get_dir_url('.index_view', path))

        if not self.is_accessible_path(path):
            flash(gettext('Permission denied.'), 'error')
            return redirect(self._get_dir_url('.index_view'))

        form = self.upload_form()
        if self.validate_form(form):
            try:
                self._save_form_files(directory, path, form)
                flash(gettext('Successfully saved file: %(name)s',
                              name=form.upload.data.filename), 'success')
                return redirect(self._get_dir_url('.index_view', path))
            except Exception as ex:
                flash(gettext('Failed to save file: %(error)s', error=ex), 'error')

        if self.upload_modal and request.args.get('modal'):
            template = self.upload_modal_template
        else:
            template = self.upload_template

        return self.render(template, form=form,
                           header_text=gettext('Upload File'),
                           modal=request.args.get('modal'))

    @expose('/download/<path:path>')
    def download(self, path=None):
        """
            Download view method.

            :param path:
                File path.
        """
        if not self.can_download:
            abort(404)

        base_path, directory, path = self._normalize_path(path)

        # backward compatibility with base_url
        base_url = self.get_base_url()
        if base_url:
            base_url = urljoin(self.get_url('.index_view'), base_url)
            return redirect(urljoin(base_url, path))

        return self.storage.send_file(directory)

    @expose('/mkdir/', methods=('GET', 'POST'))
    @expose('/mkdir/<path:path>', methods=('GET', 'POST'))
    def mkdir(self, path=None):
        """
            Directory creation view method

            :param path:
                Optional directory path. If not provided, will use the base directory
        """
        # Get path and verify if it is valid
        base_path, directory, path = self._normalize_path(path)

        dir_url = self._get_dir_url('.index_view', path)

        if not self.can_mkdir:
            flash(gettext('Directory creation is disabled.'), 'error')
            return redirect(dir_url)

        if not self.is_accessible_path(path):
            flash(gettext('Permission denied.'), 'error')
            return redirect(self._get_dir_url('.index_view'))

        form = self.name_form()

        if self.validate_form(form):
            try:
                self.storage.make_dir(directory, form.name.data)
                self.on_mkdir(directory, form.name.data)
                flash(gettext('Successfully created directory: %(directory)s',
                              directory=form.name.data), 'success')
                return redirect(dir_url)
            except Exception as ex:
                flash(gettext('Failed to create directory: %(error)s', error=ex), 'error')
        else:
            helpers.flash_errors(form, message='Failed to create directory: %(error)s')

        if self.mkdir_modal and request.args.get('modal'):
            template = self.mkdir_modal_template
        else:
            template = self.mkdir_template

        return self.render(template, form=form, dir_url=dir_url,
                           header_text=gettext('Create Directory'))

    def delete_file(self, file_path):
        """
            Deletes the file located at `file_path`
        """
        self.storage.delete_file(file_path)

    @expose('/delete/', methods=('POST',))
    def delete(self):
        """
            Delete view method
        """
        form = self.delete_form()

        path = form.path.data
        if path:
            return_url = self._get_dir_url('.index_view', op.dirname(path))
        else:
            return_url = self.get_url('.index_view')

        if self.validate_form(form):
            # Get path and verify if it is valid
            base_path, full_path, path = self._normalize_path(path)

            if not self.can_delete:
                flash(gettext('Deletion is disabled.'), 'error')
                return redirect(return_url)

            if not self.is_accessible_path(path):
                flash(gettext('Permission denied.'), 'error')
                return redirect(self._get_dir_url('.index_view'))

            if self.storage.is_dir(full_path):
                if not self.can_delete_dirs:
                    flash(gettext('Directory deletion is disabled.'), 'error')
                    return redirect(return_url)
                try:
                    self.before_directory_delete(full_path, path)
                    self.storage.delete_tree(full_path)
                    self.on_directory_delete(full_path, path)
                    flash(gettext('Directory "%(path)s" was successfully deleted.', path=path), 'success')
                except Exception as ex:
                    flash(gettext('Failed to delete directory: %(error)s', error=ex), 'error')
            else:
                try:
                    self.before_file_delete(full_path, path)
                    self.delete_file(full_path)
                    self.on_file_delete(full_path, path)
                    flash(gettext('File "%(name)s" was successfully deleted.', name=path), 'success')
                except Exception as ex:
                    flash(gettext('Failed to delete file: %(name)s', name=ex), 'error')
        else:
            helpers.flash_errors(form, message='Failed to delete file. %(error)s')

        return redirect(return_url)

    @expose('/rename/', methods=('GET', 'POST'))
    def rename(self):
        """
            Rename view method
        """
        form = self.name_form()

        path = form.path.data
        if path:
            base_path, full_path, path = self._normalize_path(path)

            return_url = self._get_dir_url('.index_view', op.dirname(path))
        else:
            return redirect(self.get_url('.index_view'))

        if not self.can_rename:
            flash(gettext('Renaming is disabled.'), 'error')
            return redirect(return_url)

        if not self.is_accessible_path(path):
            flash(gettext('Permission denied.'), 'error')
            return redirect(self._get_dir_url('.index_view'))

        if not self.storage.path_exists(full_path):
            flash(gettext('Path does not exist.'), 'error')
            return redirect(return_url)

        if self.validate_form(form):
            try:
                dir_base = op.dirname(full_path)
                filename = secure_filename(form.name.data)
                self.storage.rename_path(full_path, self._separator.join([dir_base, filename]))
                self.on_rename(full_path, dir_base, filename)
                flash(gettext('Successfully renamed "%(src)s" to "%(dst)s"',
                              src=op.basename(path),
                              dst=filename), 'success')
            except Exception as ex:
                flash(gettext('Failed to rename: %(error)s', error=ex), 'error')

            return redirect(return_url)
        else:
            helpers.flash_errors(form, message='Failed to rename: %(error)s')

        if self.rename_modal and request.args.get('modal'):
            template = self.rename_modal_template
        else:
            template = self.rename_template

        return self.render(template, form=form, path=op.dirname(path),
                           name=op.basename(path), dir_url=return_url,
                           header_text=gettext('Rename %(name)s',
                                               name=op.basename(path)))

    @expose('/edit/', methods=('GET', 'POST'))
    def edit(self):
        """
            Edit view method
        """
        next_url = None

        path = request.args.getlist('path')
        if not path:
            return redirect(self.get_url('.index_view'))

        if len(path) > 1:
            next_url = self.get_url('.edit', path=path[1:])

        path = path[0]

        base_path, full_path, path = self._normalize_path(path)

        if not self.is_accessible_path(path) or not self.is_file_editable(path):
            flash(gettext('Permission denied.'), 'error')
            return redirect(self._get_dir_url('.index_view'))

        dir_url = self._get_dir_url('.index_view', op.dirname(path))
        next_url = next_url or dir_url

        form = self.edit_form()
        error = False

        if self.validate_form(form):
            form.process(request.form, content='')
            if form.validate():
                try:
                    with open(full_path, 'w') as f:
                        f.write(request.form['content'])
                except IOError:
                    flash(gettext("Error saving changes to %(name)s.", name=path), 'error')
                    error = True
                else:
                    self.on_edit_file(full_path, path)
                    flash(gettext("Changes to %(name)s saved successfully.", name=path), 'success')
                    return redirect(next_url)
        else:
            helpers.flash_errors(form, message='Failed to edit file. %(error)s')

            try:
                with open(full_path, 'rb') as f:
                    content = f.read()
            except IOError:
                flash(gettext("Error reading %(name)s.", name=path), 'error')
                error = True
            except:
                flash(gettext("Unexpected error while reading from %(name)s", name=path), 'error')
                error = True
            else:
                try:
                    content = content.decode('utf8')
                except UnicodeDecodeError:
                    flash(gettext("Cannot edit %(name)s.", name=path), 'error')
                    error = True
                except:
                    flash(gettext("Unexpected error while reading from %(name)s", name=path), 'error')
                    error = True
                else:
                    form.content.data = content

            if error:
                return redirect(next_url)

        if self.edit_modal and request.args.get('modal'):
            template = self.edit_modal_template
        else:
            template = self.edit_template

        return self.render(template, dir_url=dir_url, path=path,
                           form=form, error=error,
                           header_text=gettext('Editing %(path)s', path=path))

    @expose('/action/', methods=('POST',))
    def action_view(self):
        return self.handle_action()

    # Actions
    @action('delete',
            lazy_gettext('Delete'),
            lazy_gettext('Are you sure you want to delete these files?'))
    def action_delete(self, items):
        if not self.can_delete:
            flash(gettext('File deletion is disabled.'), 'error')
            return

        for path in items:
            base_path, full_path, path = self._normalize_path(path)

            if self.is_accessible_path(path):
                try:
                    self.delete_file(full_path)
                    flash(gettext('File "%(name)s" was successfully deleted.', name=path), 'success')
                except Exception as ex:
                    flash(gettext('Failed to delete file: %(name)s', name=ex), 'error')

    @action('edit', lazy_gettext('Edit'))
    def action_edit(self, items):
        return redirect(self.get_url('.edit', path=items))
Esempio n. 28
0
 class EditForm(self.form_base_class):
     content = fields.TextAreaField(lazy_gettext('Content'),
                                    (validators.required(),))
Esempio n. 29
0
 def __init__(self, name, options=None, data_type=None):
     super(BaseBooleanFilter, self).__init__(name,
                                             (('1', lazy_gettext(u'Yes')),
                                              ('0', lazy_gettext(u'No'))),
                                             data_type)
Esempio n. 30
0
 def operation(self):
     return lazy_gettext('not in list')
Esempio n. 31
0
 def __init__(self, name, options=None, data_type=None):
     super(BaseBooleanFilter, self).__init__(
         name, (("1", lazy_gettext(u"Yes")), ("0", lazy_gettext(u"No"))), data_type
     )
Esempio n. 32
0
 def operation(self):
     return lazy_gettext('not between')
Esempio n. 33
0
            return True
        except Exception, ex:
            flash(gettext('Failed to delete model. %(error)s', error=str(ex)), 'error')
            logging.exception('Failed to delete model')
            return False

    # Default model actions
    def is_action_allowed(self, name):
        # Check delete action permission
        if name == 'delete' and not self.can_delete:
            return False

        return super(ModelView, self).is_action_allowed(name)

    @action('delete',
            lazy_gettext('Delete'),
            lazy_gettext('Are you sure you want to delete selected models?'))
    def action_delete(self, ids):
        try:
            model_pk = getattr(self.model, self._primary_key)

            if self.fast_mass_delete:
                count = self.model.delete().where(model_pk << ids).execute()
            else:
                count = 0

                query = self.model.select().filter(model_pk << ids)

                for m in query:
                    m.delete_instance(recursive=True)
                    count += 1
Esempio n. 34
0
 def operation(self):
     return lazy_gettext('equals')
Esempio n. 35
0
 def operation(self):
     return lazy_gettext('ObjectId equals')
Esempio n. 36
0
 def operation(self):
     return lazy_gettext('not equal')
Esempio n. 37
0
 def operation(self):
     return lazy_gettext('is')
Esempio n. 38
0
 def operation(self):
     return lazy_gettext('not contains')
Esempio n. 39
0
 def operation(self):
     return lazy_gettext('not between')
Esempio n. 40
0
 def operation(self):
     return lazy_gettext('greater than')
Esempio n. 41
0
 def operation(self):
     return lazy_gettext('not equal')
Esempio n. 42
0
 def operation(self):
     return lazy_gettext('smaller than')
Esempio n. 43
0
 def operation(self):
     return lazy_gettext('greater than')
Esempio n. 44
0
 def operation(self):
     return lazy_gettext('empty')
Esempio n. 45
0
 def operation(self):
     return lazy_gettext('empty')
Esempio n. 46
0
 def operation(self):
     return lazy_gettext(u'大于')