def view_on_site(self, request, obj, fieldname, *args, **kwargs): endpoint = kwargs.pop('endpoint', 'detail') return html.a( href=obj.get_absolute_url(endpoint), target='_blank', )(html.i(class_="icon icon-eye-open", style="margin-right: 5px;")(), lazy_gettext('View on site'))
def clean(self): current_names = [value.name for value in self.values] for name in current_names: if current_names.count(name) > 1: raise Exception(lazy_gettext("%(name)s already exists", name=name)) super(HasCustomValue, self).clean()
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
def __init__(self, name=None, category=None, endpoint=None, url=None): if url is None: url = '/admin' super(AdminIndexView, self).__init__(name or babel.lazy_gettext('Home'), category, endpoint or 'admin', url, 'static')
def validate_long_slug(self): self._create_mpath_long_slug() filters = dict(long_slug=self.long_slug) if self.id: filters["id__ne"] = self.id exist = self.__class__.objects(**filters) if exist.count(): if current_app.config.get("SMART_SLUG_ENABLED", False): self.slug = "{0}-{1}".format(self.slug, random.getrandbits(32)) self._create_mpath_long_slug() else: raise db.ValidationError(lazy_gettext("%(slug)s slug already exists", slug=self.long_slug))
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
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: count = 0 # TODO: Optimize me for pk in ids: self.coll.remove({'_id': ObjectId(pk)}) count += 1 flash(ngettext('Model was successfully deleted.', '%(count)s models were successfully deleted.', count, count=count)) except Exception, ex:
class WigoModelView(BaseModelView): edit_template = 'admin_overrides/edit.html' column_formatters = { 'actions': actions_formatter, 'group': group_formatter, 'user': user_formatter, } form_ajax_refs = { 'group': WigoAjaxModelLoader('group', {'model': Group}), 'owner': WigoAjaxModelLoader('owner', {'model': User}) } 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): super(WigoModelView, self).__init__(model, name, category, endpoint, url, static_folder, menu_class_name, menu_icon_type, menu_icon_value) def _create_ajax_loader(self, name, options): pass def is_accessible(self): return check_basic_auth() def inaccessible_callback(self, name, **kwargs): return authenticate() def scaffold_list_form(self, custom_fieldlist=ListEditableFieldList, validators=None): pass def create_model(self, form): try: instance = self.model() form.populate_obj(instance) self._on_model_change(form, instance, True) instance.save() except Exception as ex: if not self.handle_view_exception(ex): flash(gettext('Failed to create record. %(error)s', error=str(ex)), 'error') return False else: self.after_model_change(form, instance, True) return True def update_model(self, form, model): 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=str(ex)), 'error') 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() 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') return False def scaffold_form(self): class WigoModelForm(Form): pass for field in self.model.fields.values(): validators = [DataRequired() if field.required else Optional()] if field.name == 'id': continue if 'group_id' == field.name: setattr(WigoModelForm, 'group', AjaxSelectField( WigoAjaxModelLoader('group', {'model': Group}), 'group', validators=validators)) elif 'owner_id' == field.name: setattr(WigoModelForm, 'owner', AjaxSelectField( WigoAjaxModelLoader('owner', {'model': User}), 'owner', validators=validators)) elif isinstance(field, DateTimeType): setattr(WigoModelForm, field.name, DateTimeField(field.name, default=field.default, validators=validators)) elif isinstance(field, FloatType): setattr(WigoModelForm, field.name, FloatField(field.name, default=field.default, validators=validators)) elif isinstance(field, NumberType): setattr(WigoModelForm, field.name, IntegerField(field.name, default=field.default, validators=validators)) elif isinstance(field, StringType): if field.choices: setattr(WigoModelForm, field.name, SelectField(field.name, choices=[(val, val) for val in field.choices], default=field.default)) else: setattr(WigoModelForm, field.name, StringField(field.name, default=field.default, validators=validators)) elif isinstance(field, BooleanType): setattr(WigoModelForm, field.name, BooleanField(field.name)) elif isinstance(field, JsonType): setattr(WigoModelForm, field.name, JSONField(field.name, validators=validators)) elif isinstance(field, ListType): setattr(WigoModelForm, field.name, Select2TagsField(field.name, validators=validators, save_as_list=True)) return WigoModelForm def get_list(self, page, sort_field, sort_desc, search, filters): query = self.model.select().limit(self.page_size).page(page + 1) # Filters if self._filters: for flt, flt_name, value in filters: f = self._filters[flt] query = f.apply(query, flt_name, value) count, page, instances = query.execute() return count, instances def get_one(self, id): return self.model.find(id) def _get_field_value(self, model, name): return getattr(model, name) def scaffold_sortable_columns(self): return [] def scaffold_filters(self, name): return [WigoEqualsModelFilter(name)] def get_pk_value(self, model): return model.id def scaffold_list_columns(self): return self.model.fields.keys() def render(self, template, **kwargs): if template == self.edit_template: if self.model == Event: kwargs['event'] = True if self.model == Group: kwargs['group'] = True return super(WigoModelView, self).render(template, **kwargs) @action('delete', lazy_gettext('Delete'), lazy_gettext('Are you sure you want to delete selected records?')) def action_delete(self, ids): for id in ids: self.model.find(id).delete()
def operation(self): return lazy_gettext('between')
def operation(self): return lazy_gettext('greater than')
def operation(self): return lazy_gettext('not in list')
def operation(self): return lazy_gettext('not contains')
src=op.basename(path), dst=filename)) except Exception, ex: flash(gettext('Failed to rename: %(error)s', error=ex), 'error') return redirect(return_url) return self.render(self.rename_template, form=form, path=op.dirname(path), name=op.basename(path), dir_url=return_url) @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')
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
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.ext.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 = 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.ext.admin.consts.ICON_TYPE_GLYPH` - Bootstrap glyph icon - `flask.ext.admin.consts.ICON_TYPE_IMAGE` - Image relative to Flask static directory - `flask.ext.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: # TODO: Skip column and issue a warning raise TypeError('Can not convert multiple-column properties (%s.%s)' % (self.model, p.key)) 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 is_valid_filter(self, filter): """ Verify that the provided filter object is derived from the SQLAlchemy-compatible filter class. :param filter: Filter object to verify. """ return isinstance(filter, filters.BaseSQLAFilter) def handle_filter(self, filter): 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 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=exc.message), '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')
def _action_decorator(): return action( 'delete', lazy_gettext('Delete'), lazy_gettext('Are you sure you want to delete these files?') )
class ModelView(with_metaclass(CoolAdminMeta, _ModelView)): page_size = 50 can_view_details = True details_modal = True edit_modal = True model_form_converter = KModelConverter filter_converter = KFilterConverter() column_type_formatters = _ModelView.column_type_formatters or dict() column_type_formatters[datetime] = type_best column_type_formatters[FileProxy] = type_file show_popover = False robot_filters = False 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.column_formatters = dict(self.column_formatters or dict()) # 初始化标识 self.column_labels = self.column_labels or dict() self.column_labels.setdefault('id', 'ID') for field in model._fields: if field not in self.column_labels: attr = getattr(model, field) if hasattr(attr, 'verbose_name'): verbose_name = attr.verbose_name if verbose_name: self.column_labels[field] = verbose_name #初始化筛选器 types = (IntField, ReferenceField, StringField, BooleanField, DateTimeField) if self.robot_filters else (ReferenceField, ) self.column_filters = list(self.column_filters or []) primary = False for field in model._fields: attr = getattr(model, field) if hasattr(attr, 'primary_key'): if attr.primary_key is True: self.column_filters = [field] + self.column_filters primary = True elif type(attr) in types and attr.name not in self.column_filters: self.column_filters.append(attr.name) if not primary: self.column_filters = ['id'] + self.column_filters if self.robot_filters: self.column_filters = filter_sort(self.column_filters, self.column_list) #初始化类型格式化 for field in model._fields: attr = getattr(model, field) if type(attr) == StringField: self.column_formatters.setdefault(attr.name, formatter_len(40)) self.form_ajax_refs = self.form_ajax_refs or dict() for field in model._fields: attr = getattr(model, field) if type(attr) == ReferenceField: if field not in self.form_ajax_refs and hasattr( attr.document_type, 'ajax_ref'): self.form_ajax_refs[field] = dict( fields=attr.document_type.ajax_ref, page_size=20) if not self.column_default_sort and 'created' in model._fields: self.column_default_sort = ('-created', ) self._init_referenced = False 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) def _refresh_cache(self): self.column_choices = self.column_choices or dict() for field in self.model._fields: choices = getattr(self.model, field).choices if choices: self.column_choices[field] = choices super(ModelView, self)._refresh_cache() def create_model(self, form): try: model = self.model() self.pre_model_change(form, model, True) form.populate_obj(model) self._on_model_change(form, model, True) model.save() except Exception as ex: current_app.logger.error(ex) if not self.handle_view_exception(ex): flash( 'Failed to create record. %(error)s' % dict(error=format_error(ex)), 'error') return False else: self.after_model_change(form, model, True) return True def update_model(self, form, model): try: self.pre_model_change(form, model, False) form.populate_obj(model) self._on_model_change(form, model, False) model.save() except Exception as ex: current_app.logger.error(ex) if not self.handle_view_exception(ex): flash( 'Failed to update record. %(error)s' % dict(error=format_error(ex)), 'error') return False else: self.after_model_change(form, model, False) return True def pre_model_change(self, form, model, created=False): pass def on_model_change(self, form, model, created=False): if created is True and hasattr(model, 'create'): if callable(model.create): model.create() elif hasattr(model, 'modified'): model.modified = datetime.now() # @expose('/') # def index_view(self): # res = super(ModelView, self).index_view() # gc.collect() # return res def get_ref_type(self, attr): document, ref_type = attr.document_type, None if hasattr(document, 'id'): xattr = document._fields.get('id') if isinstance(xattr, IntField) or isinstance(xattr, LongField): ref_type = int elif isinstance(xattr, DecimalField) or isinstance( xattr, FloatField): ref_type = float elif isinstance(xattr, ObjectIdField): ref_type = ObjectId return ref_type def scaffold_filters(self, name): 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__ if isinstance(attr, ReferenceField): ref_type = self.get_ref_type(attr) flt = self.filter_converter.convert(type_name, attr, visible_name, ref_type) elif isinstance(attr, ListField) and isinstance( attr.field, ReferenceField): ref_type = self.get_ref_type(attr.field) flt = self.filter_converter.convert(type_name, attr, visible_name, ref_type) elif isinstance(attr, ObjectIdField): flt = self.filter_converter.convert(type_name, attr, visible_name, ObjectId) else: flt = self.filter_converter.convert(type_name, attr, visible_name) return flt def get_list(self, page, sort_column, sort_desc, search, filters, execute=True, page_size=None): query = self.get_query() if self._filters: for flt, flt_name, value in filters: f = self._filters[flt] query = f.apply(query, f.clean(value)) if self._search_supported and search: query = self._search(query, search) count = query.count() if not self.simple_list_pager else None if sort_column: query = query.order_by('%s%s' % ('-' if sort_desc else '', sort_column)) else: order = self._get_default_order() if order: if len(order) <= 1 or order[1] is not True and order[ 1] is not False: query = query.order_by(*order) else: query = query.order_by('%s%s' % ('-' if order[1] else '', order[0])) 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_filter_tpl(self, attr): for view in self.admin._views: if hasattr( view, 'model' ) and attr.document_type == view.model and view._filter_args: for idx, flt in view._filter_args.itervalues(): if type(flt) == ObjectIdEqualFilter: return ('/admin/%s/?flt0_' % view.model.__name__.lower()) + str(idx) + '=%s' if flt.column.primary_key: cls = type(flt).__name__ if 'EqualFilter' in cls and 'Not' not in cls: return ( '/admin/%s/?flt0_' % view.model.__name__.lower()) + str(idx) + '=%s' def set_filter_formatter(self, attr): def formatter(tpl, name): return lambda m: (getattr(m, name), tpl % str( getattr(m, name).id if getattr(m, name) else '')) tpl = self.get_filter_tpl(attr) if tpl: f = formatter_link(formatter(tpl, attr.name)) self.column_formatters.setdefault(attr.name, f) def init_referenced(self): #初始化类型格式化 for field in self.model._fields: attr = getattr(self.model, field) if type(attr) == ReferenceField: self.set_filter_formatter(attr) @contextfunction def get_list_value(self, context, model, name): if not self._init_referenced: self._init_referenced = True self.init_referenced() column_fmt = self.column_formatters.get(name) if column_fmt is not None: try: value = column_fmt(self, context, model, name) except: current_app.logger.error(traceback.format_exc()) value = '该对象被删了' else: value = self._get_field_value(model, name) #获取choice choices_map = self._column_choices_map.get(name, {}) if choices_map: return type_select(self, value, model, name, choices_map) or value if isinstance(value, bool): return type_bool(self, value, model, name) if value and isinstance(value, list) and isinstance( value[0], ImageProxy): self.show_popover = True return type_images(self, value) type_fmt = None for typeobj, formatter in self.column_type_formatters.items(): if isinstance(value, typeobj): type_fmt = formatter break if type_fmt is not None: try: value = type_fmt(self, value) except: current_app.logger.error(traceback.format_exc()) value = '该对象被删了' return value @action('delete', lazy_gettext('Delete'), lazy_gettext('Are you sure you want to delete selected records?')) def action_delete(self, ids): try: count = 0 id = self.model._meta['id_field'] if id in self.model._fields: if isinstance(self.model._fields[id], IntField): all_ids = [int(pk) for pk in ids] elif isinstance(self.model._fields[id], StringField): all_ids = ids else: all_ids = [self.object_id_converter(pk) for pk in ids] else: 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)) except Exception as ex: if not self.handle_view_exception(ex): flash( gettext('Failed to delete records. %(error)s', error=str(ex)), 'error') def on_field_change(self, model, name, value): model[name] = value if hasattr(model, 'modified'): model['modified'] = datetime.now() @expose('/dropdown') def dropdown(self): id = request.args.get('id', 0, unicode) val = request.args.get('key', '') name = request.args.get('name', '', unicode) value = request.args.get('value', '', unicode) model = self.model if not val: val = False if value == 'False' else True if type(val) == int: val = int(val) obj = model.objects(id=id).first() if obj: self.on_field_change(obj, name, val) obj.save() return json_success() return json_error(msg='该记录不存在') def get_field_type(self, field): if hasattr(self.model, field): return type(getattr(self.model, field)).__name__ return 'LabelField' def _create_ajax_loader(self, name, opts): return create_ajax_loader(self.model, name, name, opts)
class ModelView(BaseModelView): """ SQLAlchemy model view Usage sample:: admin = Admin() admin.add_view(ModelView(User, db.session)) """ column_hide_backrefs = ObsoleteAttr('column_hide_backrefs', 'hide_backrefs', True) """ Set this to False if you want to see multiselect for model backrefs. """ 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. Only text-based columns are searchable (`String`, `Unicode`, `Text`, `UnicodeText`). 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.ext.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.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 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'), ] """ def __init__(self, model, session, name=None, category=None, endpoint=None, url=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 """ self.session = session self._search_fields = None self._search_joins = dict() self._filter_joins = dict() if self.form_choices is None: self.form_choices = {} super(ModelView, self).__init__(model, name, category, endpoint, url) # 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 # Scaffolding def scaffold_pk(self): """ Return the primary key name from a model PK can be a single value or a tuple if multiple PKs exist """ return tools.get_primary_key(self.model) def get_pk_value(self, model): """ Return the PK value from a model object. PK can be a single value or a tuple if multiple PKs exist """ try: return getattr(model, self._primary_key) except TypeError: v = [] for attr in self._primary_key: v.append(getattr(model, attr)) return tuple(v) def scaffold_list_columns(self): """ Return a list of columns from the model. """ columns = [] for p in self._get_model_iterator(): # Verify type if hasattr(p, 'direction'): if self.column_display_all_relations or p.direction.name == 'MANYTOONE': columns.append(p.key) elif hasattr(p, 'columns'): column_inherited_primary_key = False if len(p.columns) != 1: if is_inherited_primary_key(p): column = get_column_for_current_model(p) else: raise TypeError('Can not convert multiple-column properties (%s.%s)' % (model, p.key)) else: # Grab column column = p.columns[0] # An inherited primary key has a foreign key as well if column.foreign_keys and not is_inherited_primary_key(p): 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_columns_for_field(self, field): if isinstance(field, string_types): attr = getattr(self.model, field, None) if field is None: raise Exception('Field %s was not found.' % field) else: attr = field if (not attr or not hasattr(attr, 'property') or not hasattr(attr.property, 'columns') or not attr.property.columns): raise Exception('Invalid field %s: does not contains any columns.' % field) return attr.property.columns def _need_join(self, table): return table not in self.model._sa_class_manager.mapper.tables 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 = dict() for p in self.column_searchable_list: for column in self._get_columns_for_field(p): column_type = type(column.type).__name__ if not self.is_text_column_type(column_type): raise Exception('Can only search on text columns. ' + 'Failed to setup search for "%s"' % p) self._search_fields.append(column) # If it belongs to different table - add a join if self._need_join(column.table): self._search_joins[column.table.name] = column.table return bool(self.column_searchable_list) def is_text_column_type(self, name): """ Verify if the provided column type is text-based. :returns: ``True`` for ``String``, ``Unicode``, ``Text``, ``UnicodeText`` """ return name in ('String', 'Unicode', 'Text', 'UnicodeText') def scaffold_filters(self, name): """ Return list of enabled filters """ 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 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.name): self._filter_joins[table.name] = [table.name] 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 is_valid_filter(self, filter): """ Verify that the provided filter object is derived from the SQLAlchemy-compatible filter class. :param filter: Filter object to verify. """ return isinstance(filter, filters.BaseSQLAFilter) 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_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, fields): attr = getattr(self.model, name, None) if attr is None: raise ValueError('Model %s does not have field %s.' % (self.model, name)) if not hasattr(attr, 'property') or not hasattr(attr.property, 'direction'): raise ValueError('%s.%s is not a relation.' % (self.model, name)) remote_model = attr.prop.mapper.class_ remote_fields = [] for field in fields: if isinstance(field, string_types): attr = getattr(remote_model, field, None) if not attr: raise ValueError('%s.%s does not exist.' % (remote_model, field)) remote_fields.append(attr) else: # TODO: Figure out if it is valid SQLAlchemy property? remote_fields.append(field) return QueryAjaxModelLoader(name, self.session, remote_model, remote_fields) # 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. """ return self.session.query(self.model) def get_count_query(self): """ Return a the count query for the model type """ return self.session.query(func.count('*')).select_from(self.model) def _order_by(self, query, 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 # Try to handle it as a string if isinstance(sort_field, string_types): # Create automatic join against a table if column name # contains dot. if '.' in sort_field: parts = sort_field.split('.', 1) if parts[0] not in joins: query = query.join(parts[0]) joins.add(parts[0]) elif isinstance(sort_field, InstrumentedAttribute): # SQLAlchemy 0.8+ uses 'parent' as a name mapper = getattr(sort_field, 'parent', None) if mapper is None: # SQLAlchemy 0.7.x uses parententity mapper = getattr(sort_field, 'parententity', None) if mapper is not None: table = mapper.tables[0] if self._need_join(table) and table.name not in joins: query = query.join(table) joins.add(table.name) elif isinstance(sort_field, Column): pass else: raise TypeError('Wrong argument type') 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 if isinstance(field, string_types): field = getattr(self.model, field) return field, 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() # Apply search criteria if self._search_supported and search: # Apply search-related joins if self._search_joins: for jn in self._search_joins.values(): query = query.join(jn) count_query = count_query.join(jn) joins = set(self._search_joins.keys()) # 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, value in filters: flt = self._filters[idx] # Figure out joins 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) # Apply filter query = flt.apply(query, value) count_query = flt.apply(count_query, 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] 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 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(id) # 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 self._debug: raise flash(gettext('Failed to create model. %(error)s', error=str(ex)), 'error') logging.exception('Failed to create model') 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 self._debug: raise flash(gettext('Failed to update model. %(error)s', error=str(ex)), 'error') logging.exception('Failed to update model') 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 self._debug: raise flash(gettext('Failed to delete model. %(error)s', error=str(ex)), 'error') logging.exception('Failed to delete model') 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 models?')) 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(): self.session.delete(m) count += 1 self.session.commit() flash(ngettext('Model was successfully deleted.', '%(count)s models were successfully deleted.', count, count=count)) except Exception as ex: if self._debug: raise flash(gettext('Failed to delete models. %(error)s', error=str(ex)), 'error')
class EditForm(form.BaseForm): content = MarkdownField(lazy_gettext('Content'), (validators.required(), ))
def operation(self): return lazy_gettext('equals')
class ModelAdmin(ThemeMixin, Roled, ModelView): form_subdocuments = {} datetime_format = "%Y-%m-%d %H:%M" formatters = {'datetime': format_datetime} def get_instance(self, i): try: return self.model.objects.get(id=i) except self.model.DoesNotExist: flash(gettext("Item not found %(i)s", i=i), "error") @action('toggle_publish', lazy_gettext('Publish/Unpublish'), lazy_gettext('Publish/Unpublish?')) def action_toggle_publish(self, ids): for i in ids: instance = self.get_instance(i) instance.published = not instance.published instance.save() count = len(ids) flash( ngettext( 'Item successfully published/Unpublished.', '%(count)s items were successfully published/Unpublished.', count, count=count)) @action('clone_item', lazy_gettext('Create a copy'), lazy_gettext('Are you sure you want a copy?')) def action_clone_item(self, ids): if len(ids) > 1: flash(gettext("You can select only one item for this action"), 'error') return instance = self.get_instance(ids[0]) new = instance.from_json(instance.to_json()) new.id = None new.published = False new.last_updated_by = User.objects.get(id=current_user.id) new.updated_at = datetime.datetime.now() new.slug = "{0}-{1}".format(new.slug, random.getrandbits(32)) new.save() return redirect(url_for('.edit_view', id=new.id)) @action('export_to_json', lazy_gettext('Export as json')) def export_to_json(self, ids): qs = self.model.objects(id__in=ids) return Response(qs.to_json(), mimetype="text/json", headers={ "Content-Disposition": "attachment;filename=%s.json" % self.model.__name__.lower() }) @action('export_to_csv', lazy_gettext('Export as csv')) def export_to_csv(self, ids): qs = json.loads(self.model.objects(id__in=ids).to_json()) def generate(): yield ','.join(list(qs[0].keys())) + '\n' for item in qs: yield ','.join([str(i) for i in list(item.values())]) + '\n' return Response(generate(), mimetype="text/csv", headers={ "Content-Disposition": "attachment;filename=%s.csv" % self.model.__name__.lower() })
def operation(self): return lazy_gettext('smaller than')
class ModelView(BaseModelView): """ MongoEngine model scaffolding. """ column_filters = None """ Collection of the column filters. Should contain instances of :class:`flask.ext.admin.contrib.pymongo.filters.BasePyMongoFilter` classes. For example:: class MyModelView(BaseModelView): column_filters = (BooleanEqualFilter(User.name, 'Name'),) """ def __init__(self, coll, name=None, category=None, endpoint=None, url=None): """ Constructor :param coll: MongoDB collection object :param name: Display name :param category: Display category :param endpoint: Endpoint :param url: Custom URL """ 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) 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 NotImplemented() 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 NotImplemented() 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 NotImplemented() def _get_field_value(self, model, name): """ Get unformatted field value from the model """ return model.get(name) def get_list(self, page, sort_column, sort_desc, search, filters, execute=True): """ 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 """ query = {} # Filters if self._filters: data = [] for flt, value in filters: f = self._filters[flt] data = f.apply(data, value) if data: if len(data) == 1: query = data[0] else: query['$and'] = data # Search if self._search_supported and search: values = search.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 # Get count count = self.coll.find(query).count() # 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 = [ (order[0], pymongo.DESCENDING if order[1] else pymongo.ASCENDING) ] # Pagination skip = None if page is not None: skip = page * self.page_size results = self.coll.find(query, sort=sort_by, skip=skip, limit=self.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 model. %(error)s', error=str(ex)), 'error') log.exception('Failed to create model') return False else: self.after_model_change(form, model, True) return True 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 model. %(error)s', error=str(ex)), 'error') log.exception('Failed to update model') 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}) return True except Exception as ex: flash(gettext('Failed to delete model. %(error)s', error=str(ex)), 'error') log.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: count = 0 # TODO: Optimize me for pk in ids: if self.delete_model(self.get_one(pk)): count += 1 flash( ngettext('Model was successfully deleted.', '%(count)s models were successfully deleted.', count, count=count)) except Exception as ex: flash(gettext('Failed to delete models. %(error)s', error=str(ex)), 'error')
def operation(self): return lazy_gettext('not equal')
class ModelView(_ModelView): page_size = 50 can_view_details = True details_modal = True edit_modal = True model_form_converter = KModelConverter filter_converter = KFilterConverter() column_type_formatters = _ModelView.column_type_formatters or dict() column_type_formatters[datetime] = type_best column_type_formatters[FileProxy] = type_file show_popover = False 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.column_labels = self.column_labels or dict() for field in model._fields: if field not in self.column_labels: attr = getattr(model, field) if hasattr(attr, 'verbose_name'): verbose_name = attr.verbose_name if verbose_name: self.column_labels[field] = verbose_name # 初始化选择列 self.column_choices = self.column_choices or dict() for field in model._fields: if field not in self.column_choices: choices = getattr(model, field).choices if choices: self.column_choices[field] = 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) def create_model(self, form): try: model = self.model() self.pre_model_change(form, model, True) form.populate_obj(model) self._on_model_change(form, model, True) model.save() except Exception as ex: current_app.logger.error(ex) if not self.handle_view_exception(ex): flash( 'Failed to create record. %(error)s' % dict(error=format_error(ex)), 'error') return False else: self.after_model_change(form, model, True) return True def update_model(self, form, model): try: self.pre_model_change(form, model, False) form.populate_obj(model) self._on_model_change(form, model, False) model.save() except Exception as ex: current_app.logger.error(ex) if not self.handle_view_exception(ex): flash( 'Failed to update record. %(error)s' % dict(error=format_error(ex)), 'error') return False else: self.after_model_change(form, model, False) return True def pre_model_change(self, form, model, created=False): pass def on_model_change(self, form, model, created): if created == True and hasattr(model, 'create'): if callable(model.create): model.create() elif hasattr(model, 'modified'): model.modified = datetime.now() def get_ref_type(self, attr): document, ref_type = attr.document_type, None if hasattr(document, 'id'): xattr = document._fields.get('id') if isinstance(xattr, IntField) or isinstance(xattr, LongField): ref_type = int elif isinstance(xattr, DecimalField) or isinstance( xattr, FloatField): ref_type = float elif isinstance(xattr, ObjectIdField): ref_type = ObjectId return ref_type def scaffold_filters(self, name): 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__ if isinstance(attr, ReferenceField): ref_type = self.get_ref_type(attr) flt = self.filter_converter.convert(type_name, attr, visible_name, ref_type) elif isinstance(attr, ListField) and isinstance( attr.field, ReferenceField): ref_type = self.get_ref_type(attr.field) flt = self.filter_converter.convert(type_name, attr, visible_name, ref_type) elif isinstance(attr, ObjectIdField): flt = self.filter_converter.convert(type_name, attr, visible_name, ObjectId) else: flt = self.filter_converter.convert(type_name, attr, visible_name) return flt def get_list(self, page, sort_column, sort_desc, search, filters, execute=True): query = self.get_query() if self._filters: for flt, flt_name, value in filters: f = self._filters[flt] query = f.apply(query, f.clean(value)) if self._search_supported and search: query = self._search(query, search) count = query.count() if not self.simple_list_pager else None if sort_column: query = query.order_by('%s%s' % ('-' if sort_desc else '', sort_column)) else: order = self._get_default_order() if order: if order[1] != True and order[1] != False: query = query.order_by(*order) else: query = query.order_by('%s%s' % ('-' if order[1] else '', order[0])) # Pagination if page is not None: query = query.skip(page * self.page_size) query = query.limit(self.page_size) if execute: query = query.all() return count, query @action('delete', lazy_gettext('Delete'), lazy_gettext('Are you sure you want to delete selected records?')) def action_delete(self, ids): try: count = 0 id = self.model._meta['id_field'] if id in self.model._fields: if isinstance(self.model._fields[id], IntField): all_ids = [int(pk) for pk in ids] elif isinstance(self.model._fields[id], StringField): all_ids = ids else: all_ids = [self.object_id_converter(pk) for pk in ids] else: 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)) except Exception as ex: if not self.handle_view_exception(ex): flash( gettext('Failed to delete records. %(error)s', error=str(ex)), 'error')
def operation(self): return lazy_gettext('empty')
def view_on_site(self, request, obj, fieldname, *args, **kwargs): return html.a( href=obj.get_absolute_url('detail'), target='_blank', )(html.i(class_="icon icon-eye-open", style="margin-right: 5px;")(), lazy_gettext('View on site'))
def on_model_change(self, form, model): """Sets last_updated attribute of issue object""" model.last_updated = datetime.now() return def set_on_sale_date(self, ids, date): try: issues = Issue.query.filter(Issue.id.in_(*ids)).all() for issue in issues: issue.update(**{'on_sale_date': date}) except Exception, ex: flash(gettext('Failed to set date %(error)s', error=str(ex)), 'error') return @action('set_cover_image', lazy_gettext('Set Cover Image'), lazy_gettext('Are you sure you want to set the cover image?')) def set_cover_image(self, ids): try: issues = Issue.query.filter(Issue.id.in_(ids)).all() for issue in issues: issue.set_cover_image_from_url(issue.big_image, True) issue.find_or_create_thumbnail(width=250) except Exception, ex: flash(gettext('Failed to set cover image %(errors)s', error=str(ex)), 'error') return @action('current_wednesday', lazy_gettext('This Wed | %(date)s', date=current_wednesday()), lazy_gettext('Are you sure? | %(date)s', date=current_wednesday())) def action_current_wednesday(self, ids): self.set_on_sale_date(ids, current_wednesday()) @action('next_wednesday', lazy_gettext('Next Wed | %(date)s', date=next_wednesday()), lazy_gettext('Are you sure? | %(date)s', date=next_wednesday()))
class WorkerPanel(PanelBase): #column_list = ('uid', Category.name, 'username', 'profile_picture', # 'status', 'created_time', 'updated_time') # Visible columns in the list view column_exclude_list = ['category_id'] # List of columns that can be sorted. column_sortable_list = ('uid', 'username', 'created_time', 'updated_time') # Rename 'title' columns to 'Post Title' in list view column_labels = dict(username='******', profile_picture='Avatar') column_searchable_list = ('username', Category.name) column_filters = ('username', Category.name, 'created_time') def _show_pic(self, context, model, name): return Markup('<img src=%s width=90 height=90>' % model.profile_picture) def _show_user(self, context, model, name): return Markup( '<a href="%s">%s</a>' % (url_for('user_view.profile', uid=model.uid), model.username)) def _update_worker(self, uid): access_token = get_token(fetchone=True) if not access_token: return min_id = get_last_media(uid=uid) medias = get_medias(uid=uid, access_token=access_token, min_id=min_id) if medias: insert_medias(medias[:-1]) if isinstance(medias, list): worker = medias[0].user update_worker(uid, worker.username, worker.profile_picture) set_worker_done(uid) @action('refresh', lazy_gettext('Refresh'), lazy_gettext(u'手动更新,不要超过5个')) def action_refresh(self, ids): try: query = get_query_for_ids(self.get_query(), self.model, ids) for worker in query.all(): self._update_worker(worker.uid) count = query.count() flash( ngettext('Record was successfully refreshed.', '%(count)s records were successfully refreshed.', count, count=count)) except Exception as ex: flash( gettext('Failed to refresh records. %(error)s', error=str(ex)), 'error') column_formatters = { 'username': _show_user, 'profile_picture': _show_pic, } def __init__(self, **kwargs): super(WorkerPanel, self).__init__(Worker, db.session, **kwargs)
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))
self.on_model_delete(model) model.delete_instance(recursive=True) return True except Exception, ex: flash(gettext("Failed to delete model. %(error)s", error=str(ex)), "error") 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
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)
def __init__(self, name=None, category=None, endpoint=None, url=None): super(AdminIndexView, self).__init__(name or babel.lazy_gettext('Home'), category, endpoint or 'admin', url or '/admin', 'static')
class ModelView(BaseModelView): column_filters = None """ Collection of the column filters. Can contain either field names or instances of :class:`flask.ext.admin.contrib.peewee.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 = 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:: 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): self._search_fields = [] super(ModelView, self).__init__(model, name, category, endpoint, url) self._primary_key = self.scaffold_pk() def _get_model_fields(self, model=None): if model is None: model = self.model return model._meta.get_sorted_fields() 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) field_type = type(p) # Check type if (field_type != CharField and field_type != 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 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) 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, extra_fields=self.form_extra_fields) if self.inline_models: form_class = self.scaffold_inline_form_models(form_class) return form_class 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): if field.model_class != self.model: model_name = field.model_class.__name__ if model_name not in joins: query = query.join(field.model_class) 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): if sort_field.model_class != 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): 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, value in filters: f = self._filters[flt] query = self._handle_join(query, f.column, joins) query = f.apply(query, value) # Get count count = query.count() # 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 is not None: query = query.offset(page * self.page_size) query = query.limit(self.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): raise flash(gettext('Failed to create model. %(error)s', error=str(ex)), 'error') log.exception('Failed to create model') return False else: self.after_model_change(form, model, True) return True 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): raise flash(gettext('Failed to update model. %(error)s', error=str(ex)), 'error') log.exception('Failed to update model') 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) return True except Exception as ex: if not self.handle_view_exception(ex): raise flash(gettext('Failed to delete model. %(error)s', error=str(ex)), 'error') log.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 flash( ngettext('Model was successfully deleted.', '%(count)s models were successfully deleted.', count, count=count)) except Exception as ex: if not self.handle_view_exception(ex): raise flash(gettext('Failed to delete models. %(error)s', error=str(ex)), 'error')
def _l(*args, **kwargs): return lazy_gettext(*args, **kwargs)
class ModelView(BaseModelView): """ MongoEngine model scaffolding. """ column_filters = None """ Collection of the column filters. Can contain either field names or instances of :class:`flask.ext.admin.contrib.mongoengine.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 = CustomModelConverter """ Model form conversion class. Use this to implement custom field conversion logic. Custom class should be derived from the `flask.ext.admin.contrib.mongoengine.form.CustomModelConverter`. For example:: class MyModelConverter(AdminModelConverter): pass class MyAdminView(ModelView): model_form_converter = MyModelConverter """ 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. """ def __init__(self, model, name=None, category=None, endpoint=None, url=None): """ Constructor :param model: Model class :param name: Display name :param category: Display category :param endpoint: Endpoint :param url: Custom URL """ self._search_fields = [] super(ModelView, self).__init__(model, name, category, endpoint, url) self._primary_key = self.scaffold_pk() 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 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 get_list(self, page, sort_column, sort_desc, search, filters, execute=True): """ 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 """ query = self.get_query() # Filters if self._filters: for flt, value in filters: f = self._filters[flt] query = f.apply(query, value) # Search if self._search_supported and search: # 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) 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 query = query.filter(criteria) # Get count count = query.count() # Sorting if sort_column: query = query.order_by('%s%s' % ('-' if sort_desc else '', sort_column)) else: order = self._get_default_order() if order: query = query.order_by('%s%s' % ('-' if order[1] else '', order[0])) # Pagination if page is not None: query = query.skip(page * self.page_size) query = query.limit(self.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 self._debug: raise flash( gettext('Failed to create model. %(error)s', error=format_error(ex)), 'error') logging.exception('Failed to create model') return False else: self.after_model_change(form, model, True) return True 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 self._debug: raise flash( gettext('Failed to update model. %(error)s', error=format_error(ex)), 'error') logging.exception('Failed to update model') 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() return True except Exception as ex: if self._debug: raise flash( gettext('Failed to delete model. %(error)s', error=format_error(ex)), 'error') logging.exception('Failed to delete model') return False # 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(ObjectId(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 models?')) def action_delete(self, ids): try: count = 0 all_ids = [ObjectId(pk) for pk in ids] for obj in self.get_query().in_bulk(all_ids).values(): count += self.delete_model(obj) flash( ngettext('Model was successfully deleted.', '%(count)s models were successfully deleted.', count, count=count)) except Exception as ex: if self._debug: raise flash(gettext('Failed to delete models. %(error)s', error=str(ex)), 'error')
class ModelView(BaseModelView): """ MongoEngine model scaffolding. """ column_filters = None """ Collection of the column filters. Can contain either field names or instances of :class:`flask.ext.admin.contrib.mongoengine.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 = CustomModelConverter """ Model form conversion class. Use this to implement custom field conversion logic. Custom class should be derived from the `flask.ext.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.ext.admin.contrib.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.ext.admin.consts.ICON_TYPE_GLYPH` - Bootstrap glyph icon - `flask.ext.admin.consts.ICON_TYPE_IMAGE` - Image relative to Flask static directory - `flask.ext.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, 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`. """ 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 wrap_fields_in_fieldlist(self.form_base_class, form_class, custom_fieldlist) # 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): """ 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 """ 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() # Sorting if sort_column: query = query.order_by('%s%s' % ('-' if sort_desc else '', sort_column)) else: order = self._get_default_order() if order: query = query.order_by('%s%s' % ('-' if order[1] else '', order[0])) # Pagination if page is not None: query = query.skip(page * self.page_size) query = query.limit(self.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 True 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() return True 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 # 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)) except Exception as ex: if not self.handle_view_exception(ex): flash(gettext('Failed to delete records. %(error)s', error=str(ex)), 'error')
def operation(self): return lazy_gettext('not between')
class EditForm(form.BaseForm): content = fields.TextAreaField(lazy_gettext('Content'), (validators.required(), ))
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:: admin = Admin() path = op.join(op.dirname(__file__), 'static') admin.add_view(FileAdmin(path, '/static/', name='Static Files')) admin.setup_app(app) """ 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 """ upload_form = UploadForm """ Upload form class """ def __init__(self, base_path, base_url=None, name=None, category=None, endpoint=None, url=None, verify_path=True): """ 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) 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 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 _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 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 """ # 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.')) 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)) 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))) # Sort by name items.sort(key=itemgetter(0)) # Sort by type items.sort(key=itemgetter(2), 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) @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.')) return redirect(self._get_dir_url('.index')) form = self.upload_form(self) if helpers.validate_form_on_submit(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)) 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.')) return redirect(self._get_dir_url('.index')) form = NameForm(helpers.get_form_data()) if helpers.validate_form_on_submit(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') return self.render(self.mkdir_template, form=form, dir_url=dir_url) @expose('/delete/', methods=('POST', )) def delete(self): """ Delete view method """ path = request.form.get('path') if not path: return redirect(self.get_url('.index')) # Get path and verify if it is valid base_path, full_path, path = self._normalize_path(path) return_url = self._get_dir_url('.index', op.dirname(path)) if not self.can_delete: flash(gettext('Deletion is disabled.')) return redirect(return_url) if not self.is_accessible_path(path): flash(gettext('Permission denied.')) return redirect(self._get_dir_url('.index')) if op.isdir(full_path): if not self.can_delete_dirs: flash(gettext('Directory deletion is disabled.')) 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') return redirect(return_url) @expose('/rename/', methods=('GET', 'POST')) def rename(self): """ Rename view method """ path = request.args.get('path') if not path: return redirect(self.get_url('.index')) base_path, full_path, path = self._normalize_path(path) return_url = self._get_dir_url('.index', op.dirname(path)) if not self.can_rename: flash(gettext('Renaming is disabled.')) return redirect(return_url) if not self.is_accessible_path(path): flash(gettext('Permission denied.')) return redirect(self._get_dir_url('.index')) if not op.exists(full_path): flash(gettext('Path does not exist.')) return redirect(return_url) form = NameForm(helpers.get_form_data(), name=op.basename(path)) if helpers.validate_form_on_submit(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) 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.')) 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 = EditForm(helpers.get_form_data()) error = False if helpers.validate_form_on_submit(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: try: with open(full_path, 'r') 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))
def clean(self): homepage = Channel.objects(is_homepage=True) if self.is_homepage and homepage and not self in homepage: raise db.ValidationError(lazy_gettext("Home page already exists")) super(Channel, self).clean()
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'))
class EditForm(form.BaseForm): content = wtf.TextAreaField(lazy_gettext('Content'), [wtf.validators.required()])
except Exception, ex: flash(gettext('Failed to delete model. %(error)s', error=str(ex)), 'error') logging.exception('Failed to delete model') 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 models?')) def action_delete(self, ids): try: model_pk = getattr(self.model, self._primary_key) query = self.get_query().filter(model_pk.in_(ids)) if self.fast_mass_delete: count = query.delete(synchronize_session=False) else: count = 0 for m in query.all(): self.session.delete(m) count += 1
def operation(self): return lazy_gettext("contains")
def operation(self): return lazy_gettext("not between")
def operation(self): return lazy_gettext("empty")
def operation(self): return lazy_gettext("in list")