class Module(metaclass=ModuleMeta): id: str = None endpoint: str = None label: str = None managed_class: type = None list_view = None list_view_columns: List[Dict[str, Any]] = [] single_view = None components: Tuple = () # class based views. If not provided will be automaticaly created from # EntityView etc defined below base_template = "base.html" view_cls = EntityView edit_cls = EntityEdit create_cls = EntityCreate delete_cls = EntityDelete json_search_cls = JSONWhooshSearch JSON2_SEARCH_LENGTH = 50 # form_class. Used when view_cls/edit_cls are not provided edit_form_class = None view_form_class = None # by default, same as edit_form_class url = None name = None view_new_save_and_add = False # show 'save and add new' button in /new form static_folder = None view_template = None view_options = None related_views: List["RelatedView"] = [] blueprint = None search_criterions = ( search.TextSearchCriterion("name", attributes=("name", "nom")), ) # used mostly to change datatable search_label tableview_options = {} # type: ignore _urls: List[Tuple] = [] def __init__(self) -> None: # If endpoint name is not provided, get it from the class name if self.endpoint is None: class_name = self.__class__.__name__ if class_name.endswith("Module"): class_name = class_name[0 : -len("Module")] self.endpoint = class_name.lower() if self.label is None: self.label = labelize(self.endpoint) if self.id is None: self.id = self.managed_class.__name__.lower() # If name is not provided, use capitalized endpoint name if self.name is None: self.name = self._prettify_name(self.__class__.__name__) if self.view_options is None: self.view_options = {} # self.single_view = make_single_view(self.edit_form_class, # view_template=self.view_template, # **self.view_options) if self.view_form_class is None: self.view_form_class = self.edit_form_class # init class based views kw = { "Model": self.managed_class, "pk": "entity_id", "module": self, "base_template": self.base_template, } self._setup_view( "/<int:entity_id>", "entity_view", self.view_cls, Form=self.view_form_class, **kw, ) view_endpoint = self.endpoint + ".entity_view" self._setup_view( "/<int:entity_id>/edit", "entity_edit", self.edit_cls, Form=self.edit_form_class, view_endpoint=view_endpoint, **kw, ) self._setup_view( "/new", "entity_new", self.create_cls, Form=self.edit_form_class, chain_create_allowed=self.view_new_save_and_add, view_endpoint=view_endpoint, **kw, ) self._setup_view( "/<int:entity_id>/delete", "entity_delete", self.delete_cls, Form=self.edit_form_class, view_endpoint=view_endpoint, **kw, ) self._setup_view("/json", "list_json", ListJson, module=self) self._setup_view( "/json_search", "json_search", self.json_search_cls, Model=self.managed_class, ) self.init_related_views() # copy criterions instances; without that they may be shared by # subclasses self.search_criterions = copy.deepcopy(self.__class__.search_criterions) for sc in self.search_criterions: sc.model = self.managed_class self.__components = {} for component in self.components: component.init_module(self) self.__components[component.name] = component def get_component(self, name): return self.__components.get(name) def _setup_view(self, url: str, attr: str, cls: Any, *args, **kwargs) -> None: """Register class based views.""" view = cls.as_view(attr, *args, **kwargs) setattr(self, attr, view) self._urls.append((url, attr, view.methods)) def init_related_views(self) -> None: related_views = [] for view in self.related_views: if not isinstance(view, RelatedView): view = DefaultRelatedView(*view) related_views.append(view) self.related_views = related_views @property def action_category(self) -> str: return f"module:{self.endpoint}" def get_grouped_actions(self) -> OrderedDict: items = actions.for_category(self.action_category) groups = OrderedDict() for action in items: groups.setdefault(action.group, []).append(action) return groups def register_actions(self) -> None: ACTIONS = [ ModuleAction( self, "entity", "create", title=_l("Create New"), icon=FAIcon("plus"), endpoint=Endpoint(self.endpoint + ".entity_new"), button="default", ) ] for component in self.components: ACTIONS.extend(component.get_actions()) actions.register(*ACTIONS) def create_blueprint(self, crud_app: "CRUDApp") -> Blueprint: """Create a Flask blueprint for this module.""" # Store admin instance self.crud_app = crud_app self.app = crud_app.app # If url is not provided, generate it from endpoint name if self.url is None: self.url = f"{self.crud_app.url}/{self.endpoint}" else: if not self.url.startswith("/"): self.url = f"{self.crud_app.url}/{self.url}" # Create blueprint and register rules self.blueprint = Blueprint(self.endpoint, __name__, url_prefix=self.url) for url, name, methods in self._urls: self.blueprint.add_url_rule(url, name, getattr(self, name), methods=methods) # run default_view decorator default_view(self.blueprint, self.managed_class, id_attr="entity_id")( self.entity_view ) # delay registration of our breadcrumbs to when registered on app; thus # 'parents' blueprint can register theirs befores ours self.blueprint.record_once(self._setup_breadcrumb_preprocessors) return self.blueprint def _setup_breadcrumb_preprocessors(self, state: BlueprintSetupState) -> None: self.blueprint.url_value_preprocessor(self._add_breadcrumb) def _add_breadcrumb(self, endpoint: str, values: Dict[Any, Any]) -> None: g.breadcrumb.append( BreadcrumbItem(label=self.label, url=Endpoint(".list_view")) ) @property def base_query(self) -> EntityQuery: """Return a query instance for :attr:`managed_class`.""" return self.managed_class.query @property def read_query(self): """Return a query instance for :attr:`managed_class` filtering on `READ` permission.""" return self.base_query.with_permission(READ) @property def listing_query(self) -> EntityQuery: """Like `read_query`, but can be made lightweight with only columns and joins of interest. `read_query` can be used with exports for example, with lot more columns (generallly it means more joins). """ return self.base_query.with_permission(READ) def query(self, request: Request): """Return filtered query based on request args.""" args = request.args search = args.get("sSearch", "").replace("%", "").lower() query = self.read_query.distinct() for crit in self.search_criterions: query = crit.filter(query, self, request, search) return query def list_query(self, request: Request) -> EntityQuery: """Return a filtered query based on request args, for listings. Like `query`, but subclasses can modify it to remove costly joined loads for example. """ args = request.args search = args.get("sSearch", "").replace("%", "").lower() query = self.listing_query query = query.distinct() for crit in self.search_criterions: query = crit.filter(query, self, request, search) return query def ordered_query( self, request: Request, query: Optional[EntityQuery] = None ) -> EntityQuery: """Order query according to request args. If query is None, the query is generated according to request args with self.query(request) """ if query is None: query = self.query(request) engine = query.session.get_bind(self.managed_class.__mapper__) args = request.args sort_col = int(args.get("iSortCol_0", 1)) sort_dir = args.get("sSortDir_0", "asc") sort_col_def = self.list_view_columns[sort_col] sort_col_name = sort_col_def["name"] rel_sort_names = sort_col_def.get("sort_on", (sort_col_name,)) sort_cols = [] for rel_col in rel_sort_names: sort_col = getattr(self.managed_class, rel_col) if hasattr(sort_col, "property") and isinstance( sort_col.property, orm.properties.RelationshipProperty ): # this is a related model: find attribute to filter on query = query.outerjoin(sort_col_name, aliased=True) rel_model = sort_col.property.mapper.class_ default_sort_name = "name" if issubclass(rel_model, BaseVocabulary): default_sort_name = "label" rel_sort_name = sort_col_def.get("relationship_sort_on", None) if rel_sort_name is None: rel_sort_name = sort_col_def.get("sort_on", default_sort_name) sort_col = getattr(rel_model, rel_sort_name, None) # XXX: Big hack, date are sorted in reverse order by default if isinstance(sort_col, (Date, DateTime)): sort_dir = "asc" if sort_dir == "desc" else "desc" elif ( isinstance(sort_col, sa.types.String) or hasattr(sort_col, "property") and isinstance(sort_col.property.columns[0].type, sa.types.String) ): sort_col = func.lower(sort_col) if sort_col is not None: try: direction = desc if sort_dir == "desc" else asc sort_col = direction(sort_col) except Exception: # FIXME pass # sqlite does not support 'NULLS FIRST|LAST' in ORDER BY # clauses if engine.name != "sqlite": nullsorder = nullslast if sort_dir == "desc" else nullsfirst try: sort_col = nullsorder(sort_col) except Exception: # FIXME pass sort_cols.append(sort_col) if sort_cols: try: query = query.order_by(*sort_cols) except Exception: # FIXME pass query.reset_joinpoint() return query # # Exposed views # @expose("/") def list_view(self) -> str: actions.context["module"] = self table_view = AjaxMainTableView( name=self.managed_class.__name__.lower(), columns=self.list_view_columns, ajax_source=url_for(".list_json"), search_criterions=self.search_criterions, options=self.tableview_options, ) rendered_table = table_view.render() ctx = { "rendered_table": rendered_table, "module": self, "base_template": self.base_template, } return render_template("default/list_view.html", **ctx) def list_json2_query_all(self, q): """Implements the search query for the list_json2 endpoint. May be re-defined by a Module subclass in order to customize the search results. - Return: a list of results (not json) with an 'id' and a 'text' (that will be displayed in the select2). """ cls = self.managed_class query = db.session.query(cls.id, cls.name) query = ( query.filter(cls.name.ilike("%" + q + "%")) .distinct() .order_by(cls.name) .limit(self.JSON2_SEARCH_LENGTH) ) results = query.all() results = [{"id": r[0], "text": r[1]} for r in results] return results @expose("/json2") def list_json2(self): """Other JSON endpoint, this time used for filling select boxes dynamically. You can write your own search method in list_json2_query_all, that returns a list of results (not json). """ args = request.args q = args.get("q", "").replace("%", " ") if not q or len(q) < 2: raise BadRequest() results = self.list_json2_query_all(q) return {"results": results} # # Utils # def is_current(self): return request.path.startswith(self.url) @staticmethod def _prettify_name(name: str) -> str: """Prettify class name by splitting name by capital characters. So, 'MySuperClass' will look like 'My Super Class' `name` String to prettify """ return re.sub(r"(?<=.)([A-Z])", r" \1", name)
import os import itertools from flask import jsonify from flask.blueprints import Blueprint from ...model import Trial from .._utils import convert_date, allow_origin, inject_model, answer_options from .._utils import register_invalid_error from ..errors import UnknownElement blueprint = Blueprint('block', os.path.splitext(__name__)[0]) blueprint.url_value_preprocessor(inject_model) register_invalid_error(blueprint, UnknownElement) allow_origin(blueprint) answer_options(blueprint) @blueprint.route('/<experiment>/<run>/<int:block>') def block_props(experiment, run, block): props = { 'number': block.number, 'measuredBlockNumber': block.measured_block_number(), 'factorValues': dict((value.factor.id, value.id) for value in block.factor_values), 'trialCount': block.trials.count(), 'practice': block.practice } return jsonify(props) def generate_block_trials_info(block, completed_only=False,