import os from collections import defaultdict from django.conf import settings as django_settings from django.template.loader import render_to_string from django.utils.module_loading import import_string from export_app import settings try: input = raw_input except NameError: pass # from http://stackoverflow.com/questions/3203286/how-to-create-a-read-only-class-property-in-python#3203659 class classproperty(object): def __init__(self, getter): self.getter = getter def __get__(self, instance, owner): return self.getter(owner) class BaseAdapter(object): FIELD_TYPE_MAPPING = {} DEFAULT_MAPPING = None requires_fields = False works_with = 'serializer' dasherize = False def __init__(self): assert not self.requires_fields or self.works_with != 'viewset', """ Field information is provided by a serializer. Your adapter requires field information (requires_fields = True) and therefore also needs to "works_with" serializer. If you also require information from the viewset, please update "works_with" to "both" otherwise use "works_with = 'serializer'" """ @classproperty def field_type_mapping(cls): default = cls.FIELD_TYPE_MAPPING default.update(settings.FIELD_TYPE_MAPPING) rv = defaultdict(lambda: cls.default_mapping) for k, v in default.items(): rv[k] = v return rv @classproperty def default_mapping(cls): rv = settings.DEFAULT_MAPPING if rv is None: return cls.DEFAULT_MAPPING return rv def write_to_file(self, application_name, model_name, context, force_overwrite=False): raise NotImplementedError("You need to implement your Adapter") def create_dirs(self, *args): for directory in args: if not os.path.exists(directory): try: os.makedirs(directory) except FileExistsError: # someone created the directory in the meantime # tis can happen in parrallel tests pass def write_file(self, context, target_dir, filename, template, overwrite='confirm'): target_file = os.path.join(target_dir, filename) if os.path.exists(target_file): if not overwrite: return if overwrite == 'confirm': answer = 'anything else' while answer.lower() not in ('', 'yes', 'y', 'no', 'n'): answer = input('{} already exists, do you want to overwrite it? [y/N] '.format( target_file )) if answer.lower() in ('', 'no', 'n'): return with open(target_file, 'w', encoding='utf-8') as f: output = render_to_string(template, context) f.write(output) def write_files(self, context, files): self.create_dirs(*[file[0] for file in files]) for file in files: self.write_file(context, *file) def rebuild_index(self): pass @classmethod def adapt_fields_for_model(cls, fields, relationships): return fields, relationships class EmberAdapter(BaseAdapter): FIELD_TYPE_MAPPING = { 'BooleanField': 'boolean', 'NullBooleanField': None, 'IntegerField': 'number', 'FloatField': 'number', 'DecimalField': 'number', 'ListField': None, 'DictField': None, 'JSONField': None, 'PrimaryKeyRelatedField': 'belongsTo', 'SlugRelatedField': 'belongsTo', 'ManyRelatedField': 'hasMany', 'DateField': 'nullable', 'DateTimeField': 'nullable', 'TimeField': 'nullable', 'DurationField': 'duration', 'UUIDField': 'nullable', } DEFAULT_MAPPING = 'string' requires_fields = True dasherize = True base_template_name = 'export_app/ember_model_base.js' template_name = 'export_app/ember_model.js' test_template_name = 'export_app/ember_model_test.js' dynamic_template_name = 'export_app/dynamic_model.js' def write_to_file(self, application_name, model_name, context, force_overwrite=False): context['application_name'] = context['application_name'].replace('_', '-') context['updir'] = context.get('updir', 1) base_target_dir = os.path.join(django_settings.BASE_DIR, settings.FRONT_APPLICATION_PATH, 'app', 'models', 'base', application_name.replace('_', '-')) target_dir = os.path.join(django_settings.BASE_DIR, settings.FRONT_APPLICATION_PATH, 'app', 'models', application_name.replace('_', '-')) test_target_dir = os.path.join(django_settings.BASE_DIR, settings.FRONT_APPLICATION_PATH, 'tests', 'unit', 'models', application_name.replace('_', '-')) filename = '{}.js'.format(model_name).replace('_', '-') test_filename = '{}-test.js'.format(model_name).replace('_', '-') files = [ (base_target_dir, filename, self.base_template_name, True), (target_dir, filename, self.template_name, False), (test_target_dir, test_filename, self.test_template_name, 'confirm' if not force_overwrite else False) ] self.write_files(context, files) class BaseMetadataAdapter(BaseAdapter): works_with = 'viewset' def render(self, data): from rest_framework.renderers import JSONRenderer renderer = JSONRenderer() return renderer.render(data) def get_metadata_from_viewset(self, viewset): if isinstance(viewset, dict): output = viewset else: MetadataClass = import_string(django_settings.REST_FRAMEWORK['DEFAULT_METADATA_CLASS']) output = MetadataClass().determine_metadata(None, viewset) return output def get_json(self, viewset): return self.render(self.get_metadata_from_viewset(viewset)) class MetadataAdapter(BaseMetadataAdapter): def write_to_file(self, application_name, model_name, viewset, force_overwrite=False): target_dir = os.path.join(django_settings.BASE_DIR, settings.FRONT_APPLICATION_PATH, 'data') if not os.path.exists(target_dir): os.makedirs(target_dir) filename = '{}-{}.json'.format(application_name, model_name) with open(os.path.join(target_dir, filename), 'wb') as f: f.write(self.get_json(viewset)) class MetadataES6Adapter(BaseMetadataAdapter): template_name = 'export_app/ember_metadata.js' index_template_name = 'export_app/ember_metadata_index.js' def walk_dir(self, base, ignore_index=False, prefix=''): from inflector import Inflector from django.utils.module_loading import import_string from drf_auto_endpoint.app_settings import settings as auto_settings inflector_language = import_string(auto_settings.INFLECTOR_LANGUAGE) inflector = Inflector(inflector_language) imports = [] for item in os.listdir(base): filename = os.path.join(base, item) if os.path.isdir(filename): imports += self.walk_dir(filename, prefix=os.path.join(prefix, item.replace('_', '-'))) elif item == 'index.js' and ignore_index: continue else: try: base_name, extension = item.rsplit('.', 1) except ValueError: # a file without extension continue if extension != 'js': continue imports.append(( os.path.join(prefix, inflector.pluralize(base_name)), os.path.join(prefix, base_name).replace('/', '_').replace('-', '_'), os.path.join('.', prefix.replace('-', '_'), base_name) )) return imports def rebuild_index(self): MetadataClass = import_string(django_settings.REST_FRAMEWORK['DEFAULT_METADATA_CLASS']) context = {} directory = os.path.join(django_settings.BASE_DIR, settings.FRONT_APPLICATION_PATH, 'app', 'data') context['items'] = self.walk_dir(directory, True) context['root_metadata'] = self.render( MetadataClass().determine_metadata(None, 'APIRootView') ).decode('utf-8') self.write_file(context, directory, 'index.js', self.index_template_name, True) def write_to_file(self, application_name, model_name, viewset, force_overwrite=False): target_dir = os.path.join(django_settings.BASE_DIR, settings.FRONT_APPLICATION_PATH, 'app', 'data', application_name) self.create_dirs(target_dir) filename = '{}.js'.format(model_name) output = self.get_json(viewset) context = { 'json': output.decode('utf-8') } self.write_file(context, target_dir, filename, self.template_name, True) class MobxAxiosAdapter(BaseAdapter): works_with = 'both' requires_fields = True config_template_name = 'export_app/mobxaxios_config.js' base_model_template_name = 'export_app/mobxaxios_base_model.js' model_base_template_name = 'export_app/mobxaxios_model_base.js' model_template_name = 'export_app/mobxaxios_model.js' base_store_template_name = 'export_app/mobxaxios_base_store.js' store_template_name = 'export_app/mobxaxios_store.js' def write_to_file(self, application_name, model_name, context, force_overwrite=False): base_target_dir = os.path.join(django_settings.BASE_DIR, settings.FRONT_APPLICATION_PATH) config_target_dir = os.path.join(base_target_dir, 'config') model_target_dir = os.path.join(base_target_dir, 'models') model_base_target_dir = os.path.join(model_target_dir, 'base') store_target_dir = os.path.join(base_target_dir, 'stores') filename = '{}{}.js'.format(application_name, model_name) files = [ (config_target_dir, 'axios-config.js', self.config_template_name, False), (store_target_dir, '_base.js', self.base_store_template_name, True), (store_target_dir, filename, self.store_template_name, 'confirm' if not force_overwrite else False), (model_base_target_dir, '_base.js', self.base_model_template_name, True), (model_base_target_dir, filename, self.model_base_template_name, True), (model_target_dir, filename, self.model_template_name, False) ] self.write_files(context, files) class Angular2Adapter(BaseAdapter): FIELD_TYPE_MAPPING = { 'BooleanField': 'boolean', 'NullBooleanField': 'boolean', 'IntegerField': 'number', 'FloatField': 'number', 'DecimalField': 'number', 'ListField': 'any', 'DictField': 'any', 'JSONField': 'any', 'PrimaryKeyRelatedField': 'belongsTo', 'ManyRelatedField': 'hasMany', } DEFAULT_MAPPING = 'string' requires_fields = True base_template_name = 'export_app/angular2_model_base.ts' template_name = 'export_app/angular2_model.ts' service_base_template_name = 'export_app/angular2_service_base.ts' service_template_name = 'export_app/angular2_service.ts' def write_to_file(self, application_name, model_name, context, force_overwrite=False): file_model_name = model_name context['application_name'] = context['application_name'] context['updir'] = context.get('updir', 1) target_dir = os.path.join(django_settings.BASE_DIR, settings.FRONT_APPLICATION_PATH, 'app', 'models', application_name) base_filename = '{}.base.ts'.format(file_model_name) filename = '{}.ts'.format(file_model_name) service_base_filename = '{}.service.base.ts'.format(file_model_name) service_filename = '{}.service.ts'.format(file_model_name) files = [ (target_dir, base_filename, self.base_template_name, True), (target_dir, service_base_filename, self.service_base_template_name, True), (target_dir, filename, self.template_name, False if force_overwrite else 'confirm'), (target_dir, service_filename, self.service_template_name, False if force_overwrite else 'confirm'), ] self.write_files(context, files)