h(Prototyper)
20 | })
21 |
--------------------------------------------------------------------------------
/frontend/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | import Home from './components/Home'
4 | import Admin from './components/admin'
5 | import AdminEditor from './components/admin/Editor'
6 | import AppsModels from './components/appsmodels'
7 | import ModelEditor from './components/appsmodels/ModelEditor'
8 | import Build from './components/build'
9 | import BuildSettings from './components/buildsettings'
10 | import Settings from './components/settings'
11 | import Plugins from './components/plugins'
12 |
13 | Vue.use(Router)
14 |
15 | export default new Router({
16 | routes: [
17 | {
18 | path: '/',
19 | name: 'home',
20 | component: Home
21 | },
22 | {
23 | path: '/settings/',
24 | name: 'settings',
25 | component: Settings
26 | },
27 | {
28 | path: '/buildsettings/',
29 | name: 'buildsettings',
30 | component: BuildSettings
31 | },
32 | {
33 | path: '/apps/',
34 | name: 'appsmodels',
35 | component: AppsModels
36 | },
37 | {
38 | path: '/apps/:app/:model',
39 | name: 'model',
40 | component: ModelEditor
41 | },
42 | {
43 | path: '/apps/:app/:model/:field',
44 | name: 'model-field',
45 | component: ModelEditor
46 | },
47 | {
48 | path: '/admin/',
49 | name: 'admin',
50 | component: Admin
51 | },
52 | {
53 | path: '/admin/:app/:model',
54 | name: 'admin-edit',
55 | component: AdminEditor
56 | },
57 | {
58 | path: '/plugins/',
59 | name: 'plugins',
60 | component: Plugins
61 | },
62 | {
63 | path: '/build/',
64 | name: 'build',
65 | component: Build
66 | },
67 | ]
68 | })
69 |
--------------------------------------------------------------------------------
/frontend/src/store.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import Vue from 'vue'
3 | import API from './backend'
4 | import {DJANGO_CONTRIB_APPS} from './django/apps'
5 | import {guess_type} from './django/guess'
6 |
7 |
8 | function firstUpCase(s) {
9 | return s[0].toUpperCase() + s.substr(1);
10 | }
11 |
12 |
13 | export var store = {
14 | project: PROJECT_DATA, // Global var (comes from html)
15 |
16 | save() {
17 | return API.save(this.project)
18 | },
19 |
20 | app_get(name) {
21 | return _.find(this.project.apps, {name})
22 | },
23 | app_add(name) {
24 | this.project.apps.push({
25 | name,
26 | external: false,
27 | models: [],
28 | })
29 | },
30 | app_delete(name) {
31 | let ind = _.findIndex(this.project.apps, {name: name})
32 | Vue.delete(this.project.apps, ind)
33 | },
34 | apps_add_django(name) {
35 | this.app_add(name)
36 | let app = this.app_get(name)
37 | app.external = true
38 | let models = DJANGO_CONTRIB_APPS[name]
39 | for (let i = 0; i < models.length; i++) {
40 | this.models_add(name, models[i])
41 | }
42 | },
43 |
44 | models_get(app_name, name) {
45 | let app = this.app_get(app_name)
46 | name = firstUpCase(name)
47 | return _.find(app.models, {name})
48 | },
49 | models_add(app_name, name) {
50 | name = firstUpCase(name)
51 | let app = this.app_get(app_name)
52 | app.models.push({
53 | name: name,
54 | fields: [],
55 | admin: {'generate': true},
56 | })
57 | },
58 | models_delete(app_name, name) {
59 | let app = this.app_get(app_name)
60 | let ind = _.findIndex(app.models, {name: name})
61 | Vue.delete(app.models, ind)
62 | },
63 | models_keys(skip_external = false) {
64 | let result = []
65 | _.each(_.sortBy(store.project.apps, ['name']), (app) => {
66 | if (skip_external && app.external)
67 | return
68 | _.each(app.models, (model) => result.push(app.name + '.' + model.name))
69 | })
70 | return result
71 | },
72 |
73 | fields_get(model, name) {
74 | return _.find(model.fields, {name})
75 | },
76 |
77 | fields_add(model_fields, name) {
78 | let res = guess_type(name, this)
79 | let fld = {
80 | name,
81 | 'attrs': {},
82 | 'type': res.type,
83 | 'relation': res.relation,
84 | }
85 | model_fields.push(fld)
86 | return fld
87 | },
88 |
89 | fields_delete(model, name){
90 | let ind = _.findIndex(model.fields, {name: name})
91 | console.info(name)
92 | console.info(ind)
93 | console.info(model.fields)
94 | Vue.delete(model.fields, ind)
95 | },
96 |
97 | plugins_install(plugin) {
98 | this.project.plugins.push(plugin)
99 | let plugin_apps = _.get(plugin, 'apps', [])
100 | _.each(plugin_apps, (a) => {
101 | this.app_add(a.name)
102 | let app = this.app_get(a.name)
103 | app.external = true
104 | let models = _.get(a, 'models', [])
105 | for (let i = 0; i < models.length; i++) {
106 | this.models_add(a.name, models[i])
107 | }
108 | })
109 | },
110 |
111 | plugins_get(name) {
112 | return _.find(this.project.plugins, {name})
113 | },
114 |
115 | plugins_delete(name) {
116 | let ind = _.findIndex(this.project.plugins, {name})
117 | if (ind == -1)
118 | alert('Cannot find plugin: ' + name)
119 |
120 | // cleaning plugin apps:
121 | let plugin = this.plugins_get(name)
122 | let plugin_apps = _.get(plugin, 'apps', [])
123 | _.each(plugin_apps, a => this.app_delete(a.name))
124 |
125 | Vue.delete(this.project.plugins, ind)
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/frontend/src/validation.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 |
3 | export function validate(project) {
4 | let results = []
5 | _.each(project.apps, (app) => check_app(app, results))
6 | return results
7 | }
8 |
9 | function check_app(app, results) {
10 | _.each(app.models, (model) => check_model(app, model, results))
11 | }
12 |
13 | function check_model(app, model, results) {
14 | _.each(model.fields, (field) => check_field(app, model, field, results))
15 | }
16 |
17 | function check_field(app, model, field, results) {
18 | let msgs = []
19 | if (field.type == 'DecimalField') {
20 | if (!field.attrs.max_digits) {
21 | msgs.push('max_digits is required')
22 | }
23 | if (!field.attrs.decimal_places) {
24 | msgs.push('decimal_places is required')
25 | }
26 | }
27 | else if (field.type == 'FileField' || field.type == 'ImageField') {
28 | if (!field.attrs.upload_to) {
29 | msgs.push('upload_to is required')
30 | }
31 | }
32 | else if (field.type == 'ForeignKey' || field.type == 'ManyToManyField' || field.type == 'OneToOneField') {
33 | if (!field.relation) {
34 | msgs.push('relation is required')
35 | }
36 | }
37 |
38 | // Final:
39 | if (msgs.length > 0) {
40 | let field_key = `${app.name}.${model.name}.${field.name}`
41 | results.push({type:'field', 'id': field_key, message:field.type + ': ' + msgs.join(', ')})
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/frontend/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack = require('webpack')
3 | var BundleTracker = require('webpack-bundle-tracker');
4 |
5 |
6 |
7 | var dist_dir = 'dist';
8 | var dev_server_addr = 'localhost';
9 | var dev_server_port = 9000;
10 |
11 | module.exports = {
12 | entry: './src/main.js',
13 | output: {
14 | path: path.resolve(__dirname, './dist'),
15 | publicPath: '/static/',
16 | filename: 'build.js'
17 | },
18 | plugins: [
19 | new BundleTracker({filename: './webpack-stats.json'}),
20 | ],
21 | module: {
22 | rules: [
23 | {
24 | test: /\.css$/,
25 | use: [
26 | 'vue-style-loader',
27 | 'css-loader'
28 | ],
29 | },
30 | {
31 | test: /\.scss$/,
32 | use: [
33 | 'vue-style-loader',
34 | 'css-loader',
35 | 'sass-loader'
36 | ],
37 | },
38 | {
39 | test: /\.sass$/,
40 | use: [
41 | 'vue-style-loader',
42 | 'css-loader',
43 | 'sass-loader?indentedSyntax'
44 | ],
45 | },
46 | {
47 | test: /\.vue$/,
48 | loader: 'vue-loader',
49 | options: {
50 | loaders: {
51 | // Since sass-loader (weirdly) has SCSS as its default parse mode, we map
52 | // the "scss" and "sass" values for the lang attribute to the right configs here.
53 | // other preprocessors should work out of the box, no loader config like this necessary.
54 | 'scss': [
55 | 'vue-style-loader',
56 | 'css-loader',
57 | 'sass-loader'
58 | ],
59 | 'sass': [
60 | 'vue-style-loader',
61 | 'css-loader',
62 | 'sass-loader?indentedSyntax'
63 | ]
64 | }
65 | // other vue-loader options go here
66 | }
67 | },
68 | {
69 | test: /\.js$/,
70 | loader: 'babel-loader',
71 | exclude: /node_modules/
72 | },
73 | {
74 | test: /\.(png|jpg|gif|svg)$/,
75 | loader: 'file-loader',
76 | options: {
77 | name: '[name].[ext]?[hash]'
78 | }
79 | }
80 | ]
81 | },
82 | resolve: {
83 | alias: {
84 | 'vue$': 'vue/dist/vue.esm.js'
85 | },
86 | extensions: ['*', '.js', '.vue', '.json']
87 | },
88 | devServer: {
89 | port: dev_server_port,
90 | host: dev_server_addr,
91 | headers: { "Access-Control-Allow-Origin": "*" },
92 | //publicPath: "http://localhost:9000/dist/",
93 | ///------
94 | historyApiFallback: true,
95 | noInfo: true,
96 | overlay: true
97 | },
98 | performance: {
99 | hints: false
100 | },
101 | devtool: '#eval-source-map'
102 | }
103 |
104 | if (process.env.NODE_ENV === 'production') {
105 | module.exports.devtool = '#source-map'
106 | // http://vue-loader.vuejs.org/en/workflow/production.html
107 | module.exports.plugins = (module.exports.plugins || []).concat([
108 | new webpack.DefinePlugin({
109 | 'process.env': {
110 | NODE_ENV: '"production"'
111 | }
112 | }),
113 | new webpack.optimize.UglifyJsPlugin({
114 | sourceMap: true,
115 | compress: {
116 | warnings: false
117 | }
118 | }),
119 | new webpack.LoaderOptionsPlugin({
120 | minimize: true
121 | })
122 | ])
123 | }
124 | else
125 | {
126 | // module.exports.entry.push('webpack-dev-server/client?http://' + dev_server_addr + ':' + dev_server_port);
127 | // module.exports.entry.push('webpack/hot/only-dev-server');
128 | module.exports.output['publicPath'] = 'http://' + dev_server_addr + ':' + dev_server_port + '/' + dist_dir + '/';
129 | // module.exports.plugins.push(new webpack.HotModuleReplacementPlugin());
130 | // module.exports.plugins.push(new webpack.NoEmitOnErrorsPlugin()); // don't reload if there is an error
131 | }
132 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | from prototyper.cli import main
4 |
5 | if __name__ == "__main__":
6 | # This file is used only development mode, in distrbution we have a `prototyper` command
7 | os.environ['PROTOTYPER_DEV'] = os.environ.get('PROTOTYPER_DEV', 'yes')
8 | main()
9 |
--------------------------------------------------------------------------------
/prototyper/__init__.py:
--------------------------------------------------------------------------------
1 | from .version import VERSION
2 | from .utils.inspection import inspect
3 |
--------------------------------------------------------------------------------
/prototyper/build/__init__.py:
--------------------------------------------------------------------------------
1 | from .main import run_build
2 |
--------------------------------------------------------------------------------
/prototyper/build/base.py:
--------------------------------------------------------------------------------
1 | import os
2 | import traceback
3 | import tempfile
4 | import shutil
5 | from .log import get_logger
6 |
7 |
8 | class Build(object):
9 | def __init__(self, project):
10 | self.details = project.load()
11 | self.project = project
12 | self.temp_folder = tempfile.mkdtemp(prefix='djprototyper')
13 | self.build_path = os.path.join(self.temp_folder, project.name)
14 | os.mkdir(self.build_path)
15 |
16 | self.settings_pckg_path = self.build_path
17 | if self.is_settings_py_separate():
18 | self.settings_pckg_path = os.path.join(self.settings_pckg_path, project.name)
19 |
20 | self.logger = get_logger()
21 | self.success = False # succesful build finished
22 |
23 | def log(self, message):
24 | self.logger.info(message)
25 |
26 | def save(self):
27 | self.log('Cleaning previous build {}'.format(self.project.path))
28 | for i in os.listdir(self.project.path):
29 | if i == '.djangoprototyper':
30 | continue
31 | name = os.path.join(self.project.path, i)
32 | if os.path.isdir(name):
33 | shutil.rmtree(name)
34 | else:
35 | os.remove(name)
36 |
37 | for f in os.listdir(self.temp_folder):
38 | self.log('Saving {}'.format(f))
39 | src = os.path.join(self.temp_folder, f)
40 | dst = os.path.join(self.project.path, f)
41 | shutil.move(src, dst)
42 |
43 | def cleanup(self):
44 | if os.path.exists(self.temp_folder):
45 | self.log('Cleaning: {}'.format(self.temp_folder))
46 | shutil.rmtree(self.temp_folder)
47 |
48 | def is_settings_py_separate(self):
49 | return self.details['build_settings'].get('settings_path', 'separate') == 'separate'
50 |
51 |
52 | class BuildStage(object):
53 | def __init__(self, build):
54 | self.build = build
55 |
56 | def run(self):
57 | raise NotImplementedError('please implement run')
58 |
59 | def log(self, message):
60 | self.build.log(message)
61 |
62 | def settings_module(self, module):
63 | "returns py module path based on settings_path build setting"
64 | if self.build.is_settings_py_separate():
65 | return '{0}.{1}'.format(self.build.project.name, module)
66 | return module
67 |
68 |
69 | def pipeline(build, plugins, stages):
70 | for cls in stages:
71 | assert issubclass(cls, BuildStage)
72 | try:
73 | build.log('Running ' + cls.__name__)
74 | stage = cls(build)
75 | stage.run()
76 | except Exception as e:
77 | build.logger.error(traceback.format_exc())
78 | return False
79 |
80 | for plug in plugins:
81 | plug.set_build(build)
82 | plug.on_build_complete()
83 |
84 | build.save()
85 | build.success = True
86 | return True
87 |
--------------------------------------------------------------------------------
/prototyper/build/log.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import traceback
3 | from datetime import datetime
4 |
5 |
6 | class LogHandler(logging.Handler):
7 | def __init__(self, logger):
8 | super(LogHandler, self).__init__()
9 | self.logger = logger
10 | self.start_ts = 0
11 |
12 | def emit(self, record):
13 | if self.start_ts == 0:
14 | self.start_ts = record.created
15 | print ('%.5f %s' % (record.created - self.start_ts, record.msg))
16 | self.logger.records.append(record)
17 |
18 |
19 | class Logger(logging.Logger):
20 | def __init__(self):
21 | super(Logger, self).__init__('prototyper', logging.DEBUG)
22 | self.records = []
23 | self.addHandler(LogHandler(self))
24 |
25 | def serialize(self):
26 | result = []
27 | for rec in self.records:
28 | result.append({
29 | 'time': str(datetime.fromtimestamp(rec.created).time()),#.strftime('%H:%m:%s'),
30 | 'level': rec.levelname,
31 | 'message': str(rec.msg),
32 | })
33 | return result
34 |
35 |
36 | def get_logger():
37 | return Logger()
38 |
--------------------------------------------------------------------------------
/prototyper/build/main.py:
--------------------------------------------------------------------------------
1 | import shutil
2 | import traceback
3 | from django.conf import settings
4 | from .base import Build, BuildStage, pipeline
5 | from . import stages
6 | from ..plugins import load_py_plugins
7 |
8 |
9 | def run_build():
10 | build = Build(settings.PROTOTYPER_PROJECT)
11 | try:
12 | return _run_build(build)
13 | except Exception as e:
14 | build.logger.error(traceback.format_exc())
15 | return build
16 |
17 |
18 | def _run_build(build):
19 | plugins = _init_py_plugins(build)
20 | pipeline(build, plugins, [
21 | stages.FirstStage,
22 | stages.AppsPackages,
23 | stages.SettingsStage,
24 | stages.UrlsStage,
25 | stages.WsgiStage,
26 | stages.ModelsStage,
27 | stages.AdminStage,
28 | stages.RequirementsStage,
29 | ])
30 | build.cleanup()
31 | return build
32 |
33 |
34 | def _init_py_plugins(build):
35 | build.log('Loading plugins...')
36 | plugins = []
37 | for plugin in load_py_plugins():
38 | plugins.append(plugin)
39 | build.log(str(plugins))
40 | return plugins
41 |
--------------------------------------------------------------------------------
/prototyper/build/stages/__init__.py:
--------------------------------------------------------------------------------
1 | from .core import FirstStage, AppsPackages
2 | from .settings import SettingsStage
3 | from .urls import UrlsStage
4 | from .wsgi_app import WsgiStage
5 | from .admin import AdminStage
6 | from .models import ModelsStage
7 | from .requirements import RequirementsStage
8 |
--------------------------------------------------------------------------------
/prototyper/build/stages/admin.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from ..base import BuildStage
4 |
5 |
6 | class AdminStage(BuildStage):
7 | def run(self):
8 | for app in self.build.details['apps']:
9 | if app['external'] is False:
10 | self._handle_app(app)
11 |
12 | def _handle_app(self, app):
13 | contents = ['from django.contrib import admin', 'from .models import *']
14 |
15 | for model in app['models']:
16 | if model['admin']['generate']:
17 | model['admin'].pop('generate')
18 | contents.extend(['', ''])
19 | contents.extend(self._handle_model(app, model))
20 |
21 | contents.append('') # empty line
22 |
23 | admin_py = Path(self.build.build_path) / app['name'] / 'admin.py'
24 | admin_py.write_text('\n'.join(contents))
25 |
26 | def _handle_model(self, app, model):
27 | admin = [
28 | "@admin.register(%s)" % model['name'],
29 | "class %sAdmin(admin.ModelAdmin):" % model['name'],
30 | ]
31 | lines = []
32 | if model['admin']:
33 | for name, attr in model['admin'].items():
34 | if attr['fields']:
35 | if attr['single']:
36 | lines.append(" {0} = '{1}'".format(name, attr['fields'][0]))
37 | else:
38 | fields = ["'%s'" % f for f in attr['fields']]
39 | lines.append(" {0} = [{1}]".format(name, ', '.join(fields)))
40 | if not lines:
41 | lines.append(" pass")
42 | return admin + lines
43 |
--------------------------------------------------------------------------------
/prototyper/build/stages/core.py:
--------------------------------------------------------------------------------
1 | from ..base import BuildStage
2 | from pathlib import Path
3 |
4 |
5 | MANAGE_PY = """#!/usr/bin/env python
6 | import os
7 | import sys
8 |
9 | if __name__ == "__main__":
10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{0}")
11 | try:
12 | from django.core.management import execute_from_command_line
13 | except ImportError as exc:
14 | raise ImportError(
15 | "Couldn't import Django. Are you sure it's installed and "
16 | "available on your PYTHONPATH environment variable? Did you "
17 | "forget to activate a virtual environment?"
18 | ) from exc
19 | execute_from_command_line(sys.argv)
20 | """
21 |
22 |
23 | class FirstStage(BuildStage):
24 | def run(self):
25 | root = Path(self.build.build_path)
26 | manage_py = root / 'manage.py'
27 | manage_py.touch(0o755)
28 | contents = MANAGE_PY.format(self.settings_module('settings'))
29 | manage_py.write_text(contents)
30 |
31 |
32 | class AppsPackages(BuildStage):
33 | def run(self):
34 | root = Path(self.build.build_path)
35 | for app in self.build.details['apps']:
36 | if not app['external']:
37 | app_pkg = root / app['name']
38 | app_pkg.mkdir()
39 | (app_pkg / '__init__.py').touch()
40 |
--------------------------------------------------------------------------------
/prototyper/build/stages/models.py:
--------------------------------------------------------------------------------
1 | from ..base import BuildStage
2 | from pathlib import Path
3 |
4 |
5 | class codelines(list):
6 | def __init__(self, initial=None):
7 | super(codelines, self).__init__(initial or [])
8 |
9 | def extend_indent(self, lines, indent=1):
10 | spaces = ' ' * (4 * indent)
11 | for l in lines:
12 | self.append(spaces + l)
13 |
14 |
15 | def camel_to_spaces(s):
16 | import re
17 | return re.sub("([a-z])([A-Z])", "\g<1> \g<2>", s)
18 |
19 |
20 | def code_string(s):
21 | q = "'" in s and '"' or "'"
22 | return q + s + q
23 |
24 |
25 | class ModelsStage(BuildStage):
26 | def run(self):
27 | self.use_ugettext = self.build.details['build_settings'].get('ugettext_lazy', True)
28 | for app in self.build.details['apps']:
29 | if not app['external']:
30 | self._handle_app(app)
31 |
32 | def _handle_app(self, app):
33 | mdodels_py = Path(self.build.build_path) / app['name'] / 'models.py'
34 | contents = ['from django.db import models']
35 | if self.use_ugettext:
36 | contents.append('from django.utils.translation import ugettext_lazy as _')
37 |
38 | self._inheritance_imports(app, contents)
39 |
40 | for model in app['models']:
41 | contents.extend(['', ''])
42 | model_lines = ModelBuilder(self, app, model)
43 | contents.extend(model_lines)
44 |
45 | contents.append('') # last empty line
46 |
47 | # self.log('\n'.join(contents))
48 |
49 | mdodels_py.write_text('\n'.join(contents))
50 |
51 | def _inheritance_imports(self, app, contents):
52 | all_classes = set()
53 | for model in app['models']:
54 | for m in model.get('inheritance', []):
55 | all_classes.add(m)
56 | for cls in sorted(all_classes):
57 | app, model = cls.split('.')
58 | contents.append('from %s.models import %s' % (app, model))
59 |
60 |
61 | class ModelBuilder(codelines):
62 | def __init__(self, stage, app, model):
63 | super(ModelBuilder, self).__init__()
64 | self.stage, self.app, self.model = stage, app, model
65 | self._create()
66 |
67 | def _create(self):
68 | self.append('class %s(%s):' % (self.model['name'], self._inheritance()))
69 |
70 | for field in self.model['fields']:
71 | self.extend_indent(self._handle_field(field))
72 |
73 | self.extend_indent(self._handle_meta())
74 | self.extend_indent([''])
75 | self.extend_indent(self._handle_str_output())
76 |
77 | if len(self) == 1:
78 | self.append(' pass')
79 |
80 | def _inheritance(self):
81 | classes = self.model.get('inheritance', [])
82 | if not classes:
83 | return 'models.Model'
84 | return ', '.join([i.split('.')[-1] for i in classes])
85 |
86 | def _handle_field(self, field):
87 | line = FieldBuilder(self, field).render()
88 | return [line]
89 |
90 | def _handle_meta(self):
91 | lines = codelines([''])
92 | lines.append('class Meta:')
93 |
94 | v_name = camel_to_spaces(self.model['name']).lower()
95 | v_name_plural = v_name
96 | if v_name_plural.endswith('y'):
97 | v_name_plural = v_name_plural[:-1] + 'ies'
98 | elif not v_name_plural.endswith('s'):
99 | v_name_plural += 's'
100 | lines.extend_indent([
101 | "verbose_name = %s" % self._trans_str(v_name),
102 | "verbose_name_plural = %s" % self._trans_str(v_name_plural),
103 | ])
104 |
105 | return lines
106 |
107 | def _handle_str_output(self):
108 | "Renders the __str__ method"
109 | if len(self.model['fields']) == 0:
110 | return []
111 | non_rel_fields = [i for i in self.model['fields'] if not i['relation']]
112 | if len(non_rel_fields) == 0:
113 | return []
114 |
115 | fld = 'self.' + non_rel_fields[0]['name']
116 | if non_rel_fields[0]['type'] not in ('CharField', 'TextField', 'SlugField', 'EmailField'):
117 | fld = 'str(%s)' % fld
118 |
119 | return [
120 | 'def __str__(self):',
121 | ' return %s' % fld
122 | ]
123 |
124 | def _trans_str(self, s):
125 | "Returns either _('
' or '' based on build settings)"
126 | result = code_string(s)
127 | if self.stage.use_ugettext:
128 | return '_(' + result + ')'
129 | return result
130 |
131 |
132 | ONLY_COMMON_ATTRS = [
133 | 'AutoField', 'BigAutoField', 'BigIntegerField',
134 | 'BinaryField', 'BooleanField',
135 | 'DurationField', 'EmailField', 'FloatField',
136 | 'IntegerField', 'NullBooleanField',
137 | 'PositiveIntegerField', 'PositiveSmallIntegerField',
138 | 'SlugField', 'SmallIntegerField',
139 | 'TextField', 'URLField', 'UUIDField',
140 | ]
141 |
142 |
143 | class FieldBuilder(object):
144 | def __init__(self, model_builder, field):
145 | self.model_builder = model_builder
146 | self.name = field['name']
147 | self.type = field['type']
148 | self.attrs = field['attrs']
149 | self.relation = field['relation']
150 |
151 | def render(self):
152 | if self.type in ONLY_COMMON_ATTRS:
153 | attrs = self._common_attrs()
154 | else:
155 | method = getattr(self, '_attrs_%s' % self.type)
156 | attrs = method()
157 | return '%s = models.%s(%s)' % (self.name, self.type, ', '.join(attrs))
158 |
159 | def _common_attrs(self, verbose_name_kv=False):
160 | attributes = []
161 |
162 | _trans = self.model_builder._trans_str
163 |
164 | verbose_name = _trans(self.name.replace('_', ' '))
165 | if verbose_name_kv:
166 | verbose_name = 'verbose_name=' + verbose_name
167 |
168 | attributes.append(verbose_name)
169 |
170 | if self.attrs.get('primary_key') is True:
171 | attributes.append('primary_key=True')
172 | if self.attrs.get('null') is True:
173 | attributes.append('null=True')
174 | if self.attrs.get('blank') is True:
175 | attributes.append('blank=True')
176 | if self.attrs.get('unique') is True:
177 | attributes.append('unique=True')
178 | if self.attrs.get('db_index') is True:
179 | attributes.append('db_index=True')
180 | if self.attrs.get('editable') is False:
181 | attributes.append('editable=False')
182 | if self.attrs.get('default') is True:
183 | val = code_string(self.attrs['default'])
184 | if self.type in ('BigIntegerField', 'BooleanField', 'FloatField', 'IntegerField', 'IntegerField', 'NullBooleanField', 'PositiveIntegerField', 'PositiveSmallIntegerField', 'SmallIntegerField'):
185 | val = self.attrs['default']
186 | attributes.append('default=%s' % val)
187 | if self.attrs.get('help_text'):
188 | attributes.append('help_text=%s' % _trans(self.attrs['help_text']))
189 | return attributes
190 |
191 | def _attrs_CharField(self):
192 | build_settings = self.model_builder.stage.build.details['build_settings']
193 | attrs = self._common_attrs()
194 | if self.attrs.get('max_length'):
195 | max_length = self.attrs['max_length']
196 | else:
197 | max_length = build_settings.get('charfield_max_length', 200)
198 | attrs.append('max_length=%s' % max_length)
199 | return attrs
200 |
201 | def _relational(self):
202 | attrs = [code_string(self.relation)]
203 | attrs.extend(self._common_attrs(verbose_name_kv=True))
204 | if self.type in ('OneToOneField', 'ForeignKey'):
205 | attrs.append('on_delete=models.CASCADE')
206 | return attrs
207 |
208 | _attrs_ManyToManyField = _relational
209 | _attrs_OneToOneField = _relational
210 | _attrs_ForeignKey = _relational
211 |
212 | def _attrs_DecimalField(self):
213 | attrs = self._common_attrs()
214 | # TODO: maybe build settings ?
215 | attrs.extend(['max_digits=10', 'decimal_places=2'])
216 | return attrs
217 |
218 | def _attrs_DateTimeField(self):
219 | attrs = self._common_attrs()
220 | if self.attrs.get('auto_now') is True:
221 | attrs.append('auto_now=True')
222 | if self.attrs.get('auto_now_add') is True:
223 | attrs.append('auto_now_add=True')
224 | return attrs
225 |
226 | _attrs_DateField = _attrs_DateTimeField
227 | _attrs_TimeField = _attrs_DateTimeField
228 |
229 | def _attrs_FileField(self):
230 | attrs = self._common_attrs()
231 | if self.attrs.get('upload_to'):
232 | attrs.append('upload_to=%s' % code_string(self.attrs['upload_to']))
233 | return attrs
234 |
235 | _attrs_ImageField = _attrs_FileField
236 |
237 | def _attrs_FilePathField(self):
238 | # TODO
239 | return self._common_attrs()
240 |
241 | def _attrs_GenericIPAddressField(self):
242 | # TODO
243 | return self._common_attrs()
244 |
--------------------------------------------------------------------------------
/prototyper/build/stages/requirements.py:
--------------------------------------------------------------------------------
1 | from ..base import BuildStage
2 | from pathlib import Path
3 | from prototyper.conf import DJANGO_TARGET
4 |
5 | DJ_VER_REQ = '>={maj}.{min},<{maj}.{next}'.format(
6 | maj=DJANGO_TARGET[0],
7 | min=DJANGO_TARGET[1],
8 | next=DJANGO_TARGET[1] + 1,
9 | )
10 |
11 |
12 | class RequirementsStage(BuildStage):
13 | def run(self):
14 | req_txt_file = Path(self.build.build_path) / 'requirements.txt'
15 | requirements = set([
16 | 'Django{0}'.format(DJ_VER_REQ),
17 | 'Pillow',
18 | ])
19 | for plugin in self.build.details['plugins']:
20 | for req in plugin.get('requirements', []):
21 | requirements.add(req)
22 |
23 | lines = '\n'.join(sorted(requirements)) + '\n'
24 | req_txt_file.write_text(lines)
25 |
--------------------------------------------------------------------------------
/prototyper/build/stages/settings.py:
--------------------------------------------------------------------------------
1 | from ..base import BuildStage
2 | from pathlib import Path
3 | from prototyper.conf import DJANGO_TARGET
4 | from django.core.management.utils import get_random_secret_key
5 |
6 |
7 | TPL = """\"\"\"
8 | Django settings for {project_name} project.
9 |
10 | Generated by django-prototyper
11 | https://github.com/vitalik/django-prototyper
12 |
13 | For more information on this file, see
14 | https://docs.djangoproject.com/en/{django_version}/topics/settings/
15 |
16 | For the full list of settings and their values, see
17 | https://docs.djangoproject.com/en/{django_version}/ref/settings/
18 | \"\"\"
19 |
20 | import os
21 |
22 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
23 | BASE_DIR = {BASE_DIR}
24 |
25 |
26 | # Quick-start development settings - unsuitable for production
27 | # See https://docs.djangoproject.com/en/{django_version}/howto/deployment/checklist/
28 |
29 | # SECURITY WARNING: keep the secret key used in production secret!
30 | SECRET_KEY = '{SECRET_KEY}'
31 |
32 | # SECURITY WARNING: don't run with debug turned on in production!
33 | DEBUG = True
34 |
35 | ALLOWED_HOSTS = []
36 |
37 |
38 | # Application definition
39 |
40 | INSTALLED_APPS = [
41 | {INSTALLED_APPS}
42 | ]
43 |
44 | MIDDLEWARE = [
45 | 'django.middleware.security.SecurityMiddleware',
46 | 'django.contrib.sessions.middleware.SessionMiddleware',
47 | 'django.middleware.common.CommonMiddleware',
48 | 'django.middleware.csrf.CsrfViewMiddleware',
49 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
50 | 'django.contrib.messages.middleware.MessageMiddleware',
51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
52 | ]
53 |
54 | ROOT_URLCONF = '{url_conf}'
55 |
56 | TEMPLATES = [
57 | {{
58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
59 | 'DIRS': [],
60 | 'APP_DIRS': True,
61 | 'OPTIONS': {{
62 | 'context_processors': [
63 | 'django.template.context_processors.debug',
64 | 'django.template.context_processors.request',
65 | 'django.contrib.auth.context_processors.auth',
66 | 'django.contrib.messages.context_processors.messages',
67 | ],
68 | }},
69 | }},
70 | ]
71 |
72 | WSGI_APPLICATION = '{wsgi_app}'
73 |
74 |
75 | # Database
76 | # https://docs.djangoproject.com/en/{django_version}/ref/settings/#databases
77 |
78 | DATABASES = {{
79 | 'default': {{
80 | 'ENGINE': 'django.db.backends.sqlite3',
81 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
82 | }}
83 | }}
84 |
85 |
86 | # Password validation
87 | # https://docs.djangoproject.com/en/{django_version}/ref/settings/#auth-password-validators
88 |
89 | AUTH_PASSWORD_VALIDATORS = [
90 | {{
91 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
92 | }},
93 | {{
94 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
95 | }},
96 | {{
97 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
98 | }},
99 | {{
100 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
101 | }},
102 | ]
103 |
104 |
105 | # Internationalization
106 | # https://docs.djangoproject.com/en/{django_version}/topics/i18n/
107 |
108 | LANGUAGE_CODE = 'en-us'
109 |
110 | TIME_ZONE = 'UTC'
111 |
112 | USE_I18N = True
113 |
114 | USE_L10N = True
115 |
116 | USE_TZ = True
117 |
118 |
119 | # Static files (CSS, JavaScript, Images)
120 | # https://docs.djangoproject.com/en/{django_version}/howto/static-files/
121 |
122 | STATIC_URL = '/static/'
123 | """
124 |
125 |
126 | class SettingsStage(BuildStage):
127 | def run(self):
128 | root = Path(self.build.settings_pckg_path)
129 | if not root.exists():
130 | root.mkdir()
131 | (root / '__init__.py').touch()
132 | settings_py = root / 'settings.py'
133 | content = self._get_content()
134 | settings_py.write_text(content)
135 |
136 | def _get_content(self):
137 | proj_name = self.build.project.name
138 | BASE_DIR = 'os.path.dirname(os.path.abspath(__file__))'
139 | if self.build.is_settings_py_separate():
140 | BASE_DIR = 'os.path.dirname({0})'.format(BASE_DIR)
141 | ctx = {
142 | 'project_name': proj_name,
143 | 'SECRET_KEY': get_random_secret_key(),
144 | 'url_conf': self.settings_module('urls'),
145 | 'wsgi_app': self.settings_module('wsgi.application'),
146 | 'INSTALLED_APPS': self._installed_apps_lines(),
147 | 'django_version': '{v[0]}.{v[1]}'.format(v=DJANGO_TARGET),
148 | 'BASE_DIR': BASE_DIR,
149 | }
150 | result = TPL.format(**ctx)
151 | return result
152 |
153 | def _installed_apps_lines(self):
154 | result = [
155 | "'django.contrib.admin',",
156 | "'django.contrib.auth',",
157 | "'django.contrib.contenttypes',",
158 | "'django.contrib.sessions',",
159 | "'django.contrib.messages',",
160 | "'django.contrib.staticfiles',",
161 | "",
162 | ]
163 |
164 | for plugin in self.build.details['plugins']:
165 | apps = plugin.get('apps', [])
166 | for app in apps:
167 | result.append("'{0}',".format(app['name']))
168 |
169 | result.append('')
170 |
171 | for app in self.build.details['apps']:
172 | if not app['external']:
173 | result.append("'{0}',".format(app['name']))
174 | return '\n '.join(result)
175 |
--------------------------------------------------------------------------------
/prototyper/build/stages/urls.py:
--------------------------------------------------------------------------------
1 |
2 | from ..base import BuildStage
3 | from pathlib import Path
4 |
5 | TPL = """\"\"\"Django project URL Configuration
6 |
7 | The `urlpatterns` list routes URLs to views. For more information please see:
8 | https://docs.djangoproject.com/en/2.0/topics/http/urls/
9 | Examples:
10 | Function views
11 | 1. Add an import: from my_app import views
12 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
13 | Class-based views
14 | 1. Add an import: from other_app.views import Home
15 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
16 | Including another URLconf
17 | 1. Import the include() function: from django.urls import include, path
18 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
19 | \"\"\"
20 | from django.contrib import admin
21 | from django.urls import include, path
22 | %(extra_imports)s
23 | urlpatterns = [
24 | path('admin/', admin.site.urls),
25 | %(extra_lines)s
26 | ]
27 | """
28 |
29 |
30 | class UrlsStage(BuildStage):
31 | def run(self):
32 | urls_py = Path(self.build.settings_pckg_path) / 'urls.py'
33 |
34 | extra_lines = []
35 | extra_imports = []
36 |
37 | plugins = self.build.details['plugins']
38 | for plugin in plugins:
39 | urls_conf = plugin.get('urls', {})
40 |
41 | imports = urls_conf.get('imports', [])
42 | extra_imports.extend(imports)
43 |
44 | urls = urls_conf.get('urls', [])
45 | extra_lines.append('')
46 | extra_lines.extend(urls)
47 |
48 | code = TPL % {
49 | 'extra_lines': '\n '.join(extra_lines),
50 | 'extra_imports': '\n'.join(extra_imports),
51 | }
52 | urls_py.write_text(code)
53 |
--------------------------------------------------------------------------------
/prototyper/build/stages/wsgi_app.py:
--------------------------------------------------------------------------------
1 | from ..base import BuildStage
2 | from pathlib import Path
3 |
4 | TPL = """\"\"\"
5 | WSGI config for {0} project.
6 |
7 | It exposes the WSGI callable as a module-level variable named ``application``.
8 |
9 | For more information on this file, see
10 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
11 | \"\"\"
12 |
13 | import os
14 |
15 | from django.core.wsgi import get_wsgi_application
16 |
17 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{0}")
18 |
19 | application = get_wsgi_application()
20 | """
21 |
22 |
23 | class WsgiStage(BuildStage):
24 | def run(self):
25 | wsgi_py = Path(self.build.settings_pckg_path) / 'wsgi.py'
26 | wsgi_py.write_text(TPL.format(self.settings_module('settings')))
27 |
--------------------------------------------------------------------------------
/prototyper/cli.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 | import argparse
4 | import django
5 | from django.conf import settings
6 | from prototyper import VERSION
7 | from prototyper.server import django_configure, run_server
8 |
9 |
10 | def _parse_args():
11 | parser = argparse.ArgumentParser()
12 | parser.add_argument('path_to_project')
13 | parser.add_argument('--build', action='store_true')
14 | parser.add_argument('--bind', help='HOST:PORT to bind http server on. example --bind=0.0.0.0:8000')
15 | return parser.parse_args()
16 |
17 |
18 | def build():
19 | from prototyper.build import run_build
20 | run_build()
21 |
22 |
23 | def main():
24 | print('Django Prototyper %s' % VERSION)
25 | args = _parse_args()
26 | django_configure()
27 |
28 | from prototyper.project import Project
29 | settings.PROTOTYPER_PROJECT = Project(args.path_to_project)
30 | django.setup()
31 |
32 | if args.build is True:
33 | build()
34 | else:
35 | run_server(args.bind)
36 |
--------------------------------------------------------------------------------
/prototyper/conf.py:
--------------------------------------------------------------------------------
1 | DJANGO_TARGET = (2, 1)
2 |
--------------------------------------------------------------------------------
/prototyper/demo_plugins/django-compressor/TODO.txt:
--------------------------------------------------------------------------------
1 | STATICFILES_FINDERS = (
2 | 'django.contrib.staticfiles.finders.FileSystemFinder',
3 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
4 | # other finders..
5 | 'compressor.finders.CompressorFinder',
6 | )
--------------------------------------------------------------------------------
/prototyper/demo_plugins/django-compressor/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "django-compressor",
3 | "version": "1.0",
4 | "description": "Compresses linked and inline JavaScript or CSS into a single cached file.",
5 | "apps": [
6 | {"name": "compressor", "models": []}
7 | ],
8 | "requirements": [
9 | "django_compressor"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/prototyper/demo_plugins/django-debug-toolbar/TODO.txt:
--------------------------------------------------------------------------------
1 | urls.py
2 |
3 | if settings.DEBUG:
4 | import debug_toolbar
5 | urlpatterns = [
6 | url(r'^__debug__/', include(debug_toolbar.urls)),
7 | ] + urlpatterns
8 |
9 |
10 |
11 |
12 | MIDDLEWARE = [
13 | # ...
14 | 'debug_toolbar.middleware.DebugToolbarMiddleware',
15 | # ...
16 | ]
17 |
--------------------------------------------------------------------------------
/prototyper/demo_plugins/django-debug-toolbar/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "django-debug-toolbar",
3 | "version": "1.0",
4 | "description": "A configurable set of panels that display various debug information about the current request/response.",
5 | "apps": [
6 | {"name": "debug_toolbar", "models": []}
7 | ],
8 | "requirements": [
9 | "django-debug-toolbar"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/prototyper/demo_plugins/django-mptt/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "django-mptt",
3 | "version": "1.0",
4 | "description": "Modified Preorder Tree Traversal(MPTT) Implementation for Django models",
5 | "apps": [
6 | {"name": "mptt", "models": ["MPTTModel"]}
7 | ],
8 | "requirements": [
9 | "django-mptt"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/prototyper/demo_plugins/django-rest-framework/TODO.txt:
--------------------------------------------------------------------------------
1 | OPTIONAL - browsable API
2 |
3 | urlpatterns = [
4 | ...
5 | url(r'^api-auth/', include('rest_framework.urls'))
6 | ]
--------------------------------------------------------------------------------
/prototyper/demo_plugins/django-rest-framework/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "django-rest-framework",
3 | "version": "1.0",
4 | "description": "Powerful and flexible toolkit for building Web APIs.",
5 | "apps": [
6 | {"name": "rest_framework", "models": []}
7 | ],
8 | "requirements": [
9 | "djangorestframework"
10 | ],
11 | "urls": {
12 | "imports": [],
13 | "urls": [
14 | "# path('api/', include(router.urls)), # see http://www.django-rest-framework.org/#example",
15 | "# path('api-browse/', include('rest_framework.urls'))"
16 | ]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/prototyper/demo_plugins/dummy/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dummy",
3 | "version": "1.0",
4 | "description": "I do nothing"
5 | }
6 |
--------------------------------------------------------------------------------
/prototyper/demo_plugins/dummyzip.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vitalik/django-prototyper/0bf7b2437c45868d2ee90c7c4d69c7d71247c978/prototyper/demo_plugins/dummyzip.zip
--------------------------------------------------------------------------------
/prototyper/demo_plugins/graphene-django/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "graphene-django",
3 | "version": "1.0",
4 | "description": "A Django integration for Graphene.",
5 | "apps": [
6 | {"name": "graphene_django", "models": []}
7 | ],
8 | "requirements": [
9 | "graphene-django"
10 | ],
11 | "urls": {
12 | "imports": [
13 | "from graphene_django.views import GraphQLView"
14 | ],
15 | "urls": [
16 | "# path('graphql', GraphQLView.as_view(graphiql=True)),"
17 | ]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/prototyper/demo_plugins/pydummy/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pydummy",
3 | "version": "1.0",
4 | "description": "Dummy plugin example with python code"
5 | }
6 |
--------------------------------------------------------------------------------
/prototyper/demo_plugins/pydummy/plugin.py:
--------------------------------------------------------------------------------
1 | from prototyper.plugins import PluginBase
2 |
3 |
4 | class Plugin(PluginBase):
5 |
6 | def on_build_complete(self):
7 | print('Dummy plugin on_build_complete')
8 |
--------------------------------------------------------------------------------
/prototyper/plugins/__init__.py:
--------------------------------------------------------------------------------
1 | from .install import install
2 | from .loading import load_py_plugins
3 | from .base import PluginBase
4 | from .discover import search_plugins
5 |
--------------------------------------------------------------------------------
/prototyper/plugins/base.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | class PluginBase(object):
4 |
5 | def __init__(self, name):
6 | self.name = name
7 |
8 | def __repr__(self):
9 | return '' % self.name
10 |
11 | def set_build(self, build):
12 | self.build = build
13 |
14 | def on_build_complete(self):
15 | pass
16 |
--------------------------------------------------------------------------------
/prototyper/plugins/discover.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 |
4 | USER_PLUGINS_DIR = os.path.join(os.path.dirname(__file__), '..', 'demo_plugins') # TODO: mabye ~/.prototyper ?
5 |
6 |
7 | def search_plugins(query):
8 | results = []
9 | ud = USER_PLUGINS_DIR
10 | if os.path.exists(ud):
11 | for item in os.listdir(ud):
12 | config = os.path.join(ud, item, 'config.json')
13 | if not os.path.exists(config):
14 | continue
15 | with open(config) as f:
16 | data = json.load(f)
17 | meta = {k: data.get(k, '') for k in ['name', 'title', 'version', 'description']}
18 | meta['url'] = os.path.join(ud, item)
19 | results.append(meta)
20 | return results
21 |
--------------------------------------------------------------------------------
/prototyper/plugins/install.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | import json
4 | import zipfile
5 | import re
6 | from django.conf import settings
7 |
8 |
9 | def install(name, url):
10 | _validate(name, url)
11 | plugins_path = settings.PROTOTYPER_PROJECT.plugins_path
12 | installer = get_installer(name, url, plugins_path)
13 | installer.clean()
14 | installer.install()
15 | return installer.config()
16 |
17 |
18 | def _validate(name, url):
19 | # we do not allow any other characters for plugin name so that user do no play with "/etc/passwd" sort of names
20 | assert re.match(r'^[a-z-_]+$', name.lower()), "Invalid plugin name"
21 |
22 |
23 | def get_installer(name, url, plugins_path):
24 | if os.path.exists(url):
25 | if url.lower().endswith('.zip'):
26 | return ZipFileInstaller(name, url, plugins_path)
27 | if os.path.isdir(url):
28 | return PathInstaller(name, url, plugins_path)
29 | elif url.lower().startswith('http'):
30 | if url.lower().endswith('.zip'):
31 | return ZipUrlInstaller(name, url, plugins_path)
32 | # TODO: github/git ?
33 | raise NotImplementedError('Do not know how to install %s, should be url/path to .zip file or path to directory')
34 |
35 |
36 | class Installer(object):
37 | def __init__(self, name, url, plugins_path):
38 | self.name = name
39 | self.url = url
40 | # basename is important so that users do not play with unsecure names
41 | self.plugin_dest = os.path.join(plugins_path, os.path.basename(name))
42 |
43 | def config(self):
44 | with open(os.path.join(self.plugin_dest, 'config.json')) as f:
45 | return json.load(f)
46 |
47 | def clean(self):
48 | if os.path.exists(self.plugin_dest):
49 | shutil.rmtree(self.plugin_dest)
50 |
51 | def install(self):
52 | raise NotImplementedError('Please implement install')
53 |
54 |
55 | class PathInstaller(Installer):
56 | def install(self):
57 | shutil.copytree(self.url, self.plugin_dest)
58 |
59 |
60 | class ZipFileInstaller(Installer):
61 | def install(self):
62 | with zipfile.ZipFile(self.url, 'r') as z:
63 | z.extractall(self.plugin_dest)
64 |
65 |
66 | class ZipUrlInstaller(Installer):
67 | def install(self):
68 | path = self.download()
69 | with zipfile.ZipExtFile(path, 'r') as z:
70 | z.extractall(self.plugin_dest)
71 |
72 |
73 | # DEMO_PLUGIN = """from prototyper.plugins import PluginBase
74 |
75 |
76 | # class Plugin(PluginBase):
77 |
78 | # def on_build_complete(self):
79 | # print('on_build_complete')
80 | # """
81 |
82 |
83 | # def demo(path):
84 | # plugin_py = path / 'plugin.py'
85 | # plugin_py.write_text(DEMO_PLUGIN)
86 |
87 |
88 | # def _plugin_path(name):
89 | # path = os.path.join(settings.PROTOTYPER_PROJECT.plugins_path, name)
90 | # path = pathlib.Path(path)
91 | # path.mkdir(parents=True, exist_ok=True)
92 | # return path
93 |
--------------------------------------------------------------------------------
/prototyper/plugins/loading.py:
--------------------------------------------------------------------------------
1 | import os
2 | from django.conf import settings
3 |
4 |
5 | def load_py_plugins():
6 | results = []
7 | path = settings.PROTOTYPER_PROJECT.plugins_path
8 | if not os.path.exists(path):
9 | return results
10 |
11 | plugins = settings.PROTOTYPER_PROJECT.load()['plugins']
12 | installed_plugins = set([p['name'] for p in plugins])
13 |
14 | for p in os.listdir(path):
15 | if p not in installed_plugins:
16 | continue
17 | plugin_module = os.path.join(path, p, 'plugin.py')
18 | print(plugin_module)
19 | if os.path.exists(plugin_module):
20 | klass = load(plugin_module)
21 | plugin = klass(p)
22 | results.append(klass(p))
23 | return results
24 |
25 |
26 | def load(path):
27 | module = _load_module('plugin', path)
28 | return module.Plugin
29 |
30 |
31 | def _load_module(module, path):
32 | import importlib.util
33 | spec = importlib.util.spec_from_file_location(module, path)
34 | module = importlib.util.module_from_spec(spec)
35 | spec.loader.exec_module(module)
36 | return module
37 |
--------------------------------------------------------------------------------
/prototyper/plugins/template.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | from pathlib import Path
4 | from django.template import Template, Context
5 |
6 |
7 | def build_template(path, dest, context):
8 | for root, dirs, files in os.walk(path):
9 | for f in files:
10 | filename = os.path.join(root, f)
11 | text = render_file(filename, context)
12 |
13 | rel = os.path.relpath(filename, path)
14 | rel = render_str(rel, context)
15 |
16 | dest_file = Path(os.path.join(dest, rel))
17 | dest_file.parent.mkdir(parents=True, exist_ok=True)
18 | dest_file.write_text(text)
19 |
20 |
21 | def render_str(s, context):
22 | return Template(s).render(Context(context))
23 |
24 |
25 | def render_file(tpl_file, context):
26 | with open(tpl_file) as f:
27 | return render_str(f.read(), context)
28 |
--------------------------------------------------------------------------------
/prototyper/project/__init__.py:
--------------------------------------------------------------------------------
1 | from .store import Project
2 |
--------------------------------------------------------------------------------
/prototyper/project/initial.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | def create_new_project(name):
5 | result = INITIAL_PROJECT.copy()
6 | result['settings']['ADMIN_SITE_HEADER'] = name
7 | result['settings']['EMAIL_SUBJECT_PREFIX'] = '[{}] '.format(name)
8 |
9 | if os.environ.get('PROTOTYPER_DEV') == 'yes':
10 | result['apps'].extend(DEMO_APPS)
11 |
12 | for app, models in INITIAL_DJANGO_APPS:
13 | result['apps'].append({
14 | 'name': app.split('.')[-1],
15 | 'external': True,
16 | 'models': [{'name': m, 'fields': [], 'admin': {'generate': False}} for m in models]
17 | })
18 |
19 | return result
20 |
21 |
22 | INITIAL_DJANGO_APPS = [
23 | ('django.contrib.admin', ['LogEntry']),
24 | ('django.contrib.auth', ['AbstractBaseUser', 'AbstractUser', 'Group', 'Permission', 'PermissionsMixin', 'User']),
25 | ('django.contrib.contenttypes', ['ContentType']),
26 | ('django.contrib.sessions', ['AbstractBaseSession', 'Session']),
27 | ('django.contrib.messages', []),
28 | ('django.contrib.staticfiles', []),
29 | ]
30 |
31 | INITIAL_PROJECT = {
32 | 'version': '0.1',
33 | 'build_settings': {},
34 | 'settings': {
35 | 'ADMIN_SITE_HEADER': 'project1',
36 | 'DEFAULT_FROM_EMAIL': 'noreply@site.com',
37 | 'EMAIL_SUBJECT_PREFIX': '[project1] ',
38 | 'LANGUAGE_CODE': 'en-us',
39 | 'TIME_ZONE': 'Europe/Brussels',
40 | 'USE_I18N': True,
41 | 'USE_L10N': True,
42 | },
43 | 'name': 'project1',
44 | 'ui': {'models_view': 'designer'},
45 | 'plugins': [],
46 | 'apps': [],
47 | }
48 |
49 | DEMO_APPS = [
50 | {'models': [
51 | {'admin': {'generate': True},
52 | 'fields': [{'attrs': {'max_length': '10'},
53 | 'name': 'title',
54 | 'relation': None,
55 | 'type': 'CharField'},
56 | {'attrs': {},
57 | 'name': 'slug',
58 | 'relation': None,
59 | 'type': 'SlugField'}],
60 | 'name': 'Category',
61 | 'ui_left': 20,
62 | 'ui_top': 20},
63 | {'admin': {'generate': True},
64 | 'fields': [{'attrs': {},
65 | 'name': 'title',
66 | 'relation': None,
67 | 'type': 'CharField'},
68 | {'attrs': {},
69 | 'name': 'slug',
70 | 'relation': None,
71 | 'type': 'SlugField'},
72 | {'attrs': {},
73 | 'name': 'categories',
74 | 'relation': 'products.Category',
75 | 'type': 'ManyToManyField'},
76 | {'attrs': {},
77 | 'name': 'description',
78 | 'relation': None,
79 | 'type': 'TextField'},
80 | {'attrs': {'decimal_places': '2',
81 | 'max_digits': '10'},
82 | 'name': 'price',
83 | 'relation': None,
84 | 'type': 'DecimalField'}],
85 | 'name': 'Product',
86 | 'ui_left': 20,
87 | 'ui_top': 104},
88 | {'admin': {'generate': True},
89 | 'fields': [{'attrs': {},
90 | 'name': 'product',
91 | 'relation': 'products.Product',
92 | 'type': 'ForeignKey'},
93 | {'attrs': {'upload_to': 'products/images/'},
94 | 'name': 'image',
95 | 'relation': None,
96 | 'type': 'ImageField'}],
97 | 'name': 'Image',
98 | 'ui_left': 20,
99 | 'ui_top': 264}],
100 | 'name': 'products',
101 | 'ui_color': '#9AAF90',
102 | 'external': False},
103 | {'models': [{'admin': {'generate': True},
104 | 'fields': [{'attrs': {},
105 | 'name': 'number',
106 | 'relation': None,
107 | 'type': 'CharField'},
108 | {'attrs': {},
109 | 'name': 'timestamp',
110 | 'relation': None,
111 | 'type': 'DateTimeField'},
112 | {'attrs': {'decimal_places': '2',
113 | 'max_digits': '10'},
114 | 'name': 'total_amount',
115 | 'relation': None,
116 | 'type': 'DecimalField'}],
117 | 'name': 'Order',
118 | 'ui_left': 200,
119 | 'ui_top': 20},
120 | {'admin': {'generate': True},
121 | 'fields': [{'attrs': {},
122 | 'name': 'order',
123 | 'relation': 'orders.Order',
124 | 'type': 'ForeignKey'},
125 | {'attrs': {},
126 | 'name': 'product',
127 | 'relation': 'products.Product',
128 | 'type': 'ForeignKey'},
129 | {'attrs': {'decimal_places': '2',
130 | 'max_digits': '10'},
131 | 'name': 'price',
132 | 'relation': None,
133 | 'type': 'DecimalField'},
134 | {'attrs': {},
135 | 'name': 'quantity',
136 | 'relation': None,
137 | 'type': 'PositiveSmallIntegerField'}],
138 | 'name': 'OrderItem',
139 | 'ui_left': 200,
140 | 'ui_top': 140}],
141 | 'name': 'orders',
142 | 'ui_color': '#F77A5E',
143 | 'external': False},
144 | {'models': [{'admin': {'generate': True},
145 | 'fields': [{'attrs': {},
146 | 'name': 'title',
147 | 'relation': None,
148 | 'type': 'CharField'},
149 | {'attrs': {},
150 | 'name': 'slug',
151 | 'relation': None,
152 | 'type': 'SlugField'},
153 | {'attrs': {},
154 | 'name': 'publication_date',
155 | 'relation': None,
156 | 'type': 'DateTimeField'},
157 | {'attrs': {},
158 | 'name': 'text',
159 | 'relation': None,
160 | 'type': 'TextField'}],
161 | 'name': 'News',
162 | 'ui_left': 480,
163 | 'ui_top': 20}],
164 | 'name': 'news',
165 | 'ui_color': '#DAC9B7',
166 | 'external': False},
167 | {'models': [{'admin': {'generate': True},
168 | 'fields': [
169 | {'attrs': {},
170 | 'name': 'big_integer',
171 | 'relation': None,
172 | 'type': 'BigIntegerField'},
173 | {'attrs': {},
174 | 'name': 'binary',
175 | 'relation': None,
176 | 'type': 'BinaryField'},
177 | {'attrs': {},
178 | 'name': 'boolean',
179 | 'relation': None,
180 | 'type': 'BooleanField'},
181 | {'attrs': {},
182 | 'name': 'char',
183 | 'relation': None,
184 | 'type': 'CharField'},
185 | {'attrs': {},
186 | 'name': 'date',
187 | 'relation': None,
188 | 'type': 'DateField'},
189 | {'attrs': {},
190 | 'name': 'date_time',
191 | 'relation': None,
192 | 'type': 'DateTimeField'},
193 | {'attrs': {'max_digits': 5, 'decimal_places': 2},
194 | 'name': 'decimal',
195 | 'relation': None,
196 | 'type': 'DecimalField'},
197 | {'attrs': {},
198 | 'name': 'duration',
199 | 'relation': None,
200 | 'type': 'DurationField'},
201 | {'attrs': {},
202 | 'name': 'email',
203 | 'relation': None,
204 | 'type': 'EmailField'},
205 | {'attrs': {'upload_to': 'products/images/'},
206 | 'name': 'file',
207 | 'relation': None,
208 | 'type': 'FileField'},
209 | {'attrs': {},
210 | 'name': 'file_path',
211 | 'relation': None,
212 | 'type': 'FilePathField'},
213 | {'attrs': {},
214 | 'name': 'float',
215 | 'relation': None,
216 | 'type': 'FloatField'},
217 | {'attrs': {},
218 | 'name': 'foreign_key',
219 | 'relation': 'products.Product',
220 | 'type': 'ForeignKey'},
221 | {'attrs': {},
222 | 'name': 'generic_ipaddr',
223 | 'relation': None,
224 | 'type': 'GenericIPAddressField'},
225 | {'attrs': {'upload_to': 'products/images/'},
226 | 'name': 'image',
227 | 'relation': None,
228 | 'type': 'ImageField'},
229 | {'attrs': {},
230 | 'name': 'integer',
231 | 'relation': None,
232 | 'type': 'IntegerField'},
233 | {'attrs': {},
234 | 'name': 'many_to_many',
235 | 'relation': 'news.News',
236 | 'type': 'ManyToManyField'},
237 | {'attrs': {},
238 | 'name': 'null_boolean',
239 | 'relation': None,
240 | 'type': 'NullBooleanField'},
241 | {'attrs': {},
242 | 'name': 'one_to_one',
243 | 'relation': 'orders.Order',
244 | 'type': 'OneToOneField'},
245 | {'attrs': {},
246 | 'name': 'positive_integer',
247 | 'relation': None,
248 | 'type': 'PositiveIntegerField'},
249 | {'attrs': {},
250 | 'name': 'positive_small_int',
251 | 'relation': None,
252 | 'type': 'PositiveSmallIntegerField'},
253 | {'attrs': {},
254 | 'name': 'slug',
255 | 'relation': None,
256 | 'type': 'SlugField'},
257 | {'attrs': {},
258 | 'name': 'small_integer',
259 | 'relation': None,
260 | 'type': 'SmallIntegerField'},
261 | {'attrs': {},
262 | 'name': 'text',
263 | 'relation': None,
264 | 'type': 'TextField'},
265 | {'attrs': {},
266 | 'name': 'time',
267 | 'relation': None,
268 | 'type': 'TimeField'},
269 | {'attrs': {},
270 | 'name': 'url',
271 | 'relation': None,
272 | 'type': 'URLField'},
273 | {'attrs': {},
274 | 'name': 'uuid',
275 | 'relation': None,
276 | 'type': 'UUIDField'}],
277 | 'name': 'AllFields',
278 | 'ui_left': 480,
279 | 'ui_top': 160}],
280 | 'name': 'temp',
281 | 'ui_color': '#9CC4E4',
282 | 'external': False},
283 | ]
284 |
--------------------------------------------------------------------------------
/prototyper/project/store.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | import json
4 | from .initial import create_new_project
5 |
6 |
7 | class Project(object):
8 | def __init__(self, path):
9 | self.path = os.path.abspath(path)
10 | self.name = self.get_name(path)
11 | self.storage_path = os.path.join(self.path, '.djangoprototyper')
12 | self.storage_file = os.path.join(self.storage_path, 'project.json')
13 | self.plugins_path = os.path.join(self.storage_path, 'plugins')
14 | self.init_storage()
15 |
16 | def init_storage(self):
17 | if os.path.exists(self.path) and not os.path.exists(self.storage_path):
18 | raise RuntimeError('Cannot init project.\n\nPath "%s" already exist and it is not djangoprototyper\n\n' % self.path)
19 | if not os.path.exists(self.path):
20 | self.init_new()
21 | else:
22 | self.load() # pasing check
23 |
24 | def init_new(self):
25 | print('Creating new project', self.name)
26 | os.makedirs(self.storage_path)
27 | data = create_new_project(self.name)
28 | self.save(data)
29 |
30 | def load(self):
31 | with open(self.storage_file, 'r') as f:
32 | data = json.load(f)
33 | data['name'] = self.name
34 | data['path'] = self.path
35 | return data
36 |
37 | def save(self, data):
38 | with open(self.storage_file, 'w') as f:
39 | json.dump(data, f, indent=1)
40 |
41 | def get_name(self, path):
42 | name = os.path.basename(self.path)
43 | return re.sub(r'[^a-z\d_]', '', name)
44 |
--------------------------------------------------------------------------------
/prototyper/server.py:
--------------------------------------------------------------------------------
1 | import os
2 | import django
3 | from django.conf import settings
4 |
5 |
6 | def django_configure():
7 | BASE_DIR = os.path.dirname(__file__)
8 |
9 | SECRET_KEY = os.environ.get('SECRET_KEY', '{{ secret_key }}')
10 | # ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost').split(',')
11 | ALLOWED_HOSTS = ['*']
12 |
13 | settings.configure(
14 | DEBUG=True,
15 | BASE_DIR=BASE_DIR,
16 | SECRET_KEY=SECRET_KEY,
17 | ALLOWED_HOSTS=ALLOWED_HOSTS,
18 | ROOT_URLCONF='prototyper.urls',
19 | INSTALLED_APPS=[
20 | ],
21 | TEMPLATES=[{
22 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
23 | }],
24 | MIDDLEWARE_CLASSES=(
25 | 'django.middleware.common.CommonMiddleware',
26 | 'django.middleware.csrf.CsrfViewMiddleware',
27 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
28 | ),
29 |
30 | DEV_MODE=os.environ.get('PROTOTYPER_DEV', 'no') == 'yes'
31 | )
32 |
33 |
34 | def run_server(bind):
35 | from django.core.management import call_command
36 | if not bind:
37 | bind = '8080'
38 | kwargs = {}
39 | DEV_MODE = os.environ.get('PROTOTYPER_DEV', 'no') == 'yes'
40 | if not DEV_MODE:
41 | kwargs['use_reloader'] = False
42 | call_command('runserver', bind, **kwargs)
43 |
44 |
--------------------------------------------------------------------------------
/prototyper/static/.gitignore:
--------------------------------------------------------------------------------
1 | build.js
2 |
--------------------------------------------------------------------------------
/prototyper/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vitalik/django-prototyper/0bf7b2437c45868d2ee90c7c4d69c7d71247c978/prototyper/static/logo.png
--------------------------------------------------------------------------------
/prototyper/static/welcome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vitalik/django-prototyper/0bf7b2437c45868d2ee90c7c4d69c7d71247c978/prototyper/static/welcome.png
--------------------------------------------------------------------------------
/prototyper/urls.py:
--------------------------------------------------------------------------------
1 | import os
2 | from django.urls import path, re_path
3 | from django.conf import settings
4 | from django.views.static import serve
5 | from .views import main_view, api_build, api_save, discover_plugins, install_plugin
6 |
7 |
8 | urlpatterns = (
9 | path('', main_view),
10 | path('api/build/', api_build),
11 | path('api/save/', api_save),
12 | path('api/plugin/', discover_plugins),
13 | path('api/plugin/install/', install_plugin),
14 |
15 | re_path(r'^static/(?P.*)$', serve, {'document_root': os.path.join(settings.BASE_DIR, 'static')}),
16 | )
17 |
--------------------------------------------------------------------------------
/prototyper/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vitalik/django-prototyper/0bf7b2437c45868d2ee90c7c4d69c7d71247c978/prototyper/utils/__init__.py
--------------------------------------------------------------------------------
/prototyper/utils/inspection/__init__.py:
--------------------------------------------------------------------------------
1 | from .main import inspect
2 |
--------------------------------------------------------------------------------
/prototyper/utils/inspection/apps.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | def installed_apps_with_models():
5 | "django 1.7+ detection"
6 | from django.apps import apps
7 | cwd = os.getcwd()
8 |
9 | for app in apps.get_app_configs():
10 | path = os.path.abspath(app.path)
11 | external = not path.startswith(cwd) or app.name.startswith('django.')
12 | models = list(app.get_models())
13 | yield app.name, external, models
14 |
--------------------------------------------------------------------------------
/prototyper/utils/inspection/field.py:
--------------------------------------------------------------------------------
1 | DJANGO_FIELDS = set([
2 | 'AutoField',
3 | 'BigAutoField',
4 | 'BigIntegerField',
5 | 'BinaryField',
6 | 'BooleanField',
7 | 'CharField',
8 | 'DateField',
9 | 'DateTimeField',
10 | 'DecimalField',
11 | 'DurationField',
12 | 'EmailField',
13 | 'FileField',
14 | 'FilePathField',
15 | 'FloatField',
16 | 'ForeignKey',
17 | 'GenericIPAddressField',
18 | 'ImageField',
19 | 'IntegerField',
20 | 'ManyToManyField',
21 | 'NullBooleanField',
22 | 'OneToOneField',
23 | 'PositiveIntegerField',
24 | 'PositiveSmallIntegerField',
25 | 'SlugField',
26 | 'SmallIntegerField',
27 | 'TextField',
28 | 'TimeField',
29 | 'URLField',
30 | 'UUIDField',
31 | ])
32 |
33 | REL_FIELDS = set(['ManyToManyField', 'ForeignKey', 'OneToOneField'])
34 |
35 |
36 | def get_field_details(fld):
37 | field_type = fld.__class__.__name__
38 | if field_type not in DJANGO_FIELDS:
39 | field_type = get_original_field_type(fld)
40 |
41 | relation = None
42 | if field_type in REL_FIELDS:
43 | rel_app = fld.rel.to._meta.app_label
44 | rel_model = fld.rel.to.__name__
45 | relation = '%s.%s' % (rel_app, rel_model)
46 |
47 | return {
48 | 'name': fld.name,
49 | 'type': field_type,
50 | 'relation': relation,
51 | 'attrs': {},
52 | }
53 |
54 |
55 | def get_original_field_type(fld):
56 | "Custom fields we cannot determine, so just try to find from which field it inherited"
57 | for ft in fld.__class__.__bases__:
58 | if ft.__name__ in DJANGO_FIELDS:
59 | return ft.__name__
60 | return fld.get_internal_type()
61 |
--------------------------------------------------------------------------------
/prototyper/utils/inspection/main.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | from prototyper.project.initial import create_new_project
4 | from .apps import installed_apps_with_models
5 | from .model import get_model_details
6 |
7 |
8 | def inspect(to_path):
9 | if os.path.exists(to_path):
10 | raise Exception("Directory '%s' already exist" % to_path)
11 | proj_name = os.path.basename(to_path)
12 |
13 | project = create_new_project(proj_name)
14 | project['apps'] = [] # clearnig default apps
15 |
16 | print('Bulding to %s' % to_path)
17 | for a, ext, models in installed_apps_with_models():
18 |
19 | app = {
20 | 'name': a,
21 | 'external': ext,
22 | 'models': []
23 | }
24 |
25 | print(' %s %s' % (ext and '~' or '+', a))
26 | for m in models:
27 | res_model = get_model_details(m)
28 | if ext:
29 | res_model['fields'] = []
30 | res_model['admin']['generate'] = False
31 | app['models'].append(res_model)
32 |
33 | project['apps'].append(app)
34 |
35 | save(to_path, project)
36 | print('Done.')
37 | print('now run:\nprototyper %s' % to_path)
38 |
39 |
40 | def save(to_path, data):
41 | store_path = os.path.join(to_path, '.djangoprototyper')
42 | os.makedirs(store_path)
43 | with open(os.path.join(store_path, 'project.json'), 'w') as f:
44 | json.dump(data, f, indent=1)
45 |
--------------------------------------------------------------------------------
/prototyper/utils/inspection/model.py:
--------------------------------------------------------------------------------
1 | from .field import get_field_details
2 |
3 |
4 | def get_model_details(model):
5 | name = model.__name__
6 | fields = []
7 | for f in model._meta.fields:
8 | if f.name == 'id' and f.primary_key:
9 | continue
10 |
11 | fld = get_field_details(f)
12 | fields.append(fld)
13 | return {
14 | 'name': name,
15 | 'fields': fields,
16 | 'admin': {'generate': True},
17 | }
18 |
--------------------------------------------------------------------------------
/prototyper/version.py:
--------------------------------------------------------------------------------
1 | VERSION = '0.1.21'
2 |
--------------------------------------------------------------------------------
/prototyper/views.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | from django.conf import settings
4 | from django.template import Template, Context
5 | from django.http import JsonResponse, HttpResponse
6 | from prototyper import VERSION
7 | from .build import run_build
8 | from . import plugins
9 |
10 | HOME_TEMPLATE = """
11 |
12 |
13 |
14 | prototyper
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 | """
30 |
31 |
32 | def _js_bundle():
33 | if settings.DEV_MODE and not os.environ.get('STATIC_BUNDLE'):
34 | return 'http://localhost:9000/dist/build.js'
35 | return '/static/build.js'
36 |
37 |
38 | def main_view(request):
39 | data = settings.PROTOTYPER_PROJECT.load()
40 | ctx = {
41 | 'PROJECT_DATA': json.dumps(data),
42 | 'JS_BUNDLE': _js_bundle(),
43 | 'VERSION': VERSION,
44 | }
45 | html = Template(HOME_TEMPLATE).render(Context(ctx))
46 | return HttpResponse(html)
47 |
48 |
49 | def api_build(request):
50 | build = run_build()
51 | return JsonResponse({
52 | 'success': build.success,
53 | 'logs': build.logger.serialize()
54 | })
55 |
56 |
57 | def api_save(request):
58 | data = _json_body(request)
59 | settings.PROTOTYPER_PROJECT.save(data)
60 | return JsonResponse({'success': True})
61 |
62 |
63 | def discover_plugins(request):
64 | query = request.GET['q']
65 | data = plugins.search_plugins(query)
66 | return JsonResponse({'success': True, 'results': data})
67 |
68 |
69 | def install_plugin(request):
70 | data = _json_body(request)
71 | plugin = plugins.install(data['name'], data['url'])
72 | return JsonResponse({'success': True, 'plugin': plugin})
73 |
74 |
75 | def _json_body(request):
76 | return json.loads(request.body.decode('utf-8'))
77 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from setuptools import setup, find_packages
3 | # To use a consistent encoding
4 | from codecs import open
5 | from os import path
6 |
7 | CURRENT_PYTHON = sys.version_info[:2]
8 | REQUIRED_PYTHON = (3, 5)
9 |
10 | if CURRENT_PYTHON < REQUIRED_PYTHON:
11 | sys.stderr.write("""
12 | ==========================
13 | Unsupported Python version
14 | ==========================
15 | Django Prototyper requires Python {}.{}, but you're trying to
16 | install it on Python {}.{}.
17 | """.format(*(REQUIRED_PYTHON + CURRENT_PYTHON)))
18 | sys.exit(1)
19 |
20 |
21 | here = path.abspath(path.dirname(__file__))
22 |
23 | # Get the long description from the README file
24 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
25 | long_description = f.read()
26 |
27 |
28 | version = __import__('prototyper').VERSION
29 |
30 | setup(
31 | name='django-prototyper',
32 | version=version,
33 | description='Django prototyping tool',
34 | long_description=long_description,
35 | url='https://github.com/vitalik/django-prototyper',
36 | author='Vitaliy Kucheryaviy',
37 | author_email='p.p.r.vitaly@gmail.com',
38 |
39 | packages=find_packages(exclude=['tests']),
40 |
41 | install_requires=['django>=2'],
42 |
43 | package_data={'prototyper': [
44 | 'static/build.js', 'static/logo.png', 'static/welcome.png', # TODO: make it automatic
45 | 'demo_plugins/**/*.json',
46 | ]},
47 | include_package_data=True,
48 |
49 | entry_points={
50 | 'console_scripts': [
51 | 'prototyper=prototyper.cli:main',
52 | ],
53 | },
54 |
55 |
56 | classifiers=[
57 | 'Development Status :: 3 - Alpha', # 3 - Alpha, 4 - Beta, 5 - Production
58 |
59 | 'Intended Audience :: Developers',
60 | 'Topic :: Software Development :: Build Tools',
61 |
62 | 'License :: OSI Approved :: MIT License',
63 |
64 | 'Programming Language :: Python :: 3.5',
65 | 'Programming Language :: Python :: 3.6',
66 | 'Programming Language :: Python :: 3.7',
67 | ],
68 | keywords='django prototype boilerplate development uml diagrams',
69 | )
70 |
--------------------------------------------------------------------------------
/tests/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3
2 | ENV PYTHONUNBUFFERED 1
3 | RUN pip install Django==2.0
4 | RUN pip install Pillow
5 | RUN pip install django-webpack-loader
6 | RUN pip install fabric3
7 | RUN mkdir /code
8 |
9 | ADD tests/project.json /tmp/project1/.djangoprototyper/
10 |
11 | WORKDIR /code
12 |
13 | CMD ["python", "tests/test.py"]
14 |
--------------------------------------------------------------------------------
/tests/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | prototyper:
4 | build:
5 | context: ../
6 | dockerfile: tests/Dockerfile
7 | tty: true
8 | volumes:
9 | - ..:/code
10 |
--------------------------------------------------------------------------------
/tests/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.1",
3 | "build_settings": {
4 | "ugettext_lazy": true,
5 | "pep8model_fields": false,
6 | "charfield_max_length": 200
7 | },
8 | "settings": {
9 | "ADMIN_SITE_HEADER": "project2",
10 | "DEFAULT_FROM_EMAIL": "noreply@site.com",
11 | "EMAIL_SUBJECT_PREFIX": "[project2] ",
12 | "LANGUAGE_CODE": "en-us",
13 | "TIME_ZONE": "Europe/Brussels",
14 | "USE_I18N": true,
15 | "USE_L10N": true
16 | },
17 | "name": "project2",
18 | "ui": {
19 | "models_view": "designer"
20 | },
21 | "apps": [
22 | {
23 | "models": [
24 | {
25 | "admin": {
26 | "generate": true
27 | },
28 | "fields": [
29 | {
30 | "attrs": {
31 | "max_length": "10"
32 | },
33 | "name": "title",
34 | "relation": null,
35 | "type": "CharField"
36 | },
37 | {
38 | "attrs": {},
39 | "name": "slug",
40 | "relation": null,
41 | "type": "SlugField"
42 | }
43 | ],
44 | "name": "Category",
45 | "ui_left": 20,
46 | "ui_top": 20
47 | },
48 | {
49 | "admin": {
50 | "generate": true
51 | },
52 | "fields": [
53 | {
54 | "attrs": {},
55 | "name": "title",
56 | "relation": null,
57 | "type": "CharField"
58 | },
59 | {
60 | "attrs": {},
61 | "name": "slug",
62 | "relation": null,
63 | "type": "SlugField"
64 | },
65 | {
66 | "attrs": {},
67 | "name": "categories",
68 | "relation": "products.Category",
69 | "type": "ManyToManyField"
70 | },
71 | {
72 | "attrs": {},
73 | "name": "description",
74 | "relation": null,
75 | "type": "TextField"
76 | },
77 | {
78 | "attrs": {
79 | "decimal_places": "2",
80 | "max_digits": "10"
81 | },
82 | "name": "price",
83 | "relation": null,
84 | "type": "DecimalField"
85 | }
86 | ],
87 | "name": "Product",
88 | "ui_left": 20,
89 | "ui_top": 104
90 | },
91 | {
92 | "admin": {
93 | "generate": true
94 | },
95 | "fields": [
96 | {
97 | "attrs": {},
98 | "name": "product",
99 | "relation": "products.Product",
100 | "type": "ForeignKey"
101 | },
102 | {
103 | "attrs": {},
104 | "name": "image",
105 | "relation": null,
106 | "type": "ImageField"
107 | }
108 | ],
109 | "name": "Image",
110 | "ui_left": 20,
111 | "ui_top": 264
112 | }
113 | ],
114 | "name": "products",
115 | "ui_color": "#9AAF90"
116 | },
117 | {
118 | "models": [
119 | {
120 | "admin": {
121 | "generate": true
122 | },
123 | "fields": [
124 | {
125 | "attrs": {},
126 | "name": "number",
127 | "relation": null,
128 | "type": "CharField"
129 | },
130 | {
131 | "attrs": {},
132 | "name": "timestamp",
133 | "relation": null,
134 | "type": "DateTimeField"
135 | },
136 | {
137 | "attrs": {
138 | "decimal_places": "2",
139 | "max_digits": "10"
140 | },
141 | "name": "total_amount",
142 | "relation": null,
143 | "type": "DecimalField"
144 | }
145 | ],
146 | "name": "Order",
147 | "ui_left": 200,
148 | "ui_top": 20
149 | },
150 | {
151 | "admin": {
152 | "generate": true
153 | },
154 | "fields": [
155 | {
156 | "attrs": {},
157 | "name": "order",
158 | "relation": "orders.Order",
159 | "type": "ForeignKey"
160 | },
161 | {
162 | "attrs": {},
163 | "name": "product",
164 | "relation": "products.Product",
165 | "type": "ForeignKey"
166 | },
167 | {
168 | "attrs": {
169 | "decimal_places": "2",
170 | "max_digits": "10"
171 | },
172 | "name": "price",
173 | "relation": null,
174 | "type": "DecimalField"
175 | },
176 | {
177 | "attrs": {},
178 | "name": "quantity",
179 | "relation": null,
180 | "type": "PositiveSmallIntegerField"
181 | }
182 | ],
183 | "name": "OrderItem",
184 | "ui_left": 200,
185 | "ui_top": 140
186 | }
187 | ],
188 | "name": "orders",
189 | "ui_color": "#F77A5E"
190 | },
191 | {
192 | "models": [
193 | {
194 | "admin": {
195 | "generate": true
196 | },
197 | "fields": [
198 | {
199 | "attrs": {},
200 | "name": "title",
201 | "relation": null,
202 | "type": "CharField"
203 | },
204 | {
205 | "attrs": {},
206 | "name": "slug",
207 | "relation": null,
208 | "type": "SlugField"
209 | },
210 | {
211 | "attrs": {},
212 | "name": "publication_date",
213 | "relation": null,
214 | "type": "DateTimeField"
215 | },
216 | {
217 | "attrs": {},
218 | "name": "text",
219 | "relation": null,
220 | "type": "TextField"
221 | }
222 | ],
223 | "name": "News",
224 | "ui_left": 480,
225 | "ui_top": 20
226 | }
227 | ],
228 | "name": "news",
229 | "ui_color": "#DAC9B7"
230 | },
231 | {
232 | "models": [
233 | {
234 | "admin": {
235 | "generate": true
236 | },
237 | "fields": [
238 |
239 | {
240 | "attrs": {},
241 | "name": "big_integer",
242 | "relation": null,
243 | "type": "BigIntegerField"
244 | },
245 | {
246 | "attrs": {},
247 | "name": "binary",
248 | "relation": null,
249 | "type": "BinaryField"
250 | },
251 | {
252 | "attrs": {},
253 | "name": "boolean",
254 | "relation": null,
255 | "type": "BooleanField"
256 | },
257 | {
258 | "attrs": {},
259 | "name": "char",
260 | "relation": null,
261 | "type": "CharField"
262 | },
263 | {
264 | "attrs": {},
265 | "name": "date",
266 | "relation": null,
267 | "type": "DateField"
268 | },
269 | {
270 | "attrs": {},
271 | "name": "date_time",
272 | "relation": null,
273 | "type": "DateTimeField"
274 | },
275 | {
276 | "attrs": {},
277 | "name": "decimal",
278 | "relation": null,
279 | "type": "DecimalField"
280 | },
281 | {
282 | "attrs": {},
283 | "name": "duration",
284 | "relation": null,
285 | "type": "DurationField"
286 | },
287 | {
288 | "attrs": {},
289 | "name": "email",
290 | "relation": null,
291 | "type": "EmailField"
292 | },
293 | {
294 | "attrs": {},
295 | "name": "file",
296 | "relation": null,
297 | "type": "FileField"
298 | },
299 | {
300 | "attrs": {},
301 | "name": "file_path",
302 | "relation": null,
303 | "type": "FilePathField"
304 | },
305 | {
306 | "attrs": {},
307 | "name": "float_f",
308 | "relation": null,
309 | "type": "FloatField"
310 | },
311 | {
312 | "attrs": {},
313 | "name": "foreign_key",
314 | "relation": "products.Product",
315 | "type": "ForeignKey"
316 | },
317 | {
318 | "attrs": {},
319 | "name": "generic_ipaddr",
320 | "relation": null,
321 | "type": "GenericIPAddressField"
322 | },
323 | {
324 | "attrs": {},
325 | "name": "image",
326 | "relation": null,
327 | "type": "ImageField"
328 | },
329 | {
330 | "attrs": {},
331 | "name": "integer",
332 | "relation": null,
333 | "type": "IntegerField"
334 | },
335 | {
336 | "attrs": {},
337 | "name": "many_to_many",
338 | "relation": "news.News",
339 | "type": "ManyToManyField"
340 | },
341 | {
342 | "attrs": {},
343 | "name": "null_boolean",
344 | "relation": null,
345 | "type": "NullBooleanField"
346 | },
347 | {
348 | "attrs": {},
349 | "name": "one_to_one",
350 | "relation": "orders.Order",
351 | "type": "OneToOneField"
352 | },
353 | {
354 | "attrs": {},
355 | "name": "positive_integer",
356 | "relation": null,
357 | "type": "PositiveIntegerField"
358 | },
359 | {
360 | "attrs": {},
361 | "name": "positive_small_int",
362 | "relation": null,
363 | "type": "PositiveSmallIntegerField"
364 | },
365 | {
366 | "attrs": {},
367 | "name": "slug",
368 | "relation": null,
369 | "type": "SlugField"
370 | },
371 | {
372 | "attrs": {},
373 | "name": "small_integer",
374 | "relation": null,
375 | "type": "SmallIntegerField"
376 | },
377 | {
378 | "attrs": {},
379 | "name": "text",
380 | "relation": null,
381 | "type": "TextField"
382 | },
383 | {
384 | "attrs": {},
385 | "name": "time",
386 | "relation": null,
387 | "type": "TimeField"
388 | },
389 | {
390 | "attrs": {},
391 | "name": "url",
392 | "relation": null,
393 | "type": "URLField"
394 | },
395 | {
396 | "attrs": {},
397 | "name": "uuid",
398 | "relation": null,
399 | "type": "UUIDField"
400 | }
401 | ],
402 | "name": "AllFields",
403 | "ui_left": 480,
404 | "ui_top": 160
405 | }
406 | ],
407 | "name": "temp",
408 | "ui_color": "#9CC4E4"
409 | }
410 | ]
411 | }
--------------------------------------------------------------------------------
/tests/test.py:
--------------------------------------------------------------------------------
1 | from fabric.api import local, lcd
2 | import os
3 |
4 |
5 | local('ls -al')
6 |
7 | local('python main.py /tmp/project1 --build')
8 |
9 | with lcd('/tmp/project1/project1'):
10 | local('./manage.py check')
11 | local('./manage.py migrate --run-syncdb')
12 |
--------------------------------------------------------------------------------
/tests/ui/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3
2 | ENV PYTHONUNBUFFERED 1
3 | RUN pip install Django==2.0
4 | RUN pip install Pillow
5 | RUN pip install django-webpack-loader
6 |
7 | RUN apt-get update && apt-get -y install curl
8 |
9 | RUN curl -sL https://deb.nodesource.com/setup_8.x | bash
10 | RUN apt-get install -y nodejs
11 |
12 |
13 |
14 |
15 |
16 |
17 | RUN mkdir /code
18 | ADD frontend/package.json /code/frontend/
19 | WORKDIR /code/frontend
20 |
21 | RUN npm install
22 | RUN npm rebuild node-sass --force
23 |
24 |
25 |
26 |
27 | ADD frontend /code/frontend
28 | RUN npm run build
29 |
30 |
31 | ADD backend /code/backend
32 |
33 | WORKDIR /code/backend/
34 |
35 | EXPOSE 8000
36 |
37 | CMD ["python", "main.py", "--bind=0.0.0.0:8000", "/tmp/testproject1"]
38 |
--------------------------------------------------------------------------------
/tests/ui/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | chrome:
4 | image: selenium/standalone-chrome
5 | shm_size: 1gb
6 | ports:
7 | - "4444:4444"
8 | prototyper:
9 | build:
10 | context: ../../
11 | dockerfile: tests/ui/Dockerfile
12 | tty: true
13 | ports:
14 | - "8000:8000"
15 |
16 |
--------------------------------------------------------------------------------
/tests/ui/test.py:
--------------------------------------------------------------------------------
1 | from time import sleep
2 |
3 | from selenium import webdriver
4 | from selenium.common.exceptions import NoSuchElementException, UnexpectedAlertPresentException
5 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
6 | from selenium.webdriver.support.select import Select
7 |
8 |
9 | def main():
10 | DesiredCapabilities.CHROME["unexpectedAlertBehaviour"] = "accept"
11 | chrome = webdriver.Remote(
12 | command_executor='http://localhost:4444/wd/hub',
13 | desired_capabilities=DesiredCapabilities.CHROME
14 | )
15 | chrome.get('http://prototyper:8000/')
16 | chrome.find_elements_by_css_selector('.nav a')[2].click()
17 |
18 | def invalid_input(input_field, btn_field, inputs=None):
19 | if inputs is None:
20 | inputs = ["", "_a", "12aa", "@dsa", "-ee", "~aqq", "as "]
21 | for input in inputs:
22 | input_field.send_keys(input)
23 | assert 'disabled' in btn_field.get_attribute('class')
24 | input_field.clear()
25 |
26 | def same_object(input_field, btn_field, name):
27 | input_field.send_keys(name)
28 | btn_field.click()
29 | chrome.switch_to.alert.accept()
30 |
31 | def test_project_add():
32 | try:
33 | project = chrome.find_element_by_xpath(
34 | '//div[contains(@class,"card-header") and contains(text(),"test_project")]')
35 | delete_btn = project.find_element_by_xpath('.//button')
36 | delete_btn.click()
37 | except NoSuchElementException:
38 | print('not found')
39 | pass
40 | sleep(0.1)
41 |
42 | input_field = chrome.find_element_by_xpath('//div[contains(@class,"input-group")]//input')
43 | field_add_btn = chrome.find_element_by_xpath('//div[contains(@class,"input-group")]//button')
44 | project_name = 'test_project'
45 | invalid_input(input_field, field_add_btn)
46 | input_field.send_keys(project_name)
47 | field_add_btn.click()
48 | same_object(input_field, field_add_btn, project_name)
49 | sleep(0.1)
50 |
51 | test_project_add()
52 |
53 | def test_model_add():
54 | project_header = chrome.find_element_by_xpath(
55 | '//div[contains(@class,"card-header") and contains(text(),"test_project")]')
56 |
57 | model_input = project_header.find_element_by_xpath('./following-sibling::div//input')
58 | model_add_btn = project_header.find_element_by_xpath('./following-sibling::div//button')
59 | model_name = 'test_model'
60 | model_input.send_keys(model_name)
61 | model_add_btn.click()
62 |
63 | same_object(model_input, model_add_btn, model_name)
64 |
65 | invalid_input(model_input, model_add_btn)
66 |
67 | test_model_add()
68 |
69 | def test_field_add():
70 |
71 | model_href = chrome.find_element_by_xpath('//a[text()="test_model"]')
72 | model_href.click()
73 | field_input = chrome.find_element_by_xpath('//div[@class="input-group"]/input')
74 | field_add_btn = chrome.find_element_by_xpath('//div[contains(@class,"input-group")]//button')
75 | invalid_input(field_input, field_add_btn)
76 |
77 | field_name = 'text'
78 | field_input.send_keys(field_name)
79 | field_add_btn.click()
80 |
81 | same_object(field_input, field_add_btn, field_name)
82 | field_tr = chrome.find_elements_by_xpath('//td//input/ancestor::tr')[0]
83 | type_field = field_tr.find_element_by_xpath('.//select')
84 | assert "TextField" == type_field.get_attribute('value')
85 | select = Select(type_field)
86 | select.select_by_visible_text("CharField")
87 | assert "CharField" == type_field.get_attribute('value')
88 |
89 | null_span = field_tr.find_element_by_xpath('.//span[contains(text(),"N")]')
90 | blank_span = field_tr.find_element_by_xpath('.//span[contains(text(),"B")]')
91 | unique_span = field_tr.find_element_by_xpath('.//span[contains(text(),"U")]')
92 | index_span = field_tr.find_element_by_xpath('.//span[contains(text(),"I")]')
93 | spans = [null_span, blank_span, unique_span, index_span]
94 | for span in spans:
95 | span.click()
96 |
97 | null_select = chrome.find_element_by_xpath(
98 | '//tr[contains(@class,"field-attr")]/td[contains(text(),"null")]/parent::tr//select')
99 | blank_select = chrome.find_element_by_xpath(
100 | '//tr[contains(@class,"field-attr")]/td[contains(text(),"blank")]/parent::tr//select')
101 | unique_select = chrome.find_element_by_xpath(
102 | '//tr[contains(@class,"field-attr")]/td[contains(text(),"unique")]/parent::tr//select')
103 | db_index_select = chrome.find_element_by_xpath(
104 | '//tr[contains(@class,"field-attr")]/td[contains(text(),"db_index")]/parent::tr//select')
105 | selects = [null_select, blank_select, unique_select, db_index_select]
106 | for select in selects:
107 | assert "true" in select.get_attribute('value').lower()
108 | for span in spans:
109 | span.click()
110 | for select in selects:
111 | assert not "true" in select.get_attribute('value').lower()
112 |
113 | test_field_add()
114 | chrome.save_screenshot('/tmp/screen.png')
115 |
116 |
117 | main()
118 |
--------------------------------------------------------------------------------