class Generator(object): def __init__(self, verbose: bool, output_dir=DEFAULT_OUTPUT_DIR, module_name=DEFAULT_MODULE, omit_exporter=False, permissions=False, permissions_store=False): self.parser = None self._resources = None self.verbose = verbose self.output_dir = output_dir self.module_name = module_name self._currentIO = None self.page_details = None self.omit_exporter = omit_exporter self.permissions = permissions or permissions_store self.permissions_store = permissions_store self.permissions_import = "permissions_store" \ if permissions_store else "permissions" self._directories = set([]) def load_specification(self, specification_path: str, spec_format: str): """ This function will load the swagger specification using the swagger_parser. The function will automatically call the `_init_class_resources` after. :param specification_path: The path where the swagger specification is located. :param spec_format: The file format of the specification. """ # If the swagger spec format is not specified explicitly, we try to # derive it from the specification path if not spec_format: filename = os.path.basename(specification_path) extension = filename.rsplit(".", 1)[-1] if extension in YAML_EXTENSIONS: spec_format = SPEC_YAML elif extension in JSON_EXTENSIONS: spec_format = SPEC_JSON else: raise RuntimeError("Could not infer specification format. Use " "--spec-format to specify it explicitly.") click.secho(f"Using spec format '{spec_format}'", fg="green") if spec_format == SPEC_YAML: with open(specification_path, "r") as f: self.parser = SwaggerParser(swagger_yaml=f) else: self.parser = SwaggerParser(swagger_path=specification_path) self._init_class_resources() def _get_definition_from_ref(self, definition: dict): """ Get the swagger definition from a swagger reference declaration in the spec. :param definition: The definition containing the reference declaration. :return: The definition pointed to by the ref and the title """ if "$ref" in definition: definition_name = \ self.parser.get_definition_name_from_ref(definition["$ref"]) title = definition_name.replace("_", " ").title().replace(" ", "") return self.parser.specification["definitions"][ definition_name], title else: title = next(iter(definition)).replace(" ", " ").title().replace( " ", "") return definition, title def _get_parameter_from_ref(self, parameter: dict): """ Get the parameter definition from a swagger reference declaration in the spec. :param parameter: The definition containing the reference declaration. :return: The parameter pointed to by the ref """ # If the parameter is a reference, get the actual parameter. if "$ref" in parameter: ref = parameter["$ref"].split("/")[2] return self.parser.specification["parameters"][ref] else: return parameter def _get_parameter_definition(self): """ Get the create/update object definition from the current parameters. :return: The definition desired. """ for parameter in self._currentIO.get("parameters", []): param = self._get_parameter_from_ref(parameter) # Grab the body parameter as the create/update definition if param["in"] == "body": return self._get_definition_from_ref( definition=param["schema"]) return None def _get_related_field_permissions(self, field_name_resource: str): """ Get the list permissions of a given related field resource. :param field_name_resource: The name of the related field resource. :return: The permissions found. """ permissions = [] list_path = self.parser.specification["paths"].get( f"/{field_name_resource}", None) if list_path: operation = list_path.get("get", None) if operation: permissions = operation.get("x-permissions", []) return permissions def _build_related_field(self, resource: str, name: str, _field: dict, _property: dict): """ Build out a related field :param resource: The name of the current resource. :param name: The name of the current field. :param _field: The field to be built out further as a related field. :param _property: The current property being looked at. :return: Tuple of the resultant field and if a related field or not. """ related = False if "x-related-info" in _property: # Check and handle related info related_info = _property["x-related-info"] if related_info: model = related_info.get("model", False) # Check if related model is not set to None. if model is not None: # Check and add permission imports if found has_permissions = self._resources[resource][ "has_permission_fields"] if self.permissions and not has_permissions: self._resources[resource][ "has_permission_fields"] = True self._resources[resource]["custom_imports"].update( ["empty", self.permissions_import]) related = True # If model didn't even exist then attempt to guess the model # from the substring before the last "_". if not model: model = name.rsplit("_", 1)[0] _field["label"] = model.replace("_", " ").title() # If a custom base path has been given set the reference to it # else attempt to get the plural of the given model. if related_info.get("rest_resource_name", None) is not None: reference = related_info["rest_resource_name"] else: reference = words.plural(model.replace("_", "")) _field["reference"] = reference # Add permissions of reference if using normal permission generation scheme. if self.permissions and not self.permissions_store: _field[ "permissions"] = self._get_related_field_permissions( reference) # Get the option text to be used in the Select input from the # label field, else guess it from the current property name. guess = name.rsplit("_", 1)[1] label = related_info.get("label", None) or guess _field["option_text"] = label return related, _field def _build_fields(self, resource: str, singular: str, properties: dict, _input: bool, fields: list): """ Build out fields for the given properties. :param resource: The current resource name. :param singular: The current singular resource name. :param properties: The properties to build the fields from. :param _input: Boolean signifying if input fields or not. :param fields: List of fields desired. If NONE all are allowed. :return: A tuple of fields and imports """ _imports = set([]) _fields = [] required_properties = self._current_definition.get("required", []) page_details = self.page_details.get(singular, None) sortable = page_details.get("sortable_fields", []) if page_details else [] for name, details in properties.items(): # Check if in list of accepted fields if fields: if name not in fields: continue # Handle possible reference definition. _property, title = self._get_definition_from_ref(details) # Not handling nested object definitions, yet, maybe. if "properties" in _property: continue read_only = _property.get("readOnly", False) and _input _type = _property.get("type", None) # Build field length object for strings and integer types length_values = { "minLength": _property.get("minLength", None), "maxLength": _property.get("maxLength", None), "minValue": _property.get("minimum", None), "maxValue": _property.get("maximum", None) } if _type in ["string", "integer"] else {} # Load imports one_value = False for key, value in length_values.items(): if value: one_value = True _imports.add(key) _field = { "source": name, "type": _type, "required": name in required_properties, "sortable": name in sortable, "read_only": read_only, "length_values": length_values if one_value else {} } if read_only: _imports.add("DisabledInput") if _field["required"]: _imports.add("required") if _input: mapping = INPUT_COMPONENT_MAPPING else: mapping = FIELD_COMPONENT_MAPPING # Check for enum possibility. if "enum" in _property: _field["component"] = mapping["enum"] if _input: _field["choices"] = _property["enum"] elif _field["type"] in mapping or _field["type"] in CUSTOM_IMPORTS: related, _field = self._build_related_field( resource=resource, name=name, _field=_field, _property=_property) if not related: # Check if format overrides the component. _format = _property.get("format", None) if _format in mapping or _format in CUSTOM_IMPORTS: _type = _format # Check if a custom component exists for this _type if _type in CUSTOM_IMPORTS and _type not in mapping: _field["component"] = CUSTOM_IMPORTS[_type]["name"] else: _field["component"] = mapping[_type] else: # Get relation component and the related component _field["component"] = mapping["relation"] relation = "SelectInput" if _input else mapping[_type] _field["related_component"] = relation _imports.add(relation) if _type in CUSTOM_IMPORTS and _type not in mapping: self._resources[resource]["custom_imports"].add(_type) else: # Add component to imports if "component" in _field: _imports.add(_field["component"]) _fields.append(_field) return _fields, _imports def _build_resource(self, resource: str, singular: str, method: str): """ Build out a resource. :param resource: The name of the resource. :param singular: The singular name of the resource. :param method: The method to build out. """ _input = method in ["create", "update"] properties = self._current_definition.get("properties", {}) permissions = self._currentIO.get("x-permissions", []) if properties: # Build out fields for a resource. _fields, _imports = self._build_fields(resource=resource, singular=singular, properties=properties, _input=_input, fields=[]) # Get responsive fields for a resource. page_details = self.page_details.get(singular, {}) responsive_fields = page_details.get("responsive_fields", {}) responsive_obj = {} for prop, field in responsive_fields.items(): responsive_obj[prop] = { "field": field, "title": field.replace("_", " ").title() } if responsive_obj: _imports.update(["Responsive", "SimpleList"]) self._resources[resource]["methods"][ SUPPORTED_COMPONENTS[method]] = { "fields": _fields, "permissions": permissions, "responsive_fields": responsive_obj if method == "list" else None } current_imports = self._resources[resource]["imports"] self._resources[resource]["imports"] = current_imports.union( _imports) def _build_filters(self, resource: str): """ Build out filters for a resource. :param resource: The current resource to build filters for. """ filters = [] filter_imports = set([]) custom_imports = set([]) for parameter in self._currentIO.get("parameters", []): param = self._get_parameter_from_ref(parameter) x_filter = param.get("x-filter", None) valid = all([ param["in"] == "query", param["type"] in INPUT_COMPONENT_MAPPING or x_filter, "x-admin-exclude" not in param ]) if valid: _type = param["type"] # Check if related model filter. relation = None related_info = param.get("x-related-info", None) if related_info: _type = "relation" relation = { "component": "SelectInput", "resource": related_info["rest_resource_name"], "text": related_info.get("label", None) } filter_imports.add("SelectInput") # Add filter component if x_filter: _type = x_filter["format"] if x_filter.get("range", False): _type = f"{_type}-range" if _type in INPUT_COMPONENT_MAPPING: component = INPUT_COMPONENT_MAPPING[_type] filter_imports.add(component) else: component = CUSTOM_IMPORTS[_type]["name"] custom_imports.add(_type) # Load any custom props props = CUSTOM_PROPS.get(_type, []) # Load Min and Max values of filter. _min = param.get("minLength", None) _max = param.get("maxLength", None) source = param["name"] label = source.replace("_", " ").title() if _min or _max: self._resources[resource]["filter_lengths"][source] = { "min_length": _min, "max_length": _max } array_validation = param["items"]["type"] \ if _type == "array" else None filters.append({ "source": source, "label": label, "title": label.replace(" ", ""), "component": component, "props": props, "relation": relation, "array": array_validation }) self._resources[resource]["filters"] = { "filters": list(filters), "imports": list(filter_imports), "custom_imports": [CUSTOM_IMPORTS[_import] for _import in custom_imports] } def _build_in_lines(self, resource: str, singular: str, method: str): """ Build out the given resource in lines for a resource. :param resource: The name of the current resource. :param singular: The singular name of the current resource. :param method: The current opertation method. """ in_lines = [] _input = method in ["create", "update"] if _input: mapping = INPUT_COMPONENT_MAPPING else: mapping = FIELD_COMPONENT_MAPPING for in_line in self.page_details[singular].get("inlines", []): has_permissions = self._resources[resource][ "has_permission_fields"] if self.permissions and not has_permissions: self._resources[resource]["has_permission_fields"] = True self._resources[resource]["custom_imports"].update( ["empty", self.permissions_import]) model = in_line["model"] label = in_line.get("label", None) # If a custom base path has been given, use that as reference. ref = in_line.get("rest_resource_name", None) reference = ref or words.plural(model.replace("_", "")) permissions = [] if self.permissions and not self.permissions_store: permissions = self._get_related_field_permissions(reference) fields = in_line.get("fields", None) many_field = { "label": label or model.replace("_", " ").title(), "reference": reference, "target": in_line["key"], "component": mapping["many"], "permissions": permissions } self._resources[resource]["imports"].add(many_field["component"]) _def = self.parser.specification["definitions"][in_line["model"]] properties = _def.get("properties", {}) many_field["fields"], _imports = self._build_fields( resource=resource, singular=singular, properties=properties, _input=False, fields=fields) in_lines.append(many_field) self._resources[resource]["imports"].update(_imports) self._resources[resource]["methods"][ SUPPORTED_COMPONENTS[method]]["inlines"] = in_lines def _init_class_resources(self): """ Initialize the class resources object. """ self._resources = {} self.page_details = self.parser.specification.get( "x-detail-page-definitions", {}) for path, verbs in self.parser.specification["paths"].items(): for verb, io in verbs.items(): self._currentIO = io # Ignore top level parameter definition (not a path). if verb == "parameters": continue else: # Check if operation id is a valid operation. operation_id = io.get("operationId", "") exclude = io.get("x-admin-exclude", False) valid_operation = any([ operation in operation_id for operation in VALID_OPERATIONS ]) if not operation_id or not valid_operation or exclude: continue singular, op = operation_id.rsplit("_", 1) plural = path[1:].split("/")[0] details = VALID_OPERATIONS.get(op, None) if details: if plural not in self._resources: self._resources[plural] = { "singular": singular, "imports": set([]), "methods": {}, "filter_lengths": {}, "has_permission_fields": False, "custom_imports": set([]) } self._resources[plural]["imports"].update( details["imports"]) # Special additions for certain operation types. if op == "list": self._current_definition, title = self._get_definition_from_ref( definition=io['responses']['200']['schema'] ['items']) self._resources[plural]["title"] = title self._build_filters(resource=plural) elif op == "read": self._current_definition, title = self._get_definition_from_ref( definition=io['responses']['200']['schema']) self._resources[plural]["title"] = title elif op in ["create", "update"]: if op == "update": self._resources[plural]["custom_imports"].add( self.permissions_import) self._current_definition, title = self._get_parameter_definition( ) elif op == "delete": self._resources[plural]["custom_imports"].add( self.permissions_import) self._current_definition = None if self.permissions: permissions = io.get("x-permissions", []) else: permissions = None self._resources[plural]["methods"]["remove"] = { "permissions": permissions } # Build out the current resource if a definition is found. if self._current_definition: self._build_resource(resource=plural, singular=singular, method=op) # Build in lines if existing page definition. in_lines = all([ op in ["update", "read"], singular in self.page_details ]) if in_lines: self._build_in_lines(resource=plural, singular=singular, method=op) for resource in self._resources.keys(): custom_imports = list(self._resources[resource]["custom_imports"]) custom_imports = [ CUSTOM_IMPORTS[_import] for _import in custom_imports ] self._resources[resource]["custom_imports"] = custom_imports @staticmethod def generate_js_file(filename: str, context: dict): """ Generate a js file from the given specification. :param filename: The name of the template file. :param context: Context to be passed. :return: str """ return render_to_string(filename, context) @staticmethod def add_additional_file(filename: str): """ Add an additional file, that does not require context, to the generated admin. :return: str """ return render_to_string(filename, {}) def create_and_generate_file(self, _dir: str, filename: str, context: dict, source=None): """ Create a file of the given name and context. :param _dir: The output directory. :param filename: The name of the file to be created. :param context: The context for jinja. :param source: Alternative source file for the template. """ click.secho(f"Generating {filename}.js file...", fg="green") with open(os.path.join(_dir, f"{filename}.js"), "w") as f: data = self.generate_js_file(filename=f"{source or filename}.js", context=context) f.write(data) if self.verbose: print(data) def create_additional_files(self, additional_files: dict): for _dir, files in additional_files.items(): if _dir != "root": path_dir = f"{self.output_dir}/{_dir}" if not os.path.exists(path_dir): os.makedirs(path_dir) else: path_dir = self.output_dir for _file in files: click.secho(f"Adding {_file} file...", fg="cyan") with open(os.path.join(path_dir, _file), "w") as f: data = self.add_additional_file(_file) f.write(data) if self.verbose: print(data) def get_and_create_directory(self, _dir_name: str): """ Gets the full directory and creates it if it does not exist. :param _dir_name: The sub directory to get and create if needed. :return: string of the full directory """ full_dir = f"{self.output_dir}/{_dir_name}" if _dir_name not in self._directories: os.makedirs(full_dir) self._directories.add(_dir_name) return full_dir def admin_generation(self): click.secho("Generating main JS component file...", fg="blue") self.create_and_generate_file(_dir=self.output_dir, filename="ReactAdmin", context={ "title": self.module_name, "resources": self._resources, "permissions": self.permissions, "permissions_store": self.permissions_store, "supported_components": SUPPORTED_COMPONENTS.values() }, source="ReactAdmin_permissions" if self.permissions else "ReactAdmin") click.secho("Generating menu...", fg="blue") self.create_and_generate_file(_dir=self.output_dir, filename="Menu", context={"resources": self._resources}) click.secho("Generation auth provider...", fg="blue") auth_dir = self.get_and_create_directory("auth") self.create_and_generate_file(_dir=auth_dir, filename="authProvider", context={ "permissions": self.permissions, "permissions_store": self.permissions_store }) click.secho("Generating data provider...", fg="blue") self.create_and_generate_file(_dir=self.output_dir, filename="dataProvider", context={"resources": self._resources}) click.secho("Generating catch all...", fg="blue") self.create_and_generate_file(_dir=self.output_dir, filename="catchAll", context={ "permissions": self.permissions, "permissions_store": self.permissions_store }) click.secho("Generating utils file...", fg="blue") self.create_and_generate_file(_dir=self.output_dir, filename="utils", context={ "permissions": self.permissions, "permissions_store": self.permissions_store }) click.secho("Generating resource component files...", fg="blue") resource_dir = self.get_and_create_directory("resources") for name, resource in self._resources.items(): title = resource.get("title", None) if title: self.create_and_generate_file( _dir=resource_dir, filename=title, context={ "title": title, "name": name, "resource": resource, "omit_exporter": self.omit_exporter, "permissions": self.permissions, "permissions_store": self.permissions_store, "supported_components": SUPPORTED_COMPONENTS.values() }, source="Resource_permissions" if self.permissions else "Resource") click.secho("Generating custom edit toolbar component files...", fg="blue") action_dir = self.get_and_create_directory("customActions") for name, resource in self._resources.items(): title = resource.get("title", None) if title: if "edit" in resource["methods"]: action_file = f"{title}EditToolbar" self.create_and_generate_file( _dir=action_dir, filename=action_file, context={ "name": name, "resource": resource, "permissions": self.permissions, "permissions_store": self.permissions_store }, source="EditToolbar_permissions" if self.permissions else "EditToolbar") if self.omit_exporter: click.secho("Generating List Action files...", fg="blue") for name, resource in self._resources.items(): title = resource.get("title", None) if title: action_file = f"{title}ListActions" self.create_and_generate_file( _dir=action_dir, filename=action_file, context={ "name": name, "resource": resource, "permissions": self.permissions, "permissions_store": self.permissions_store }, source="ListActions_permissions" if self.permissions else "ListActions") click.secho("Generating Filter files for resources...", fg="blue") filter_dir = self.get_and_create_directory("filters") for name, resource in self._resources.items(): if resource.get("filters", None) is not None: title = resource.get("title", None) if title: filter_file = f"{title}Filter" self.create_and_generate_file(_dir=filter_dir, filename=filter_file, context={ "title": title, "filters": resource["filters"] }, source="Filters") if self.permissions: self.create_additional_files(PERMISSION_ADDITIONAL_FILES) if self.permissions_store: click.secho("Generating Permissions Store...", fg="blue") auth_dir = self.get_and_create_directory("auth") self.create_and_generate_file( _dir=auth_dir, filename="PermissionsStore", context={ "resources": self._resources, "supported_components": SUPPORTED_COMPONENTS.values() }) self.create_additional_files(ADDITIONAL_FILES)
class Generator(object): def __init__(self, output_dir, module_name=DEFAULT_MODULE, verbose=False, rest_server_url=None, permissions=False): self.parser = None self.module_name = module_name self._resources = None self.verbose = verbose self.output_dir = output_dir self.rest_server_url = rest_server_url self.permissions = permissions def load_specification(self, specification_path, spec_format=None): # If the swagger spec format is not specified explicitly, we try to # derive it from the specification path if not spec_format: filename = os.path.basename(specification_path) extension = filename.rsplit(".", 1)[-1] if extension in YAML_EXTENSIONS: spec_format = SPEC_YAML elif extension in JSON_EXTENSIONS: spec_format = SPEC_JSON else: raise RuntimeError("Could not infer specification format. Use " "--spec-format to specify it explicitly.") click.secho("Using spec format '{}'".format(spec_format), fg="green") if spec_format == SPEC_YAML: with open(specification_path, "r") as f: self.parser = SwaggerParser(swagger_yaml=f) else: self.parser = SwaggerParser(swagger_path=specification_path) self._make_resource_definitions() def _get_definition_from_ref(self, definition): if "$ref" in definition: definition_name = \ self.parser.get_definition_name_from_ref(definition["$ref"]) ref_def = self.parser.specification["definitions"][definition_name] title = definition_name.replace("_", " ").title().replace(" ", "") return ref_def, title else: return definition, None def _get_resource_attributes(self, resource_name, properties, definition, suffix, fields=None): attributes = [] found_reference = False for name, details in properties.items(): # Check for desired fields and continue if not in there. if fields is not None: if name not in fields: continue # Handle reference definition _property, title = self._get_definition_from_ref(details) if _property.get("properties", None) is not None: continue attribute = { "source": name, "type": _property.get("type", None), "required": name in definition.get("required", []), "read_only": _property.get("readOnly", False) if suffix == "Input" else False } # Add DisabledInput to Imports if read_only is true. if attribute["read_only"] and "DisabledInput" \ not in self._resources[resource_name]["imports"]: self._resources[resource_name]["imports"].append( "DisabledInput") # Based on the type/format combination get the correct # AOR component to use. related_field = False if attribute["type"] in COMPONENT_MAPPING[suffix]: # Check if it is a related field or not if _property.get("x-related-info", None) is not None: if self.permissions and not found_reference: found_reference = True custom_imports = [ custom["name"] for custom in self._resources[resource_name]["custom_imports"] ] if "EmptyField" not in custom_imports: self._resources[resource_name]["custom_imports"].append( CUSTOM_IMPORTS["empty"] ) related_info = _property["x-related-info"] model = related_info.get("model", False) # Check if related model is not set to None. if model is not None: related_field = True # If model didn't even exist then attempt to guess the model # from the substring before the last "_". if not model: model = name.rsplit("_", 1)[0] attribute["label"] = model.replace("_", " ").title() # If a custom base path has been given set the reference to it # else attempt to get the plural of the given model. if related_info.get("rest_resource_name", None) is not None: reference = related_info["rest_resource_name"] else: reference = words.plural(model.replace("_", "")) attribute["reference"] = reference # Get the option text to be used in the Select input from the # label field, else guess it from the current property name. guess = name.rsplit("_", 1)[1] label = related_info.get("label", None) or guess attribute["option_text"] = label elif name.endswith("_id"): related_field = True relation = name.replace("_id", "") attribute["label"] = relation.title() attribute["reference"] = words.plural(relation) attribute["related_field"] = "id" # LongTextFields don't exist # Handle component after figuring out if a related field or not. if not related_field: if _property.get("format", None) in COMPONENT_MAPPING[suffix]: # DateTimeField is currently not supported. if suffix == "Field" and _property["format"] == "date-time": _type = "date" else: _type = _property["format"] attribute["component"] = \ COMPONENT_MAPPING[suffix][_type] else: attribute["component"] = \ COMPONENT_MAPPING[suffix][attribute["type"]] else: attribute["component"] = \ COMPONENT_MAPPING[suffix]["relation"] if suffix != "Input": attribute["related_component"] = \ COMPONENT_MAPPING[suffix][attribute["type"]] else: attribute["related_component"] = "SelectInput" # Handle an enum possibility if _property.get("enum", None) is not None: attribute["component"] = COMPONENT_MAPPING[suffix]["enum"] # Only add choices if an input if suffix == "Input": attribute["choices"] = _property["enum"] if attribute.get("component", None) is not None: # Add component to resource imports if not there. if attribute["component"] not in \ self._resources[resource_name]["imports"] and \ attribute["component"] not in CUSTOM_COMPONENTS: self._resources[resource_name]["imports"].append( attribute["component"] ) # Add related component to resource imports if not there. if attribute.get("related_component", None) is not None: if attribute["related_component"] not in \ self._resources[resource_name]["imports"]: self._resources[resource_name]["imports"].append( attribute["related_component"] ) attributes.append(attribute) # Check for custom import types here. _type = "{}-{}".format(attribute["type"], suffix.lower()) _format = "{}-{}".format(_property.get("format", ""), suffix.lower()) if _type in CUSTOM_IMPORTS or _format in CUSTOM_IMPORTS: custom_imports = [ custom["name"] for custom in self._resources[resource_name]["custom_imports"] ] _import = CUSTOM_IMPORTS.get( _format) or CUSTOM_IMPORTS.get(_type) if _import["name"] not in custom_imports: self._resources[resource_name]["custom_imports"].append( _import ) return attributes def _get_resource_from_definition(self, resource_name, head_component, definition, permissions=None): self._resources[resource_name][head_component] = { "permissions": permissions or [] } suffix = COMPONENT_SUFFIX[head_component] properties = definition.get("properties", {}) resource = self._get_resource_attributes( resource_name=resource_name, properties=properties, definition=definition, suffix=suffix ) # Only add if there is something in resource if resource: self._resources[resource_name][head_component]["fields"] = resource # Check if there are inline models for the given resource. inlines = self.parser.specification.get( "x-detail-page-definitions", None ) # Inlines are only shown on the Show and Edit components. if inlines is not None and head_component in ["show", "edit"]: if resource_name in inlines: if self.permissions: custom_imports = [ custom["name"] for custom in self._resources[resource_name]["custom_imports"] ] if "EmptyField" not in custom_imports: self._resources[resource_name]["custom_imports"].append( CUSTOM_IMPORTS["empty"] ) self._resources[resource_name][head_component]["inlines"] = [] inlines = inlines[resource_name]["inlines"] for inline in inlines: model = inline["model"] label = inline.get("label", None) # If a custom base path has been given. if inline.get("rest_resource_name", None) is not None: reference = inline["rest_resource_name"] else: reference = words.plural(model.replace("_", "")) fields = inline.get("fields", None) many_field = { "label": label or model.replace("_", " ").title(), "reference": reference, "target": inline["key"], "component": COMPONENT_MAPPING[suffix]["many"] } # Add ReferenceMany component to imports if many_field["component"] not in \ self._resources[resource_name]["imports"]: self._resources[resource_name]["imports"].append( many_field["component"] ) inline_def = \ self.parser.specification["definitions"][inline["model"]] properties = inline_def.get("properties", {}) many_field["fields"] = self._get_resource_attributes( resource_name=resource_name, properties=properties, definition=inline_def, suffix="Field", fields=fields ) self._resources[resource_name][head_component]["inlines"].append( many_field ) def _make_resource_definitions(self): self._resources = {} permission_imports_loaded = False for path, verbs in self.parser.specification["paths"].items(): for verb, io in verbs.items(): # Check if this is not a valid path method then skip it. if verb == "parameters": continue else: operation_id = io.get("operationId", "") valid_operation = any([ operation in operation_id for operation in OPERATION_SUFFIXES ]) if operation_id and not valid_operation: continue # Get resource name and path and add it to the list # for the first occurring instance of the resource name = operation_id.split("_")[0] if name not in self._resources: permission_imports_loaded = False self._resources[name] = { "path": path[1:].split("/")[0], "imports": [], "custom_imports": [], "has_methods": False, "filter_lengths": {} } definition = None head_component = None permissions = io.get("x-aor-permissions", []) if self.permissions else None if not permission_imports_loaded and self.permissions: permission_imports_loaded = True self._resources[name]["custom_imports"].append( CUSTOM_IMPORTS["permissions"] ) # Get the correct definition/head_component/component suffix per # verb based on the operation. _create = "create" in operation_id _update = "update" in operation_id if "read" in operation_id: definition, title = self._get_definition_from_ref( definition=io["responses"]["200"]["schema"] ) self._resources[name]["title"] = title or name head_component = "show" # Add show component imports if "Show" not in self._resources[name]["imports"]: self._resources[name]["imports"].append("Show") self._resources[name]["imports"].append( "SimpleShowLayout") elif "list" in operation_id: definition, title = self._get_definition_from_ref( definition=io["responses"]["200"]["schema"]["items"] ) head_component = "list" # Add list component imports if "List" not in self._resources[name]["imports"]: self._resources[name]["imports"].append("List") self._resources[name]["imports"].append("Datagrid") filters = [] filter_imports = [] # Get all method filters for the list component. for parameter in io.get("parameters", []): # If the parameter is a reference, get the actual parameter. if "$ref" in parameter: ref = parameter["$ref"].split("/")[2] param = self.parser.specification["parameters"][ref] else: param = parameter # Filters are only in the query string and their type needs # to be a supported component. if param["in"] == "query" \ and param["type"] in COMPONENT_MAPPING["Input"]\ and not param.get("x-admin-on-rest-exclude", False): # Get component based on the explicit declaration or just the type. declared_input = param.get("x-aor-filter", None) related_input = param.get("x-related-info", None) _type = param["type"] relation = None if declared_input: _range = "-range" if declared_input.get( "range", False) else "" _type = "{_type}{_range}".format( _type=declared_input["format"], _range=_range ) elif related_input: _type = "relation" relation = { "component": COMPONENT_MAPPING["Input"]["enum"], "resource": related_input["rest_resource_name"], "text": related_input.get("label", None) } if relation["component"] not in filter_imports: filter_imports.append(relation["component"]) component = COMPONENT_MAPPING["Input"][_type] # Add props if needed. props = None if _type in PROPS_MAPPING["Input"]: props = PROPS_MAPPING["Input"][_type] # Add component to filter imports if not there. if component not in filter_imports: filter_imports.append(component) source = param["name"] label = source.replace("_", " ").title() _min = param.get("minLength", None) _max = param.get("maxLength", None) if _min or _max: self._resources[name]["filter_lengths"][source] = { "min_length": _min, "max_length": _max } # Handle Array filter types finally! array_validation = param["items"]["type"] \ if _type == "array" else None filters.append({ "source": source, "label": label, "title": label.replace(" ", ""), "component": component, "relation": relation, "props": props, "array": array_validation }) if filters: self._resources[name]["filters"] = { "filters": filters, "imports": filter_imports } elif _create or _update: for parameter in io.get("parameters", []): # If the parameter is a reference, get the actual parameter. if "$ref" in parameter: ref = parameter["$ref"].split("/")[2] param = self.parser.specification["parameters"][ref] else: param = parameter # Grab the body parameter as the create definition if param["in"] == "body": definition, title = self._get_definition_from_ref( definition=param["schema"] ) head_component = "create" if _create else "edit" # Add SimpleForm and the head component to the imports if "SimpleForm" not in self._resources[name]["imports"]: self._resources[name]["imports"].append("SimpleForm") the_import = head_component.title() if the_import not in self._resources[name]["imports"]: self._resources[name]["imports"].append(the_import) elif "delete" in operation_id: self._resources[name]["remove"] = { "permissions": permissions } if head_component and definition: # Toggle to be included in AOR if it has a single method. self._resources[name]["has_methods"] = True self._get_resource_from_definition( resource_name=name, head_component=head_component, definition=definition, permissions=permissions ) @staticmethod def generate_js_file(filename, context): """ Generate a js file from the given specification. :param filename: The name of the template file. :param context: Context to be passed. :return: str """ return render_to_string(filename, context) @staticmethod def add_additional_file(filename): """ Add an additional file, that does not require context, to the generated admin. :return: str """ return render_to_string(filename, {}) def aor_generation(self): click.secho("Generating App.js component file...", fg="green") with open(os.path.join(self.output_dir, "App.js"), "w") as f: data = self.generate_js_file( filename="App.js", context={ "title": self.module_name, "rest_server_url": self.rest_server_url, "resources": self._resources, "supported_components": SUPPORTED_COMPONENTS, "add_permissions": self.permissions }) f.write(data) if self.verbose: print(data) click.secho("Generating Menu.js component file...", fg="green") with open(os.path.join(self.output_dir, "Menu.js"), "w") as f: data = self.generate_js_file( filename="Menu.js", context={ "resources": self._resources }) f.write(data) if self.verbose: print(data) click.secho("Generating resource component files...", fg="blue") resource_dir = self.output_dir + "/resources" if not os.path.exists(resource_dir): os.makedirs(resource_dir) for name, resource in self._resources.items(): title = resource.get("title", None) if title: click.secho("Generating {}.js file...".format( title), fg="green") with open(os.path.join(resource_dir, "{}.js".format(title)), "w") as f: data = self.generate_js_file( filename="Resource.js", context={ "name": title, "resource": resource, "supported_components": SUPPORTED_COMPONENTS, "add_permissions": self.permissions } ) f.write(data) if self.verbose: print(data) click.secho("Generating Filter files for resources...", fg="blue") filter_dir = self.output_dir + "/filters" if not os.path.exists(filter_dir): os.makedirs(filter_dir) for name, resource in self._resources.items(): if resource.get("filters", None) is not None: title = resource.get("title", None) if title: click.secho("Generating {}Filter.js file...".format( title), fg="green") with open(os.path.join(filter_dir, "{}Filter.js".format(title)), "w") as f: data = self.generate_js_file( filename="Filters.js", context={ "title": title, "filters": resource["filters"] }) f.write(data) if self.verbose: print(data) click.secho("Adding basic swagger rest server file...", fg="cyan") with open(os.path.join(self.output_dir, "swaggerRestServer.js"), "w") as f: data = self.generate_js_file( filename="swaggerRestServer.js", context={ "resources": self._resources } ) f.write(data) if self.verbose: print(data) if self.permissions: path_dir = self.output_dir + "/auth" if not os.path.exists(path_dir): os.makedirs(path_dir) with open(os.path.join(path_dir, "PermissionsStore.js"), "w") as f: data = self.generate_js_file( filename="PermissionsStore.js", context={ "resources": self._resources, "supported_components": SUPPORTED_COMPONENTS } ) f.write(data) if self.verbose: print(data) # Generate additional Files for _dir, files in ADDITIONAL_FILES.items(): if _dir != "root": path_dir = "{}/{}".format(self.output_dir, _dir) if not os.path.exists(path_dir): os.makedirs(path_dir) else: path_dir = self.output_dir for file in files: click.secho("Adding {} file...".format(file), fg="cyan") with open(os.path.join(path_dir, file), "w") as f: data = self.add_additional_file(file) f.write(data) if self.verbose: print(data)