├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── README.rst ├── fabfile.py ├── frontend ├── .babelrc ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── Prototyper.vue │ ├── assets │ │ ├── bootstrap_custom.scss │ │ └── styles.scss │ ├── backend.js │ ├── components │ │ ├── Home.vue │ │ ├── admin │ │ │ ├── Editor.vue │ │ │ ├── SelectFields.vue │ │ │ └── index.vue │ │ ├── appsmodels │ │ │ ├── App.vue │ │ │ ├── Field.vue │ │ │ ├── FieldAttr.vue │ │ │ ├── FieldAttrsEditor.vue │ │ │ ├── FieldType.vue │ │ │ ├── Inheritance.vue │ │ │ ├── MetaEditor.vue │ │ │ ├── ModelEditor.vue │ │ │ ├── SelectMultipleFields.vue │ │ │ ├── SelectOrderableFields.vue │ │ │ ├── designer │ │ │ │ ├── AddModelDialog.vue │ │ │ │ ├── AppColors.vue │ │ │ │ ├── DragArea.vue │ │ │ │ ├── Model.vue │ │ │ │ ├── Relations.vue │ │ │ │ ├── colors.js │ │ │ │ └── index.vue │ │ │ └── index.vue │ │ ├── build │ │ │ ├── ValidationIcon.vue │ │ │ └── index.vue │ │ ├── buildsettings │ │ │ └── index.vue │ │ ├── plugins │ │ │ ├── Discover.vue │ │ │ └── index.vue │ │ ├── settings │ │ │ ├── DjangoContrib.vue │ │ │ └── index.vue │ │ └── utils │ │ │ ├── CheckLabel.vue │ │ │ ├── InputTrueFalse.vue │ │ │ ├── Modal.vue │ │ │ └── PatternInput.vue │ ├── django │ │ ├── apps.js │ │ ├── fields.js │ │ └── guess.js │ ├── main.js │ ├── router.js │ ├── store.js │ └── validation.js └── webpack.config.js ├── main.py ├── prototyper ├── __init__.py ├── build │ ├── __init__.py │ ├── base.py │ ├── log.py │ ├── main.py │ └── stages │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── core.py │ │ ├── models.py │ │ ├── requirements.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi_app.py ├── cli.py ├── conf.py ├── demo_plugins │ ├── django-compressor │ │ ├── TODO.txt │ │ └── config.json │ ├── django-debug-toolbar │ │ ├── TODO.txt │ │ └── config.json │ ├── django-mptt │ │ └── config.json │ ├── django-rest-framework │ │ ├── TODO.txt │ │ └── config.json │ ├── dummy │ │ └── config.json │ ├── dummyzip.zip │ ├── graphene-django │ │ └── config.json │ └── pydummy │ │ ├── config.json │ │ └── plugin.py ├── plugins │ ├── __init__.py │ ├── base.py │ ├── discover.py │ ├── install.py │ ├── loading.py │ └── template.py ├── project │ ├── __init__.py │ ├── initial.py │ └── store.py ├── server.py ├── static │ ├── .gitignore │ ├── logo.png │ └── welcome.png ├── urls.py ├── utils │ ├── __init__.py │ └── inspection │ │ ├── __init__.py │ │ ├── apps.py │ │ ├── field.py │ │ ├── main.py │ │ └── model.py ├── version.py └── views.py ├── setup.py └── tests ├── Dockerfile ├── docker-compose.yml ├── project.json ├── test.py └── ui ├── Dockerfile ├── docker-compose.yml └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.*~ 3 | *.swp 4 | *.swo 5 | *.pot 6 | *.py[co] 7 | __pycache__ 8 | *.egg-info 9 | .env/* 10 | dist/* 11 | build/* 12 | *.suo 13 | *.ntvs* 14 | *.njsproj 15 | *.sln 16 | *.code-workspace 17 | .idea 18 | .vscode 19 | frontend/node_modules 20 | frontend/webpack-stats.json 21 | frontend/dist 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Vitaliy Kucheryaviy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include prototyper/static/*.js 2 | include prototyper/static/*.png 3 | 4 | recursive-include prototyper/demo_plugins *.json *.py 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-prototyper 2 | 3 | [![Downloads](https://pepy.tech/badge/django-prototyper)](https://pepy.tech/project/django-prototyper) 4 | 5 | A UI for generating django code 6 | 7 | Please see introduction video for more details: 8 | 9 | [](https://www.youtube.com/watch?v=QqHm2LfcKx0) 10 | 11 | 12 | ## Installation 13 | 14 | ``` 15 | # if you have python2 16 | python3 -m venv .env 17 | source .env/bin/activate 18 | ``` 19 | 20 | ``` 21 | pip install django-prototyper 22 | ``` 23 | 24 | ## Usage 25 | 26 | ``` 27 | prototyper ./myproject1 28 | ``` 29 | 30 | Then open your browser and go to http://localhost:8080/ and follow UI 31 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-prototyper 2 | =============== 3 | 4 | 5 | UI for generating django code 6 | 7 | 8 | Installation 9 | ----------------- 10 | 11 | :: 12 | 13 | # if you have python2 14 | python3 -m venv .env 15 | source .env/bin/activate 16 | 17 | 18 | :: 19 | 20 | pip install django-prototyper 21 | 22 | 23 | 24 | Usage 25 | ----------------- 26 | 27 | 28 | :: 29 | 30 | prototyper ./myproject1 31 | 32 | 33 | 34 | Then open your browser and go to http://localhost:8080/ and follow UI 35 | 36 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from fabric.api import lcd, local, settings 4 | 5 | 6 | def release(): 7 | version_bump() 8 | 9 | with lcd('frontend'): 10 | local('npm run build') 11 | local('cp frontend/dist/build.js prototyper/static/build.js') 12 | with settings(warn_only=True): 13 | local('rm dist/*.gz dist/*.whl') 14 | local('python setup.py bdist_wheel') 15 | local('python setup.py sdist') 16 | local('twine upload dist/*') 17 | 18 | 19 | def version_bump(): 20 | ver_file = os.path.join(os.path.dirname(__file__), 'prototyper/version.py') 21 | with open(ver_file) as f: 22 | contents = f.read() 23 | assert len(contents.splitlines()) == 1 # we expect only 1 line as this is automaticlly generated 24 | ver = re.findall(r'\d+\.\d+\.\d+', contents)[0] 25 | ver = map(int, ver.split('.')) 26 | ver[-1] += 1 27 | with open(ver_file, 'w') as f: 28 | f.write("VERSION = '%d.%d.%d'\n" % tuple(ver)) 29 | 30 | local('git add prototyper/version.py') 31 | local('git commit -m "version %s"' % ver) 32 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }], 4 | "stage-3" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | prototyper 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prototyper", 3 | "description": "PROTOTYPER", 4 | "version": "1.0.0", 5 | "author": "Vitaliy Kucheriavyi ", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": { 9 | "dev": "cross-env NODE_ENV=development webpack-dev-server --hot", 10 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" 11 | }, 12 | "dependencies": { 13 | "axios": "^0.18.1", 14 | "bootstrap": "^4.0.0", 15 | "lightness": "^1.0.0", 16 | "lodash": "^4.17.13", 17 | "vue": "^2.5.11", 18 | "vue-outside-events": "^1.1.0", 19 | "vue-router": "^3.0.1", 20 | "vuedraggable": "^2.16.0" 21 | }, 22 | "browserslist": [ 23 | "> 1%", 24 | "last 2 versions", 25 | "not ie <= 8" 26 | ], 27 | "devDependencies": { 28 | "babel-core": "^6.26.0", 29 | "babel-loader": "^7.1.2", 30 | "babel-preset-env": "^1.6.0", 31 | "babel-preset-stage-3": "^6.24.1", 32 | "cross-env": "^5.0.5", 33 | "css-loader": "^0.28.7", 34 | "file-loader": "^1.1.4", 35 | "node-sass": "^4.9.1", 36 | "sass-loader": "^6.0.6", 37 | "vue-loader": "^13.0.5", 38 | "vue-template-compiler": "^2.4.4", 39 | "webpack": "^3.6.0", 40 | "webpack-bundle-tracker": "^0.2.1", 41 | "webpack-dev-server": "^2.9.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/Prototyper.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 73 | -------------------------------------------------------------------------------- /frontend/src/assets/bootstrap_custom.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | //$border-radius: 0px; 4 | $font-size-base: 0.85rem; 5 | 6 | 7 | $input-focus-box-shadow: none; 8 | 9 | $primary: #00C187; 10 | 11 | 12 | 13 | @import "~bootstrap/scss/functions"; 14 | @import "~bootstrap/scss/variables"; 15 | @import "~bootstrap/scss/mixins"; 16 | //@import "~bootstrap/scss/print"; 17 | @import "~bootstrap/scss/reboot"; 18 | @import "~bootstrap/scss/type"; 19 | @import "~bootstrap/scss/images"; 20 | @import "~bootstrap/scss/code"; 21 | @import "~bootstrap/scss/grid"; 22 | @import "~bootstrap/scss/tables"; 23 | @import "~bootstrap/scss/forms"; 24 | @import "~bootstrap/scss/buttons"; 25 | @import "~bootstrap/scss/transitions"; 26 | @import "~bootstrap/scss/dropdown"; 27 | @import "~bootstrap/scss/button-group"; 28 | @import "~bootstrap/scss/input-group"; 29 | @import "~bootstrap/scss/custom-forms"; 30 | @import "~bootstrap/scss/nav"; 31 | @import "~bootstrap/scss/navbar"; 32 | @import "~bootstrap/scss/card"; 33 | @import "~bootstrap/scss/breadcrumb"; 34 | @import "~bootstrap/scss/pagination"; 35 | @import "~bootstrap/scss/badge"; 36 | @import "~bootstrap/scss/jumbotron"; 37 | @import "~bootstrap/scss/alert"; 38 | @import "~bootstrap/scss/progress"; 39 | //@import "~bootstrap/scss/media"; 40 | @import "~bootstrap/scss/list-group"; 41 | @import "~bootstrap/scss/close"; 42 | @import "~bootstrap/scss/modal"; 43 | // @import "~bootstrap/scss/tooltip"; 44 | // @import "~bootstrap/scss/popover"; 45 | // @import "~bootstrap/scss/carousel"; 46 | @import "~bootstrap/scss/utilities"; 47 | 48 | 49 | .btn-outline-secondary:focus, .btn-primary:focus { 50 | box-shadow: none; 51 | } 52 | -------------------------------------------------------------------------------- /frontend/src/assets/styles.scss: -------------------------------------------------------------------------------- 1 | 2 | #app { 3 | position: fixed; 4 | top: 0; 5 | bottom: 0; 6 | left: 0; 7 | right: 0; 8 | 9 | >.navbar { 10 | position: fixed; 11 | top: 0; 12 | left: 0; 13 | right: 0; 14 | z-index: 100; 15 | background-color: white; 16 | border: 1px solid #EBEDF8; 17 | 18 | .branding { 19 | font-weight: bold; 20 | font-size: 1.1rem; 21 | color: #212529; 22 | 23 | sup { 24 | color: #ccc; 25 | font-weight: 100; 26 | font-size: 0.6rem; 27 | } 28 | } 29 | } 30 | 31 | #sidebar { 32 | position: fixed; 33 | left: 0; 34 | bottom: 0; 35 | top: 57px; 36 | width: 200px; 37 | 38 | background-color: #272C38; 39 | border-right: 1px solid #DFE3E8; 40 | 41 | .nav-pills .nav-link { 42 | color: #fff; 43 | 44 | &.active { 45 | background-color: #1E222E; 46 | border-radius: 0; 47 | border-left: 0.3rem solid #00C187; 48 | padding-left: 0.7rem; 49 | } 50 | 51 | } 52 | } 53 | 54 | #maincontent { 55 | position: fixed; 56 | left: 200px; 57 | bottom: 0; 58 | top: 57px; 59 | right: 0; 60 | } 61 | 62 | .heading { 63 | padding-top: 0.5rem; 64 | padding-bottom: 1rem; 65 | margin-bottom: 1rem; 66 | background-color: #f1f6fa; 67 | border-bottom: 1px solid #EBEDF8; 68 | } 69 | } 70 | 71 | 72 | .plugins { 73 | .plugin-item { 74 | margin-bottom: 1rem; 75 | padding-bottom: 1rem; 76 | border-bottom: 1px solid #EBEDF8; 77 | } 78 | } 79 | 80 | .admin { 81 | .djangoapp { 82 | table { 83 | margin-bottom: 0; 84 | 85 | tr:first-child td { 86 | border: none !important; 87 | } 88 | 89 | } 90 | } 91 | } 92 | 93 | .designer { 94 | overflow: scroll; 95 | position: relative; 96 | 97 | .model { 98 | padding: 0.1rem 0.3rem; 99 | position: absolute; 100 | min-width: 150px; 101 | border-radius: 2px; 102 | 103 | &.selected { 104 | border: 1px solid #00f !important; 105 | box-shadow: 0px 0px 4px #00f; 106 | } 107 | 108 | &.active { 109 | box-shadow: 1px 1px 4px ", " #ccc; 110 | z-index: 100; 111 | margin-left: -4px; 112 | margin-top: -4px; 113 | padding-left: 8px; 114 | padding-top: 6px; 115 | box-shadow: 0px 0px 4px #ccc; 116 | } 117 | } 118 | } 119 | 120 | .appcolors .modal-dialog { 121 | max-width: 700px; 122 | } 123 | 124 | .sortable-ghost { 125 | opacity: 0.3; 126 | } -------------------------------------------------------------------------------- /frontend/src/backend.js: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | 3 | var request = axios.create({ 4 | baseURL: '/api/' 5 | }) 6 | 7 | request.defaults.xsrfCookieName = 'csrftoken' 8 | request.defaults.xsrfHeaderName = 'X-CSRFToken' 9 | 10 | 11 | export default { 12 | build(body) { 13 | return request.post('/build/', body) 14 | }, 15 | save(project) { 16 | return request.post('/save/', project) 17 | }, 18 | plugin_search(query) { 19 | return request.get('/plugin/', {params:{q:query}}) 20 | }, 21 | plugin_install(name, url) { 22 | return request.post('/plugin/install/', {name, url}) 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/components/Home.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 63 | -------------------------------------------------------------------------------- /frontend/src/components/admin/Editor.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 61 | -------------------------------------------------------------------------------- /frontend/src/components/admin/SelectFields.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 118 | -------------------------------------------------------------------------------- /frontend/src/components/admin/index.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/App.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 83 | 84 | 85 | 123 | 124 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/Field.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 68 | 69 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/FieldAttr.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 62 | 63 | 71 | 72 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/FieldAttrsEditor.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 36 | 37 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/FieldType.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 45 | 46 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/Inheritance.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 97 | 98 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/MetaEditor.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 66 | 67 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/ModelEditor.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 169 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/SelectMultipleFields.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 39 | 40 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/SelectOrderableFields.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 127 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/designer/AddModelDialog.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/designer/AppColors.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 56 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/designer/DragArea.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 125 | 126 | 127 | 130 | 131 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/designer/Model.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 119 | 120 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/designer/Relations.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 115 | 116 | 117 | 133 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/designer/colors.js: -------------------------------------------------------------------------------- 1 | import { random } from 'lodash' 2 | import { store } from '../../../store' 3 | 4 | export const APP_COLORS = [ 5 | "#F77A5E", 6 | "#D7E684", 7 | "#FFF7C1", 8 | "#1FD8A8", 9 | "#9CC4E4", 10 | "#B7E6F2", 11 | "#C1ABB7", 12 | "#DAC9B7", 13 | "#eeeeee", 14 | "#9AAF90", 15 | ] 16 | 17 | const GRAY = '#eeeeee' // should be in APP_COLORS 18 | 19 | 20 | export function get_some_color(external) { 21 | if (external) 22 | return GRAY 23 | return APP_COLORS[_.random(APP_COLORS.length-1)] 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/designer/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 106 | 107 | -------------------------------------------------------------------------------- /frontend/src/components/appsmodels/index.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 98 | -------------------------------------------------------------------------------- /frontend/src/components/build/ValidationIcon.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 80 | 81 | 90 | -------------------------------------------------------------------------------- /frontend/src/components/build/index.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 128 | -------------------------------------------------------------------------------- /frontend/src/components/buildsettings/index.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 72 | -------------------------------------------------------------------------------- /frontend/src/components/plugins/Discover.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 76 | -------------------------------------------------------------------------------- /frontend/src/components/plugins/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 62 | -------------------------------------------------------------------------------- /frontend/src/components/settings/DjangoContrib.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 72 | -------------------------------------------------------------------------------- /frontend/src/components/settings/index.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 78 | 79 | 90 | -------------------------------------------------------------------------------- /frontend/src/components/utils/CheckLabel.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 24 | -------------------------------------------------------------------------------- /frontend/src/components/utils/InputTrueFalse.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 51 | -------------------------------------------------------------------------------- /frontend/src/components/utils/Modal.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /frontend/src/components/utils/PatternInput.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 64 | -------------------------------------------------------------------------------- /frontend/src/django/apps.js: -------------------------------------------------------------------------------- 1 | export const DJANGO_CONTRIB_APPS = { 2 | 'admin': ['LogEntry'], 3 | 'admindocs': [], 4 | 'auth': ['AbstractBaseUser', 'AbstractUser', 'Group', 'Permission', 'PermissionsMixin', 'User'], 5 | 'contenttypes': ['ContentType'], 6 | 'flatpages': ['FlatPage'], 7 | 'gis': [], 8 | 'humanize': [], 9 | 'messages': [], 10 | 'postgres': [], 11 | 'redirects': ['Redirect'], 12 | 'sessions': ['AbstractBaseSession', 'Session'], 13 | 'sitemaps': [], 14 | 'sites': ['Site'], 15 | 'staticfiles': [], 16 | 'syndication': [], 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/django/fields.js: -------------------------------------------------------------------------------- 1 | export const FIELDS = { 2 | AutoField: [], 3 | BigAutoField: [], 4 | BigIntegerField: [], 5 | BinaryField: [], 6 | BooleanField: [], 7 | CharField: ['max_length'], 8 | DateField: ['auto_now', 'auto_now_add'], 9 | DateTimeField: ['auto_now', 'auto_now_add'], 10 | DecimalField: ['max_digits', 'decimal_places'], 11 | DurationField: [], 12 | EmailField: [], 13 | FileField: ['upload_to', 'storage'], 14 | FilePathField: ['path', 'match', 'recursive', 'allow_files', 'allow_folders'], 15 | FloatField: [], 16 | ForeignKey: ['on_delete', 'related_name', 'related_query_name', 'parent_link', 'to_field'], 17 | GenericIPAddressField: ['protocol', 'unpack_ipv4'], 18 | ImageField: ['upload_to', 'width_field', 'height_field'], 19 | IntegerField: [], 20 | ManyToManyField: ['related_name', 'related_query_name', 'symmetrical'], 21 | NullBooleanField: [], 22 | OneToOneField: ['on_delete', 'to_field'], 23 | PositiveIntegerField: [], 24 | PositiveSmallIntegerField: [], 25 | SlugField: [], 26 | SmallIntegerField: [], 27 | TextField: [], 28 | TimeField: ['auto_now', 'auto_now_add'], 29 | URLField: [], 30 | UUIDField: [], 31 | } 32 | 33 | export const RELATIONAL_FIELDS = [ 34 | 'ForeignKey', 'OneToOneField', 'ManyToManyField' 35 | ] 36 | 37 | export const ATTRIBUTES = { 38 | allow_files: {type: Boolean, default: true}, 39 | allow_folders: {type: Boolean, default: false}, 40 | auto_now: {type: Boolean, default: false}, 41 | auto_now_add: {type: Boolean, default: false}, 42 | blank: {type: Boolean, default: false}, 43 | db_index: {type: Boolean, default: false}, 44 | decimal_places: {type: Number, default: null}, 45 | default: {type: String, default: null}, 46 | editable: {type: Boolean, default: true}, 47 | height_field: {type: Number, default: null}, 48 | help_text: {type: String, default: null}, 49 | match: {type: String, default: null}, 50 | max_digits: {type: Number, default: null}, 51 | max_length: {type: Number, default: null}, 52 | 'null': {type: Boolean, default: false}, 53 | on_delete: {type: String, default: null}, 54 | parent_link: {type: Boolean, default: false}, 55 | path: {type: String, default: ''}, 56 | primary_key: {type: Boolean, default: false}, 57 | protocol: {type: String, default: 'both'}, 58 | recursive: {type: Boolean, default: false}, 59 | related_name: {type: String, default: null}, 60 | related_query_name: {type: String, default: null}, 61 | storage: {type: String, default: null}, 62 | symmetrical: {type: Boolean, default: true}, 63 | to_field: {type: String, default: null}, 64 | unique: {type: Boolean, default: false}, 65 | unpack_ipv4: {type: Boolean, default: false}, 66 | upload_to: {type: String, default: ''}, 67 | verbose_name: {type: String, default: null}, 68 | width_field: {type: String, default: null}, 69 | } 70 | 71 | export const COMMON_ATTRIBUTES = [ 72 | 'null', 73 | 'blank', 74 | 'primary_key', 75 | 'unique', 76 | 'db_index', 77 | 'editable', 78 | 'verbose_name', 79 | 'default', 80 | 'help_text', 81 | ] 82 | 83 | 84 | -------------------------------------------------------------------------------- /frontend/src/django/guess.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | 4 | 5 | function guess_common_names(name) { 6 | if (_.startsWith(name, 'is_') || _.startsWith(name, 'has_') || _.startsWith(name, 'have_') || _.startsWith('allow_') || _.startsWith('can_')) 7 | return 'BooleanField' 8 | 9 | if (_.includes(name, 'slug')) 10 | return 'SlugField' 11 | 12 | if (_.includes(name, 'time')) 13 | return 'DateTimeField' 14 | 15 | if (_.includes(name, 'date')) 16 | return 'DateField' 17 | 18 | if (_.includes(name, 'price')) 19 | return 'DecimalField' 20 | 21 | if (_.includes(name, 'duration')) 22 | return 'DurationField' 23 | 24 | if (_.includes(name, 'email')) 25 | return 'EmailField' 26 | 27 | if (_.endsWith(name, 'url')) 28 | return 'URLField' 29 | 30 | if (_.includes(name, 'file') || _.includes(name, 'pdf')) 31 | return 'FileField' 32 | 33 | if (_.includes(name, 'image') || _.includes(name, 'picture') || _.includes(name, 'photo') || _.includes(name, 'avatar')) 34 | return 'ImageField' 35 | 36 | if (_.includes(name, 'text') || _.includes(name, 'description') || _.includes(name, 'message') || _.includes(name, 'intro') || _.includes(name, 'content')) 37 | return 'TextField' 38 | 39 | return null 40 | } 41 | 42 | function guess_relational(name, store) { 43 | let model_names = _.keyBy(store.models_keys(), (key) => { 44 | return key.split('.')[1].toLowerCase() 45 | }) 46 | if (model_names[name] !== undefined) 47 | return {type: 'ForeignKey', relation: model_names[name]} 48 | if (_.endsWith(name, 's')) { 49 | let m2m_name = name.slice(0, -1) 50 | if (model_names[m2m_name] !== undefined) 51 | return {type: 'ManyToManyField', relation: model_names[m2m_name]} 52 | } 53 | if (_.endsWith(name, 'ies')) { // countries > country, industries > industry... 54 | let m2m_name = name.slice(0, -3) + 'y' 55 | if (model_names[m2m_name] !== undefined) 56 | return {type: 'ManyToManyField', relation: model_names[m2m_name]} 57 | } 58 | return null 59 | } 60 | 61 | 62 | 63 | export function guess_type(name, store) { 64 | // tries to guess a type by name (returns dict(type, relation)) 65 | let result = guess_common_names(name) 66 | if (result !== null) { 67 | return {type: result, relation: null} 68 | } 69 | 70 | result = guess_relational(name, store) 71 | if (result !== null) { 72 | return result 73 | } 74 | 75 | // by default we put it as charfield as seems the most common case 76 | return {type: 'CharField', relation: null} 77 | } 78 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Prototyper from './Prototyper.vue' 3 | 4 | 5 | import router from './router' 6 | import VueRouter from 'vue-router' 7 | Vue.use(VueRouter) 8 | 9 | import vOutsideEvents from 'vue-outside-events' 10 | Vue.use(vOutsideEvents) // for
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 | --------------------------------------------------------------------------------