├── .github └── workflows │ └── label.yml ├── .gitignore ├── README.md ├── client ├── .gitignore ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ ├── AppSubForm.vue │ │ ├── FieldSubForm.vue │ │ ├── FlexWrapper.vue │ │ ├── Form.vue │ │ ├── ModelSubForm.vue │ │ ├── ProjectSubForm.vue │ │ └── fields │ │ │ ├── BooleanFieldSubForm.vue │ │ │ ├── CharFieldSubForm.vue │ │ │ ├── DateFieldSubForm.vue │ │ │ ├── DateTimeFieldSubForm.vue │ │ │ ├── DecimalFieldSubForm.vue │ │ │ ├── EmailFieldSubForm.vue │ │ │ ├── FileFieldSubForm.vue │ │ │ ├── ForeignKeyFieldSubForm.vue │ │ │ ├── NoArgFieldSubForm.vue │ │ │ ├── NumberFieldSubForm.vue │ │ │ ├── SlugFieldSubForm.vue │ │ │ ├── TextFieldSubForm.vue │ │ │ ├── TimeFieldSubForm.vue │ │ │ └── URLFieldSubForm.vue │ ├── main.js │ ├── quasar.js │ └── styles │ │ ├── quasar.sass │ │ └── quasar.variables.sass └── vue.config.js ├── images ├── 1.png ├── 2.png ├── 3.png └── 4.png └── server ├── .dockerignore ├── .gitignore ├── Dockerfile ├── dist ├── css │ ├── app.e820ce6b.css │ └── chunk-vendors.c6f8963b.css ├── favicon.ico ├── fonts │ ├── flUhRq6tzZclQEJ-Vdg-IuiaDsNa.463cfa6b.woff │ └── flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.8ff0ce02.woff2 ├── index.html └── js │ ├── app.012999eb.js │ ├── app.012999eb.js.map │ ├── chunk-vendors.3ebf6a6f.js │ └── chunk-vendors.3ebf6a6f.js.map ├── nodemon.json ├── package.json ├── src ├── __test__ │ └── helper.test.ts ├── app.ts ├── bash-helper.model.ts ├── helper.ts ├── index.ts ├── models │ ├── __test__ │ │ ├── app.model.test.ts │ │ └── field.model.test.ts │ ├── app.model.ts │ ├── field.model.ts │ ├── fields │ │ ├── __test__ │ │ │ ├── no-arg-field.model.test.ts │ │ │ └── number-field.model.test.ts │ │ ├── boolean-field.model.ts │ │ ├── decimal-field.model.ts │ │ ├── dt-field.model.ts │ │ ├── file-field.model.ts │ │ ├── foreign-key-field.model.ts │ │ ├── index.ts │ │ ├── no-arg-field.model.ts │ │ ├── number-field.model.ts │ │ └── string-field.model.ts │ ├── model.model.ts │ └── project.model.ts ├── routes │ ├── project.ts │ └── status.ts └── templates │ ├── django-admin.ts │ ├── django-api.ts │ ├── django-app-urls.ts │ ├── django-project-urls.ts │ ├── django-requirements.ts │ ├── django-serializers.ts │ ├── django-settings.ts │ ├── django-utils.ts │ ├── docker-compose.ts │ ├── docker.ts │ └── index.ts └── tsconfig.json /.github/workflows/label.yml: -------------------------------------------------------------------------------- 1 | # This workflow will triage pull requests and apply a label based on the 2 | # paths that are modified in the pull request. 3 | # 4 | # To use this workflow, you will need to set up a .github/labeler.yml 5 | # file with configuration. For more information, see: 6 | # https://github.com/actions/labeler 7 | 8 | name: Labeler 9 | on: [pull_request] 10 | 11 | jobs: 12 | label: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/labeler@v2 18 | with: 19 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django CRUD REST API Generator 2 | 3 | This is a simple tool that generates a Django REST API with the given models. 4 | 5 | Specs: 6 | - Authentication, 7 | - DRF generic views, 8 | - Routes, 9 | - PEP8, 10 | - Dockerized. 11 | 12 | You can test it out [here](https://django-apigen.herokuapp.com/). 13 | 14 | ### To run the generated Django apps 15 | 16 | Docker and docker-compose are required to run the generated apps. 17 | 18 | ```sh 19 | # Unzip the project at 'Downloads' or 'server/projects'. 20 | cd django_project # Go into the generated app 21 | docker-compose up # Run the containers 22 | ``` 23 | 24 | ## Run local version 25 | 26 | Run the server. 27 | ```sh 28 | cd server 29 | npm i 30 | npm run dev 31 | ``` 32 | 33 | Run the client. 34 | ```sh 35 | cd client 36 | npm i 37 | npm run serve 38 | ``` 39 | 40 | ## Screenshots 41 | 1) Choose project and app parameters. 42 | 43 | ![Step 1](/images/1.png) 44 | 45 | 2) Create app models with the form served by Vue. 46 | 47 | ![Step 2](/images/2.png) 48 | 49 | 3) Review your models structure. 50 | 51 | ![Step 3](/images/3.png) 52 | 53 | 4) All these will be generated by the app and can be downloaded. 54 | 55 | ![Step 4](/images/4.png) 56 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "presets": [ 3 | "@vue/cli-plugin-babel/preset" 4 | ], 5 | "plugins": [ 6 | [ 7 | "transform-imports", 8 | { 9 | "quasar": { 10 | "transform": "quasar/dist/babel-transforms/imports.js", 11 | "preventFullImport": true 12 | } 13 | } 14 | ] 15 | ] 16 | } -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-generator-vue-client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "test:unit": "vue-cli-service test:unit" 10 | }, 11 | "dependencies": { 12 | "@quasar/extras": "^1.0.0", 13 | "@vue/test-utils": "^1.0.3", 14 | "axios": "^0.19.2", 15 | "core-js": "^3.6.4", 16 | "quasar": "^1.0.0", 17 | "vue": "^2.6.11", 18 | "vue-github-button": "^1.2.0" 19 | }, 20 | "devDependencies": { 21 | "@vue/cli-plugin-babel": "~4.3.0", 22 | "@vue/cli-plugin-eslint": "~4.3.0", 23 | "@vue/cli-service": "~4.3.0", 24 | "babel-eslint": "^10.1.0", 25 | "babel-plugin-transform-imports": "1.5.0", 26 | "eslint": "^6.7.2", 27 | "eslint-plugin-vue": "^6.2.2", 28 | "node-sass": "^4.13.0", 29 | "sass-loader": "^8.0.0", 30 | "vue-cli-plugin-quasar": "~2.0.2", 31 | "vue-template-compiler": "^2.6.11" 32 | }, 33 | "eslintConfig": { 34 | "root": true, 35 | "env": { 36 | "node": true 37 | }, 38 | "extends": [ 39 | "plugin:vue/essential", 40 | "eslint:recommended" 41 | ], 42 | "parserOptions": { 43 | "parser": "babel-eslint" 44 | }, 45 | "rules": {} 46 | }, 47 | "browserslist": [ 48 | "> 1%", 49 | "last 2 versions", 50 | "not dead" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehmetalpsumer/django-rest-api-generator/9d0ee5266be57dc2a4460bf72097ae78e8cdbe7a/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Django API Generator 8 | 9 | 10 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /client/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehmetalpsumer/django-rest-api-generator/9d0ee5266be57dc2a4460bf72097ae78e8cdbe7a/client/src/assets/logo.png -------------------------------------------------------------------------------- /client/src/components/AppSubForm.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 36 | -------------------------------------------------------------------------------- /client/src/components/FieldSubForm.vue: -------------------------------------------------------------------------------- 1 | 139 | 140 | 220 | -------------------------------------------------------------------------------- /client/src/components/FlexWrapper.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /client/src/components/Form.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 244 | -------------------------------------------------------------------------------- /client/src/components/ModelSubForm.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 76 | -------------------------------------------------------------------------------- /client/src/components/ProjectSubForm.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 50 | -------------------------------------------------------------------------------- /client/src/components/fields/BooleanFieldSubForm.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 36 | -------------------------------------------------------------------------------- /client/src/components/fields/CharFieldSubForm.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 50 | -------------------------------------------------------------------------------- /client/src/components/fields/DateFieldSubForm.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 29 | -------------------------------------------------------------------------------- /client/src/components/fields/DateTimeFieldSubForm.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 29 | -------------------------------------------------------------------------------- /client/src/components/fields/DecimalFieldSubForm.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 77 | -------------------------------------------------------------------------------- /client/src/components/fields/EmailFieldSubForm.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 52 | -------------------------------------------------------------------------------- /client/src/components/fields/FileFieldSubForm.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 43 | -------------------------------------------------------------------------------- /client/src/components/fields/ForeignKeyFieldSubForm.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 56 | -------------------------------------------------------------------------------- /client/src/components/fields/NoArgFieldSubForm.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /client/src/components/fields/NumberFieldSubForm.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 43 | -------------------------------------------------------------------------------- /client/src/components/fields/SlugFieldSubForm.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 54 | -------------------------------------------------------------------------------- /client/src/components/fields/TextFieldSubForm.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 35 | -------------------------------------------------------------------------------- /client/src/components/fields/TimeFieldSubForm.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 29 | -------------------------------------------------------------------------------- /client/src/components/fields/URLFieldSubForm.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 49 | -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import './quasar' 4 | 5 | Vue.config.productionTip = false 6 | 7 | new Vue({ 8 | render: h => h(App), 9 | }).$mount('#app') 10 | -------------------------------------------------------------------------------- /client/src/quasar.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import './styles/quasar.sass' 4 | import '@quasar/extras/material-icons/material-icons.css' 5 | import { Quasar } from 'quasar' 6 | 7 | Vue.use(Quasar, { 8 | config: {}, 9 | components: { /* not needed if importStrategy is not 'manual' */ }, 10 | directives: { /* not needed if importStrategy is not 'manual' */ }, 11 | plugins: { 12 | } 13 | }) -------------------------------------------------------------------------------- /client/src/styles/quasar.sass: -------------------------------------------------------------------------------- 1 | @import './quasar.variables.sass' 2 | @import '~quasar-styl' 3 | // @import '~quasar-addon-styl' 4 | -------------------------------------------------------------------------------- /client/src/styles/quasar.variables.sass: -------------------------------------------------------------------------------- 1 | // It's highly recommended to change the default colors 2 | // to match your app's branding. 3 | 4 | $primary : #027BE3 5 | $secondary : #26A69A 6 | $accent : #9C27B0 7 | 8 | $dark : #1D1D1D 9 | 10 | $positive : #21BA45 11 | $negative : #C10015 12 | $info : #31CCEC 13 | $warning : #F2C037 14 | 15 | @import '~quasar-variables-styl' 16 | -------------------------------------------------------------------------------- /client/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pluginOptions: { 3 | quasar: { 4 | importStrategy: 'kebab', 5 | rtlSupport: false 6 | } 7 | }, 8 | transpileDependencies: [ 9 | 'quasar' 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehmetalpsumer/django-rest-api-generator/9d0ee5266be57dc2a4460bf72097ae78e8cdbe7a/images/1.png -------------------------------------------------------------------------------- /images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehmetalpsumer/django-rest-api-generator/9d0ee5266be57dc2a4460bf72097ae78e8cdbe7a/images/2.png -------------------------------------------------------------------------------- /images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehmetalpsumer/django-rest-api-generator/9d0ee5266be57dc2a4460bf72097ae78e8cdbe7a/images/3.png -------------------------------------------------------------------------------- /images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehmetalpsumer/django-rest-api-generator/9d0ee5266be57dc2a4460bf72097ae78e8cdbe7a/images/4.png -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | build 4 | package-lock.json 5 | projects 6 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | 3 | RUN apt-get update && apt-get install -y python python-pip zip && pip install Django && pip install autopep8 4 | 5 | WORKDIR /app 6 | COPY package.json /app 7 | RUN npm install 8 | COPY . /app 9 | RUN npm run build 10 | 11 | CMD ["node", "build/index.js"] -------------------------------------------------------------------------------- /server/dist/css/chunk-vendors.c6f8963b.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:Material Icons;font-style:normal;font-weight:400;font-display:block;src:url(../fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.8ff0ce02.woff2) format("woff2"),url(../fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.463cfa6b.woff) format("woff")}.material-icons{font-family:Material Icons;font-weight:400;font-style:normal;display:inline-block;line-height:1;text-transform:none;letter-spacing:normal;word-wrap:normal;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga"} -------------------------------------------------------------------------------- /server/dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehmetalpsumer/django-rest-api-generator/9d0ee5266be57dc2a4460bf72097ae78e8cdbe7a/server/dist/favicon.ico -------------------------------------------------------------------------------- /server/dist/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.463cfa6b.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehmetalpsumer/django-rest-api-generator/9d0ee5266be57dc2a4460bf72097ae78e8cdbe7a/server/dist/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.463cfa6b.woff -------------------------------------------------------------------------------- /server/dist/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.8ff0ce02.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehmetalpsumer/django-rest-api-generator/9d0ee5266be57dc2a4460bf72097ae78e8cdbe7a/server/dist/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.8ff0ce02.woff2 -------------------------------------------------------------------------------- /server/dist/index.html: -------------------------------------------------------------------------------- 1 | Django API Generator
-------------------------------------------------------------------------------- /server/dist/js/app.012999eb.js: -------------------------------------------------------------------------------- 1 | (function(e){function t(t){for(var a,o,s=t[0],r=t[1],u=t[2],c=0,m=[];c1}},[l("ProjectSubForm",{ref:"projectSubForm"}),l("q-stepper-navigation",[l("q-btn",{attrs:{color:"primary",label:"Continue"},on:{click:e.toNextStep}})],1)],1),l("q-step",{attrs:{name:2,title:"App",icon:"create_new_folder",done:e.step>2}},[l("AppSubForm",{ref:"appSubForm"}),l("q-stepper-navigation",[l("q-btn",{attrs:{color:"primary",label:"Continue"},on:{click:e.toNextStep}}),l("q-btn",{staticClass:"q-ml-sm",attrs:{flat:"",color:"primary",label:"Back"},on:{click:e.toPrevStep}})],1)],1),l("q-step",{attrs:{name:3,title:"Models",icon:"assignment",done:e.step>3}},[e._l(e.models,(function(t){return l("ModelSubForm",{key:t,ref:"modelSubForms",refInFor:!0,attrs:{id:t},on:{removeModelClicked:function(l){return e.removeModel(t)}}})})),l("q-stepper-navigation",[l("q-btn",{attrs:{color:"primary",label:"Continue"},on:{click:e.toNextStep}}),l("q-btn",{staticClass:"q-ml-sm",attrs:{flat:"",color:"primary",label:"Add model"},on:{click:e.addModel}}),l("q-btn",{staticClass:"q-ml-sm",attrs:{flat:"",color:"primary",label:"Back"},on:{click:e.toPrevStep}})],1)],2),l("q-step",{attrs:{name:4,title:"Review & Complete",icon:"add_comment"}},[e._v(" Review your project settings. "),l("div",{staticClass:"q-pa-md q-gutter-sm"},[l("q-tree",{attrs:{"default-expand-all":!0,nodes:e.treeData,"node-key":"label"}})],1),l("q-stepper-navigation",[l("q-btn",{attrs:{color:"primary",label:"Finish",loading:e.submitted,disable:e.submitted},on:{click:e.submit}}),l("q-btn",{staticClass:"q-ml-sm",attrs:{flat:"",color:"primary",label:"Back"},on:{click:function(t){e.step=3}}})],1)],1)],1),e._m(0)],1)},u=[function(){var e=this,t=e.$createElement,l=e._self._c||t;return l("p",[e._v(" You can check the "),l("a",{attrs:{href:"https://github.com/mehmetalpsumer/django-rest-api-generator/blob/master/README.md",target:"_blank"}},[e._v("README on GitHub")]),e._v(" for the instructions. ")])}],d=(l("99af"),l("c975"),l("d81d"),l("a434"),l("b0c0"),l("d3b7"),l("3ca3"),l("ddb0"),l("2b3d"),l("96cf"),l("1da1")),c=l("bc3a"),m=l.n(c),f=function(){var e=this,t=e.$createElement,l=e._self._c||t;return l("div",{staticClass:"q-pa-md",staticStyle:{"max-width":"400px"}},[l("q-form",{staticClass:"q-gutter-md"},[l("q-input",{attrs:{filled:"",label:"Project name",hint:"Name of the Django project","lazy-rules":"",rules:[function(e){return e&&e.length>0||"Please type something"}]},model:{value:e.project.name,callback:function(t){e.$set(e.project,"name",t)},expression:"project.name"}}),l("q-input",{attrs:{filled:"",type:"number",label:"Port",hint:"Port that Django will run on","lazy-rules":"",rules:[function(e){return null!==e&&""!==e||"Please type something"},function(e){return e>1024&&e<65535||"Please type a valid port number"}]},model:{value:e.project.port,callback:function(t){e.$set(e.project,"port",t)},expression:"project.port"}})],1)],1)},p=[],b={data:function(){return{project:{name:"my_project",port:8e3}}},methods:{getData:function(){return this.project}}},h=b,F=l("2877"),g=l("fe09"),v=Object(F["a"])(h,f,p,!1,null,null,null),x=v.exports;v.options.components=Object.assign(Object.create(v.options.components||null),v.options.components||{},{QForm:g["d"],QInput:g["f"]});var y=function(){var e=this,t=e.$createElement,l=e._self._c||t;return l("div",{staticClass:"q-pa-md",staticStyle:{"max-width":"400px"}},[l("q-form",{staticClass:"q-gutter-md"},[l("q-input",{attrs:{filled:"",label:"App name",hint:"Name of the Django app","lazy-rules":"",rules:[function(e){return e&&e.length>0||"Please type something"}]},model:{value:e.app.name,callback:function(t){e.$set(e.app,"name",t)},expression:"app.name"}})],1)],1)},D=[],q={data:function(){return{app:{name:"my_app"}}},methods:{getData:function(){return this.app}}},k=q,j=Object(F["a"])(k,y,D,!1,null,null,null),S=j.exports;j.options.components=Object.assign(Object.create(j.options.components||null),j.options.components||{},{QForm:g["d"],QInput:g["f"]});var _=function(){var e=this,t=e.$createElement,l=e._self._c||t;return l("div",{staticStyle:{"margin-bottom":"30px"}},[l("q-card-section",[l("div",{staticClass:"text-h6"},[l("div",{staticClass:"row justify-between"},[l("div",{staticClass:"col-4"},[e._v(" Model ")]),l("div",{staticClass:"col-8"},[l("q-btn",{staticStyle:{float:"right"},attrs:{size:"sm",flat:"",icon:"close",label:"Remove model"},on:{click:e.onRemoveModel}}),l("q-btn",{staticStyle:{float:"right"},attrs:{size:"sm",flat:"",icon:"add",label:"Add field"},on:{click:e.addField}})],1)])])]),l("q-card-section",{staticClass:"q-pt-none"},[l("q-form",{staticClass:"q-gutter-md"},[l("q-input",{attrs:{filled:"",label:"Model name","lazy-rules":"",hint:"Recomended to separate words with spaces.",rules:[function(e){return e&&e.length>0||"Please type something"}]},model:{value:e.model.name,callback:function(t){e.$set(e.model,"name",t)},expression:"model.name"}})],1)],1),l("q-separator",{attrs:{inset:""}}),e._l(e.fields,(function(t){return l("FieldSubForm",{key:t,ref:"fields",refInFor:!0,attrs:{id:t},on:{removeFieldClicked:function(l){return e.removeField(t)}}})}))],2)},O=[],C=function(){var e=this,t=e.$createElement,l=e._self._c||t;return l("q-form",{staticClass:"q-gutter-md",staticStyle:{"margin-left":"50px","margin-top":"10px","margin-right":"10px"}},[l("div",{staticClass:"text-h6"},[l("div",{staticClass:"row justify-between"},[l("div",{staticClass:"col-4"},[e._v(" Field ")]),l("div",{staticClass:"col-4"},[l("q-btn",{staticStyle:{float:"right"},attrs:{flat:"",size:"sm",icon:"close"},on:{click:e.onRemoveField}})],1)])]),l("q-input",{attrs:{filled:"",label:"Field name",hint:"Recommended to use snake_case.","lazy-rules":"",rules:[function(e){return e&&e.length>0||"Please type something"}]},model:{value:e.field.name,callback:function(t){e.$set(e.field,"name",t)},expression:"field.name"}}),l("q-select",{attrs:{options:e.fieldTypes,label:"Field Type"},model:{value:e.field.fieldType,callback:function(t){e.$set(e.field,"fieldType",t)},expression:"field.fieldType"}}),l("div",{staticClass:"q-gutter-sm"},[l("q-checkbox",{attrs:{label:"Can be null"},model:{value:e.field.isNull,callback:function(t){e.$set(e.field,"isNull",t)},expression:"field.isNull"}}),l("q-checkbox",{attrs:{label:"Can be blank"},model:{value:e.field.isBlank,callback:function(t){e.$set(e.field,"isBlank",t)},expression:"field.isBlank"}}),l("q-checkbox",{attrs:{label:"Is unique"},model:{value:e.field.isUnique,callback:function(t){e.$set(e.field,"isUnique",t)},expression:"field.isUnique"}}),l("q-checkbox",{attrs:{label:"Is PK"},model:{value:e.field.isPrimaryKey,callback:function(t){e.$set(e.field,"isPrimaryKey",t)},expression:"field.isPrimaryKey"}})],1),"AutoField"===e.field.fieldType?l("NoArgFieldSubForm",{ref:"AutoField"}):e._e(),"BigAutoField"===e.field.fieldType?l("NoArgFieldSubForm",{ref:"BigAutoField"}):e._e(),"DurationField"===e.field.fieldType?l("NoArgFieldSubForm",{ref:"DurationField"}):"SmallAutoField"===e.field.fieldType?l("NoArgFieldSubForm",{ref:"SmallAutoField"}):e._e(),"DateField"===e.field.fieldType?l("DateFieldSubForm",{ref:"DateField"}):e._e(),"DateTimeField"===e.field.fieldType?l("DateFieldSubForm",{ref:"DateTimeField"}):"TimeField"===e.field.fieldType?l("DateFieldSubForm",{ref:"TimeField"}):"BigIntegerField"===e.field.fieldType?l("NumberFieldSubForm",{ref:"BigIntegerField",attrs:{min:"-9223372036854775808",max:"9223372036854775807"}}):"IntegerField"===e.field.fieldType?l("NumberFieldSubForm",{ref:"IntegerField",attrs:{min:"-2147483648",max:"2147483647"}}):"PositiveIntegerField"===e.field.fieldType?l("NumberFieldSubForm",{ref:"PositiveIntegerField",attrs:{min:"0",max:"2147483647"}}):"PositiveSmallIntegerField"===e.field.fieldType?l("NumberFieldSubForm",{ref:"PositiveSmallIntegerField",attrs:{min:"0",max:"32767"}}):"SmallIntegerField"===e.field.fieldType?l("NumberFieldSubForm",{ref:"SmallIntegerField",attrs:{min:"-32768",max:"32767"}}):"BooleanField"===e.field.fieldType?l("BooleanFieldSubForm",{ref:"BooleanField"}):e._e(),"CharField"===e.field.fieldType?l("CharFieldSubForm",{ref:"CharField"}):e._e(),"DecimalField"===e.field.fieldType?l("DecimalFieldSubForm",{ref:"DecimalField"}):e._e(),"EmailField"===e.field.fieldType?l("EmailFieldSubForm",{ref:"EmailField"}):e._e(),"FileField"===e.field.fieldType?l("FileFieldSubForm",{ref:"FileField"}):"ForeignKeyField"===e.field.fieldType?l("ForeignKeyFieldSubForm",{ref:"ForeignKeyField"}):"SlugField"===e.field.fieldType?l("SlugFieldSubForm",{ref:"SlugField"}):"TextField"===e.field.fieldType?l("TextFieldSubForm",{ref:"TextField"}):"URLField"===e.field.fieldType?l("URLFieldSubForm",{ref:"URLField"}):e._e()],1)},$=[],T=l("5530"),w=function(){var e=this,t=e.$createElement,l=e._self._c||t;return l("div",[l("q-form",{staticClass:"q-gutter-md"},[l("div",{staticClass:"q-gutter-sm"},[l("q-checkbox",{attrs:{label:"Has default?"},model:{value:e.booleanField.hasDefault,callback:function(t){e.$set(e.booleanField,"hasDefault",t)},expression:"booleanField.hasDefault"}})],1),l("q-select",{attrs:{options:e.boolOptions,disable:!e.booleanField.hasDefault,label:"Default value"},model:{value:e.booleanField.defaultValue,callback:function(t){e.$set(e.booleanField,"defaultValue",t)},expression:"booleanField.defaultValue"}})],1)],1)},Q=[],P={data:function(){return{booleanField:{defaultValue:"True",hasDefault:!1},boolOptions:["True","False"]}},methods:{getData:function(){return this.booleanField}}},M=P,E=Object(F["a"])(M,w,Q,!1,null,null,null),L=E.exports;E.options.components=Object.assign(Object.create(E.options.components||null),E.options.components||{},{QForm:g["d"],QCheckbox:g["c"],QSelect:g["j"]});var I=function(){var e=this,t=e.$createElement,l=e._self._c||t;return l("div",[l("q-form",{staticClass:"q-gutter-md"},[l("div",{staticClass:"q-gutter-sm"},[l("q-checkbox",{attrs:{label:"Has default?"},model:{value:e.charField.hasDefault,callback:function(t){e.$set(e.charField,"hasDefault",t)},expression:"charField.hasDefault"}})],1),l("q-input",{attrs:{filled:"",label:"Default value",disable:!e.charField.hasDefault,maxlength:e.charField.maxLength},model:{value:e.charField.defaultValue,callback:function(t){e.$set(e.charField,"defaultValue",t)},expression:"charField.defaultValue"}}),l("q-input",{attrs:{filled:"",type:"number",label:"Max length","lazy-rules":"",rules:[function(e){return null!==e&&""!==e||"Max length cannot be blank."},function(e){return e>0||"Max length cannot be negative."}]},model:{value:e.charField.maxLength,callback:function(t){e.$set(e.charField,"maxLength",t)},expression:"charField.maxLength"}})],1)],1)},A=[],N={data:function(){return{charField:{maxLength:32,defaultValue:"",hasDefault:!1}}},methods:{getData:function(){return this.charField}}},V=N,R=Object(F["a"])(V,I,A,!1,null,null,null),B=R.exports;R.options.components=Object.assign(Object.create(R.options.components||null),R.options.components||{},{QForm:g["d"],QCheckbox:g["c"],QInput:g["f"]});var z=function(){var e=this,t=e.$createElement,l=e._self._c||t;return l("div",[l("q-form",{staticClass:"q-gutter-md"},[l("div",{staticClass:"q-gutter-sm"},[l("q-checkbox",{attrs:{label:"auto_now"},model:{value:e.dateField.autoNow,callback:function(t){e.$set(e.dateField,"autoNow",t)},expression:"dateField.autoNow"}}),l("q-checkbox",{attrs:{label:"auto_now_add"},model:{value:e.dateField.autoNowAdd,callback:function(t){e.$set(e.dateField,"autoNowAdd",t)},expression:"dateField.autoNowAdd"}})],1)])],1)},U=[],K={data:function(){return{dateField:{autoNow:!1,autoNowAdd:!1}}},methods:{getData:function(){return this.dateField}}},H=K,G=Object(F["a"])(H,z,U,!1,null,null,null),J=G.exports;G.options.components=Object.assign(Object.create(G.options.components||null),G.options.components||{},{QForm:g["d"],QCheckbox:g["c"]});var W=function(){var e=this,t=e.$createElement,l=e._self._c||t;return l("div",[l("q-form",{staticClass:"q-gutter-md"},[l("div",{staticClass:"q-gutter-sm"},[l("q-checkbox",{attrs:{label:"Has default?"},model:{value:e.decimalField.hasDefault,callback:function(t){e.$set(e.decimalField,"hasDefault",t)},expression:"decimalField.hasDefault"}})],1),l("q-input",{attrs:{filled:"",type:"number",label:"Max digits","lazy-rules":"",rules:[function(e){return null!==e&&""!==e||"Max digits cannot be blank."},function(e){return e>0||"Max digits cannot be negative."}]},model:{value:e.decimalField.maxDigits,callback:function(t){e.$set(e.decimalField,"maxDigits",t)},expression:"decimalField.maxDigits"}}),l("q-input",{attrs:{filled:"",type:"number",label:"Decimal places","lazy-rules":"",rules:[function(e){return null!==e&&""!==e||"Decimal places cannot be blank."},function(e){return e>0||"Decimal places cannot be negative."},function(t){return t0||"Max length cannot be negative."}]},model:{value:e.fileField.maxLength,callback:function(t){e.$set(e.fileField,"maxLength",t)},expression:"fileField.maxLength"}})],1)],1)},ue=[],de={data:function(){return{fileField:{maxLength:100,uploadTo:"uploads/"}}},methods:{getData:function(){return this.fileField}}},ce=de,me=Object(F["a"])(ce,re,ue,!1,null,null,null),fe=me.exports;me.options.components=Object.assign(Object.create(me.options.components||null),me.options.components||{},{QForm:g["d"],QInput:g["f"]});var pe=function(){var e=this,t=e.$createElement,l=e._self._c||t;return l("q-form",[l("q-select",{attrs:{options:e.otherModels,label:"Foreign key to"},model:{value:e.foreignKeyField.model,callback:function(t){e.$set(e.foreignKeyField,"model",t)},expression:"foreignKeyField.model"}}),l("q-select",{attrs:{options:e.onDeleteOptions,label:"On delete"},model:{value:e.foreignKeyField.onDelete,callback:function(t){e.$set(e.foreignKeyField,"onDelete",t)},expression:"foreignKeyField.onDelete"}})],1)},be=[],he={data:function(){return{foreignKeyField:{model:"",onDelete:"models.SET_NULL"},onDeleteOptions:["models.SET_NULL","models.SET_DEFAULT","models.CASCADE"]}},computed:{otherModels:function(){var e=this.getParent("MainForm");return e?e.getModelNames():[]}},methods:{getParent:function(e){var t=this.$parent;while("undefined"!==typeof t){if(t.$options.name===e)return t;t=t.$parent}return!1},getData:function(){return this.foreignKeyField}}},Fe=he,ge=Object(F["a"])(Fe,pe,be,!1,null,null,null),ve=ge.exports;ge.options.components=Object.assign(Object.create(ge.options.components||null),ge.options.components||{},{QForm:g["d"],QSelect:g["j"]});var xe=function(){var e=this,t=e.$createElement,l=e._self._c||t;return l("div",[l("q-form",{staticClass:"q-gutter-md"},[l("div",{staticClass:"q-gutter-sm"},[l("q-checkbox",{attrs:{label:"Has default?"},model:{value:e.slugField.hasDefault,callback:function(t){e.$set(e.slugField,"hasDefault",t)},expression:"slugField.hasDefault"}})],1),l("q-input",{attrs:{filled:"",disable:!e.slugField.hasDefault,maxlength:e.slugField.maxLength,label:"Default value","lazy-rules":"",rules:[function(t){return e.isSlug(t)||"Please enter a valid slug"}]},model:{value:e.slugField.defaultValue,callback:function(t){e.$set(e.slugField,"defaultValue",t)},expression:"slugField.defaultValue"}}),l("q-input",{attrs:{filled:"",type:"number",label:"Max length","lazy-rules":"",rules:[function(e){return null!==e&&""!==e||"Please type something"},function(e){return e>0||"Please type a valid length"}]},model:{value:e.slugField.maxLength,callback:function(t){e.$set(e.slugField,"maxLength",t)},expression:"slugField.maxLength"}})],1)],1)},ye=[],De=(l("4d63"),l("ac1f"),l("25f0"),{data:function(){return{slugField:{maxLength:50,defaultValue:"",hasDefault:!1}}},methods:{getData:function(){return this.slugField},isSlug:function(e){return new RegExp("[a-zA-Z0-9_-]").test(e)}}}),qe=De,ke=Object(F["a"])(qe,xe,ye,!1,null,null,null),je=ke.exports;ke.options.components=Object.assign(Object.create(ke.options.components||null),ke.options.components||{},{QForm:g["d"],QCheckbox:g["c"],QInput:g["f"]});var Se=function(){var e=this,t=e.$createElement,l=e._self._c||t;return l("div",[l("q-form",{staticClass:"q-gutter-md"},[l("div",{staticClass:"q-gutter-sm"},[l("q-checkbox",{attrs:{label:"Has default?"},model:{value:e.textField.hasDefault,callback:function(t){e.$set(e.textField,"hasDefault",t)},expression:"textField.hasDefault"}})],1),l("q-input",{attrs:{filled:"",disable:!e.textField.hasDefault,label:"Default value"},model:{value:e.textField.defaultValue,callback:function(t){e.$set(e.textField,"defaultValue",t)},expression:"textField.defaultValue"}})],1)],1)},_e=[],Oe={data:function(){return{textField:{defaultValue:"",hasDefault:!1}}},methods:{getData:function(){return this.textField}}},Ce=Oe,$e=Object(F["a"])(Ce,Se,_e,!1,null,null,null),Te=$e.exports;$e.options.components=Object.assign(Object.create($e.options.components||null),$e.options.components||{},{QForm:g["d"],QCheckbox:g["c"],QInput:g["f"]});var we=function(){var e=this,t=e.$createElement,l=e._self._c||t;return l("div",[l("q-form",{staticClass:"q-gutter-md"},[l("div",{staticClass:"q-gutter-sm"},[l("q-checkbox",{attrs:{label:"Has default?"},model:{value:e.urlField.hasDefault,callback:function(t){e.$set(e.urlField,"hasDefault",t)},expression:"urlField.hasDefault"}})],1),l("q-input",{attrs:{filled:"",disable:!e.urlField.hasDefault,maxlength:e.urlField.maxLength,label:"Default value","lazy-rules":"",rules:[function(t){return e.isUrl(t)||"Default value must be a valid URL."}]},model:{value:e.urlField.defaultValue,callback:function(t){e.$set(e.urlField,"defaultValue",t)},expression:"urlField.defaultValue"}}),l("q-input",{attrs:{filled:"",type:"number",label:"Max length"},model:{value:e.urlField.maxLength,callback:function(t){e.$set(e.urlField,"maxLength",t)},expression:"urlField.maxLength"}})],1)],1)},Qe=[],Pe={data:function(){return{urlField:{maxLength:200,defaultValue:"",hasDefault:!1}}},methods:{getData:function(){return this.urlField},isUrl:function(e){return/^(ftp|http|https):\/\/[^ "]+$/.test(e)}}},Me=Pe,Ee=Object(F["a"])(Me,we,Qe,!1,null,null,null),Le=Ee.exports;Ee.options.components=Object.assign(Object.create(Ee.options.components||null),Ee.options.components||{},{QForm:g["d"],QCheckbox:g["c"],QInput:g["f"]});var Ie=function(){var e=this,t=e.$createElement,l=e._self._c||t;return l("div")},Ae=[],Ne={methods:{getData:function(){return{}}}},Ve=Ne,Re=Object(F["a"])(Ve,Ie,Ae,!1,null,null,null),Be=Re.exports,ze=function(){var e=this,t=e.$createElement,l=e._self._c||t;return l("div",[l("q-form",{staticClass:"q-gutter-md"},[l("div",{staticClass:"q-gutter-sm"},[l("q-checkbox",{attrs:{label:"Has default?"},model:{value:e.numberField.hasDefault,callback:function(t){e.$set(e.numberField,"hasDefault",t)},expression:"numberField.hasDefault"}})],1),l("q-input",{attrs:{disable:!e.numberField.hasDefault,filled:"",type:"number",label:"Default value","lazy-rules":"",hint:"Values must be between "+e.min+" and "+e.max+".",rules:[function(t){return t>=e.min||"This number is too small!"},function(t){return t<=e.max||"This number is too big!"}]},model:{value:e.numberField.defaultValue,callback:function(t){e.$set(e.numberField,"defaultValue",t)},expression:"numberField.defaultValue"}})],1)],1)},Ue=[],Ke={props:["min","max"],data:function(){return{numberField:{defaultValue:null,hasDefault:!1}}},methods:{getData:function(){return this.numberField}}},He=Ke,Ge=Object(F["a"])(He,ze,Ue,!1,null,null,null),Je=Ge.exports;Ge.options.components=Object.assign(Object.create(Ge.options.components||null),Ge.options.components||{},{QForm:g["d"],QCheckbox:g["c"],QInput:g["f"]});var We={components:{BooleanFieldSubForm:L,CharFieldSubForm:B,DecimalFieldSubForm:te,EmailFieldSubForm:se,FileFieldSubForm:fe,ForeignKeyFieldSubForm:ve,SlugFieldSubForm:je,TextFieldSubForm:Te,URLFieldSubForm:Le,NoArgFieldSubForm:Be,DateFieldSubForm:J,NumberFieldSubForm:Je},props:["id"],data:function(){return{field:{name:"my_field",fieldType:"AutoField",isUnique:!1,isNull:!1,isBlank:!1,isPrimaryKey:!1},fieldTypes:["AutoField","BigAutoField","BigIntegerField","BooleanField","CharField","DateField","DateTimeField","DecimalField","DurationField","EmailField","FileField","ForeignKeyField","IntegerField","PositiveIntegerField","PositiveSmallIntegerField","SlugField","SmallAutoField","SmallIntegerField","TextField","TimeField","URLField"]}},methods:{onRemoveField:function(){this.$emit("removeFieldClicked",this.id)},getData:function(){var e=this.field.fieldType;if(this.$refs[e])return Object(T["a"])({commonOptions:this.field},this.$refs[e].getData())}}},Ye=We,Ze=Object(F["a"])(Ye,C,$,!1,null,null,null),Xe=Ze.exports;Ze.options.components=Object.assign(Object.create(Ze.options.components||null),Ze.options.components||{},{QForm:g["d"],QBtn:g["a"],QInput:g["f"],QSelect:g["j"],QCheckbox:g["c"]});var et={components:{FieldSubForm:Xe},props:["id"],data:function(){return{model:{name:"My Model"},fields:[],fieldCounter:0}},methods:{addField:function(){this.fields.push(this.fieldCounter++)},removeField:function(e){var t=this.fields.indexOf(e);this.fields.splice(t,1)},onRemoveModel:function(){this.$emit("removeModelClicked",this.id)},getData:function(){var e=this.model;return e["fields"]=this.$refs.fields.map((function(e){return e.getData()})),e}},mounted:function(){this.addField()}},tt=et,lt=Object(F["a"])(tt,_,O,!1,null,null,null),at=lt.exports;lt.options.components=Object.assign(Object.create(lt.options.components||null),lt.options.components||{},{QCardSection:g["b"],QBtn:g["a"],QForm:g["d"],QInput:g["f"],QSeparator:g["k"]}),console.log();var nt={name:"MainForm",data:function(){return{host:"https://django-apigen.herokuapp.com",submitted:!1,step:1,models:[],modelCounter:0,formData:{project:{},app:{},models:[]},treeData:[]}},components:{ProjectSubForm:x,AppSubForm:S,ModelSubForm:at},methods:{toNextStep:function(){this.updateData(),this.step=Math.min(++this.step,4)},toPrevStep:function(){this.updateData(),this.step=Math.max(--this.step,1)},updateData:function(){var e;switch(this.step){case 1:e=this.$refs.projectSubForm,this.formData.project=e.getData();break;case 2:e=this.$refs.appSubForm,this.formData.app=e.getData();break;case 3:e=this.$refs.modelSubForms,this.formData.models=e.map((function(e){return e.getData()}));break;default:break}this.updateTreeData()},updateTreeData:function(){var e=this.getJsonData();this.treeData=[{label:e.name,children:[{label:e.app.name,children:e.app.models.map((function(e){return{label:e.name,children:e.fields.map((function(e){return{label:"".concat(e.commonOptions.name," [").concat(e.commonOptions.fieldType,"]")}}))}}))}]}]},addModel:function(){this.models.push(this.modelCounter++)},removeModel:function(e){var t=this.models.indexOf(e);this.models.splice(t,1)},getModelNames:function(){var e=this.$refs.modelSubForms;return e.map((function(e){return e.getData().name}))},getJsonData:function(){var e=this.formData.project;if(e)return e.app=this.formData.app,e.app.models=this.formData.models,e},submit:function(){var e=Object(d["a"])(regeneratorRuntime.mark((function e(){var t,l,a,n,i,o;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return t=this.getJsonData(),this.submitted=!0,e.prev=2,l=this.host+"/api/project",e.next=6,m()({method:"POST",url:l,data:t,responseType:"blob"});case 6:a=e.sent,n=window.URL.createObjectURL(new Blob([a.data])),i=document.createElement("a"),i.href=n,i.setAttribute("download","project.zip"),document.body.appendChild(i),i.click(),this.submitted=!1,e.next=22;break;case 16:e.prev=16,e.t0=e["catch"](2),this.submitted=!1,o="\n An error occurred! :(\n\n Please consider submitting a GitHub issue, thanks.\n\n ".concat(e.t0),alert(o),console.error(e.t0);case 22:case"end":return e.stop()}}),e,this,[[2,16]])})));function t(){return e.apply(this,arguments)}return t}()},mounted:function(){this.addModel()}},it=nt,ot=Object(F["a"])(it,r,u,!1,null,null,null),st=ot.exports;ot.options.components=Object.assign(Object.create(ot.options.components||null),ot.options.components||{},{QStepper:g["m"],QStep:g["l"],QStepperNavigation:g["n"],QBtn:g["a"],QTree:g["q"]});var rt={name:"FlexWrapper",components:{Form:st}},ut=rt,dt=Object(F["a"])(ut,o,s,!1,null,null,null),ct=dt.exports;dt.options.components=Object.assign(Object.create(dt.options.components||null),dt.options.components||{},{QPage:g["h"]});var mt=l("e878"),ft={name:"LayoutDefault",components:{GithubButton:mt["a"],FlexWrapper:ct}},pt=ft,bt=Object(F["a"])(pt,n,i,!1,null,null,null),ht=bt.exports;bt.options.components=Object.assign(Object.create(bt.options.components||null),bt.options.components||{},{QLayout:g["g"],QHeader:g["e"],QToolbar:g["o"],QToolbarTitle:g["p"],QPageContainer:g["i"]});l("c867"),l("e54f");var Ft=l("b05d");a["a"].use(Ft["a"],{config:{},components:{},directives:{},plugins:{}}),a["a"].config.productionTip=!1,new a["a"]({render:function(e){return e(ht)}}).$mount("#app")},c867:function(e,t,l){}}); 2 | //# sourceMappingURL=app.012999eb.js.map -------------------------------------------------------------------------------- /server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "exec": "ts-node ./src/index.ts" 5 | } 6 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zreportapi", 3 | "version": "1.0.0", 4 | "description": "API for Django API generator", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon", 8 | "start": "ts-node src/index.ts", 9 | "build": "tsc", 10 | "test": "jest --watchAll --no-cache", 11 | "postinstall": "mkdir -p projects" 12 | }, 13 | "jest": { 14 | "preset": "ts-jest", 15 | "testEnvironment": "node", 16 | "setupFilesAfterEnv": [] 17 | }, 18 | "author": "Mehmet Alp Sümer", 19 | "license": "ISC", 20 | "dependencies": { 21 | "@types/shelljs": "^0.8.8", 22 | "body-parser": "^1.19.0", 23 | "cors": "^2.8.5", 24 | "express": "^4.17.1", 25 | "shelljs": "^0.8.4" 26 | }, 27 | "devDependencies": { 28 | "@types/body-parser": "^1.19.0", 29 | "@types/cors": "^2.8.6", 30 | "@types/express": "^4.17.6", 31 | "@types/jest": "^26.0.4", 32 | "@types/node": "^13.11.1", 33 | "@types/supertest": "^2.0.10", 34 | "jest": "^26.1.0", 35 | "nodemon": "^2.0.3", 36 | "supertest": "^4.0.2", 37 | "ts-jest": "^26.1.2", 38 | "ts-node": "^8.10.2", 39 | "typescript": "^3.8.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server/src/__test__/helper.test.ts: -------------------------------------------------------------------------------- 1 | import { slugify, execCmd } from '../helper'; 2 | 3 | it("slugifies strings correctly", () => { 4 | const str = "To ße Sluğified"; 5 | const expectedSlug = "to_sse_slugified"; 6 | const slug = slugify(str); 7 | 8 | expect(slug).toEqual(expectedSlug); 9 | }); 10 | 11 | it("throws an error for invalid cmd", async () => { 12 | const invalidCmd = "Invalid Cmd"; 13 | await expect(execCmd([invalidCmd], "This will not run")).rejects.toThrow(); 14 | }); 15 | -------------------------------------------------------------------------------- /server/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import bodyParser from "body-parser"; 3 | import cors from "cors"; 4 | import { projectRouter } from './routes/project'; 5 | import { statusRouter } from './routes/status'; 6 | 7 | const app = express(); 8 | 9 | app.use(bodyParser.json({ limit: "50mb" })); 10 | app.use(bodyParser.urlencoded({ limit: "50mb", extended: true })); 11 | app.use(cors()); 12 | app.use(express.static("dist")); 13 | app.use(express.static("projects")); 14 | 15 | // Routes 16 | app.use(projectRouter); 17 | app.use(statusRouter); 18 | 19 | app.all("*", async (req, res) => { 20 | try { 21 | res.sendFile("../dist/index.html"); 22 | } catch(error) { 23 | res.status(404).send({ error: error, message: 'Not found.' }); 24 | } 25 | 26 | }); 27 | 28 | export { app }; 29 | -------------------------------------------------------------------------------- /server/src/bash-helper.model.ts: -------------------------------------------------------------------------------- 1 | import * as sh from "shelljs"; 2 | import { writeFileSync, copyFileSync, readFileSync } from "fs"; 3 | 4 | export class BashHelper { 5 | /** 6 | * Executes a bash command, and returns executed command. 7 | * @param cmd Command to be executed 8 | * @param description Description of the task to be executed 9 | */ 10 | private static async execCmd(cmd: string[], description: string = "") { 11 | console.log(`=> Action: ${description}...`); 12 | const runCmd = sh.exec(cmd.join(" ")); 13 | 14 | if (!runCmd.stderr) { 15 | console.log(`=> Completed: ${description}!`); 16 | return cmd.join(" "); 17 | } else { 18 | console.log(`=> Error: ${description}`); 19 | throw new Error(description); 20 | } 21 | } 22 | 23 | /** 24 | * Creates a directory 25 | * @param directory Path of the directory to be created 26 | * @param createNested If selected, all nested dirs will be created 27 | */ 28 | public static async createDirectory( 29 | directory: string, 30 | createNested: boolean = false 31 | ) { 32 | const cmd = ["mkdir", ...(createNested ? ["-p"] : []), directory]; 33 | const action = `Create directory: ${directory}`; 34 | await BashHelper.execCmd(cmd, action); 35 | } 36 | 37 | /** 38 | * Creates an empty file 39 | * @param directory Directory where the file will be placed 40 | * @param fileName Name of the file with the extension 41 | */ 42 | public static async createFile(directory: string, fileName: string) { 43 | const fileDir = `${directory}/${fileName}`; 44 | const touchCmd = ["touch", fileDir]; 45 | const touchAction = `Create file: ${fileDir}`; 46 | await BashHelper.execCmd(touchCmd, touchAction); 47 | } 48 | 49 | /** 50 | * Writes a string to a file 51 | * @param filePath Path of the file 52 | * @param content Content to be written 53 | * @param append Append or overwrite 54 | */ 55 | public static async writeToFile(filePath: string, content: string, append: boolean = false) { 56 | if (append) { 57 | content = readFileSync(filePath) + content; 58 | } 59 | writeFileSync(filePath, content); 60 | } 61 | 62 | public static async copyFile(sourceDir: string, targetDir: string) { 63 | copyFileSync(sourceDir, targetDir); 64 | } 65 | 66 | /** 67 | * Creates a new Django project 68 | * @param projectDir Directory where the Django project will be placed 69 | * @param projectName Name of the Django project 70 | */ 71 | public static async createDjangoProject( 72 | projectDir: string, 73 | projectName: string 74 | ) { 75 | const cmd = ["django-admin", "startproject", projectName, projectDir]; 76 | const action = `Create Django project: ${projectDir} ${projectName}`; 77 | await BashHelper.execCmd(cmd, action); 78 | } 79 | 80 | /** 81 | * Creates a new Django app 82 | * @param projectDir Project directory where the Django app will be created 83 | * @param appName Name of the Django app 84 | */ 85 | public static async createDjangoApp(projectDir: string, appName: string) { 86 | const cmd = ["django-admin", "startapp", appName, projectDir]; 87 | const action = `Create Django app: ${projectDir} ${appName}`; 88 | await BashHelper.execCmd(cmd, action); 89 | } 90 | 91 | /** 92 | * Formats Python project with PEP8 standards 93 | * @param projectDir Path of the proejct to be formatted 94 | */ 95 | public static async formatProjectWithAutopep8(projectDir: string) { 96 | const cmd = ["autopep8", "--in-place", "--recursive", projectDir]; 97 | const action = `Format project files: ${projectDir}`; 98 | await BashHelper.execCmd(cmd, action); 99 | } 100 | 101 | /** 102 | * Compress a directory into a ZIP file 103 | * @param directory Directory to be compressed 104 | */ 105 | public static async compressDirectory(directory: string) { 106 | const cmd = ["zip", "-r", `${directory}.zip`, directory]; 107 | const action = `Compress directory: ${directory}`; 108 | await BashHelper.execCmd(cmd, action); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /server/src/helper.ts: -------------------------------------------------------------------------------- 1 | import * as sh from "shelljs"; 2 | import { Project, ProjectJson } from "./models/project.model"; 3 | import { App } from "./models/app.model"; 4 | import { Model } from "./models/model.model"; 5 | import { 6 | StringField, 7 | NumberField, 8 | DtField, 9 | BooleanField, 10 | DecimalField, 11 | FileField, 12 | ForeignKeyField, 13 | NoArgField 14 | } from "./models/fields/index"; 15 | 16 | 17 | const parseJsonProject = (projectJson: ProjectJson): Project => { 18 | console.log(projectJson); 19 | const project = new Project(projectJson.name, projectJson.port); 20 | const appJson = projectJson.app; 21 | const app = new App(appJson.name); 22 | 23 | for (let j = 0; j < appJson.models.length; j++) { 24 | const modelJson = appJson.models[j]; 25 | const model = new Model(modelJson.name); 26 | 27 | for (let k = 0; k < modelJson.fields.length; k++) { 28 | const fieldJson = modelJson.fields[k]; 29 | 30 | switch (fieldJson.commonOptions.fieldType) { 31 | case "AutoField": 32 | case "BigAutoField": 33 | case "DurationField": 34 | case "SmallAutoField": 35 | model.fields.push(new NoArgField(fieldJson.commonOptions)); 36 | break; 37 | case "BigIntegerField": 38 | case "IntegerField": 39 | case "PositiveIntegerField": 40 | case "PositiveSmallIntegerField": 41 | case "SmallIntegerField": 42 | model.fields.push( 43 | new NumberField(fieldJson.commonOptions, fieldJson.defaultValue, fieldJson.hasDefault!) 44 | ); 45 | break; 46 | case "CharField": 47 | case "EmailField": 48 | case "SlugField": 49 | case "TextField": 50 | case "URLField": 51 | model.fields.push( 52 | new StringField( 53 | fieldJson.commonOptions, 54 | fieldJson.maxLength, 55 | fieldJson.defaultValue, 56 | fieldJson.hasDefault 57 | ) 58 | ); 59 | break; 60 | case "DateField": 61 | case "DateTimeField": 62 | case "TimeField": 63 | model.fields.push( 64 | new DtField( 65 | fieldJson.commonOptions, 66 | fieldJson.autoNow || false, 67 | fieldJson.autoNowAdd || false 68 | ) 69 | ); 70 | break; 71 | case "DecimalField": 72 | model.fields.push( 73 | new DecimalField( 74 | fieldJson.commonOptions, 75 | fieldJson.maxDigits, 76 | fieldJson.decimalPlaces, 77 | fieldJson.defaultValue, 78 | fieldJson.hasDefault! 79 | ) 80 | ); 81 | break; 82 | case "BooleanField": 83 | model.fields.push( 84 | new BooleanField( 85 | fieldJson.commonOptions, 86 | fieldJson.defaultValue, 87 | fieldJson.hasDefault! 88 | ) 89 | ); 90 | break; 91 | case "FileField": 92 | model.fields.push( 93 | new FileField( 94 | fieldJson.commonOptions, 95 | fieldJson.maxLength, 96 | fieldJson.uploadTo 97 | ) 98 | ); 99 | break; 100 | case "ForeignKeyField": 101 | model.fields.push( 102 | new ForeignKeyField( 103 | fieldJson.commonOptions, 104 | fieldJson.model!, 105 | fieldJson.onDelete! 106 | ) 107 | ); 108 | break; 109 | default: 110 | break; 111 | } 112 | } 113 | 114 | app.models.push(model); 115 | } 116 | project.app = app; 117 | 118 | return project; 119 | }; 120 | 121 | const execCmd = async (cmd: string[], action: string) => { 122 | console.log(`=> Action: ${action}...`); 123 | const runCmd = sh.exec(cmd.join(" ")); 124 | 125 | if (!runCmd.stderr) { 126 | console.log(`=> Completed: ${action}!`); 127 | } else { 128 | console.log(`=> Error: ${action}`); 129 | throw new Error(action); 130 | } 131 | }; 132 | 133 | const slugify = (text: string, separator: string = "_") => { 134 | let slug = text.toString().toLowerCase().trim(); 135 | 136 | const sets = [ 137 | { to: "a", from: "[ÀÁÂÃÅÆĀĂĄẠẢẤẦẨẪẬẮẰẲẴẶ]" }, 138 | { to: "ae", from: "[Ä]" }, 139 | { to: "c", from: "[ÇĆĈČ]" }, 140 | { to: "d", from: "[ÐĎĐÞ]" }, 141 | { to: "e", from: "[ÈÉÊËĒĔĖĘĚẸẺẼẾỀỂỄỆ]" }, 142 | { to: "g", from: "[ĜĞĢǴ]" }, 143 | { to: "h", from: "[ĤḦ]" }, 144 | { to: "i", from: "[ÌÍÎÏĨĪĮİỈỊ]" }, 145 | { to: "j", from: "[Ĵ]" }, 146 | { to: "ij", from: "[IJ]" }, 147 | { to: "k", from: "[Ķ]" }, 148 | { to: "l", from: "[ĹĻĽŁ]" }, 149 | { to: "m", from: "[Ḿ]" }, 150 | { to: "n", from: "[ÑŃŅŇ]" }, 151 | { to: "o", from: "[ÒÓÔÕØŌŎŐỌỎỐỒỔỖỘỚỜỞỠỢǪǬƠ]" }, 152 | { to: "oe", from: "[ŒÖ]" }, 153 | { to: "p", from: "[ṕ]" }, 154 | { to: "r", from: "[ŔŖŘ]" }, 155 | { to: "s", from: "[ŚŜŞŠ]" }, 156 | { to: "ss", from: "[ß]" }, 157 | { to: "t", from: "[ŢŤ]" }, 158 | { to: "u", from: "[ÙÚÛŨŪŬŮŰŲỤỦỨỪỬỮỰƯ]" }, 159 | { to: "ue", from: "[Ü]" }, 160 | { to: "w", from: "[ẂŴẀẄ]" }, 161 | { to: "x", from: "[ẍ]" }, 162 | { to: "y", from: "[ÝŶŸỲỴỶỸ]" }, 163 | { to: "z", from: "[ŹŻŽ]" }, 164 | { to: "-", from: "[·/_,:;']" }, 165 | ]; 166 | 167 | sets.forEach((set) => { 168 | slug = slug.replace(new RegExp(set.from, "gi"), set.to); 169 | }); 170 | 171 | slug = slug 172 | .toString() 173 | .toLowerCase() 174 | .replace(/\s+/g, "-") // Replace spaces with - 175 | .replace(/&/g, "-and-") // Replace & with 'and' 176 | .replace(/[^\w\-]+/g, "") // Remove all non-word chars 177 | .replace(/\--+/g, "-") // Replace multiple - with single - 178 | .replace(/^-+/, "") // Trim - from start of text 179 | .replace(/-+$/, ""); // Trim - from end of text 180 | 181 | if (typeof separator !== "undefined" && separator !== "-") { 182 | slug = slug.replace(/-/g, separator); 183 | } 184 | 185 | return slug; 186 | } 187 | 188 | export { parseJsonProject, execCmd, slugify }; 189 | -------------------------------------------------------------------------------- /server/src/index.ts: -------------------------------------------------------------------------------- 1 | import { app } from "./app"; 2 | 3 | const port = process.env.PORT || 3000; 4 | const start = async () => { 5 | app.listen(port, () => { 6 | console.log("Listening on " + port); 7 | }); 8 | }; 9 | 10 | start(); 11 | -------------------------------------------------------------------------------- /server/src/models/__test__/app.model.test.ts: -------------------------------------------------------------------------------- 1 | import { App } from '../app.model'; 2 | 3 | it("should convert app name to slug", () => { 4 | const appName1 = "My App 1"; 5 | const formattedAppName1 = "my_app_1"; 6 | 7 | const appName2 = "*1App"; 8 | const formattedAppName2 = "1app"; 9 | 10 | const appName3 = "This App Haş Weird Çharacterş"; 11 | const formattedAppName3 = "this_app_has_weird_characters"; 12 | 13 | const app1 = new App(appName1); 14 | const app2 = new App(appName2); 15 | const app3 = new App(appName3); 16 | 17 | expect(app1.name).toEqual(formattedAppName1); 18 | expect(app2.name).toEqual(formattedAppName2); 19 | expect(app3.name).toEqual(formattedAppName3); 20 | }); 21 | -------------------------------------------------------------------------------- /server/src/models/__test__/field.model.test.ts: -------------------------------------------------------------------------------- 1 | import { Field } from '../field.model'; 2 | 3 | it("formats field name with snake_case", () => { 4 | const name1 = "Field Name"; 5 | const formattedName1 = "field_name"; 6 | 7 | const name2 = "fieldName"; 8 | const formattedName2 = "fieldname"; 9 | 10 | const name3 = "field Name"; 11 | const formattedName3 = "field_name"; 12 | 13 | const field1 = new Field({ name: name1, fieldType: 'CharField' }); 14 | const field2 = new Field({ name: name2, fieldType: 'CharField' }); 15 | const field3 = new Field({ name: name3, fieldType: "CharField" }); 16 | 17 | expect(field1.formatFieldName()).toEqual(formattedName1); 18 | expect(field2.formatFieldName()).toEqual(formattedName2); 19 | expect(field3.formatFieldName()).toEqual(formattedName3); 20 | }); 21 | 22 | it("converts common options to string correctly", () => { 23 | const commonOptionsStr = 24 | "null=False,blank=False,unique=False,primary_key=False"; 25 | const field = new Field({ name: "field", fieldType: "CharField" }); 26 | expect(field.commonOptionsToString()).toEqual(commonOptionsStr); 27 | }); 28 | 29 | it("converts boolean values to Python equivalent", () => { 30 | const field = new Field({ name: "field", fieldType: "CharField" }); 31 | 32 | expect(field.boolToString(true)).toEqual("True"); 33 | expect(field.boolToString(false)).toEqual("False"); 34 | expect(field.boolToString(undefined)).toEqual("False"); 35 | }); 36 | -------------------------------------------------------------------------------- /server/src/models/app.model.ts: -------------------------------------------------------------------------------- 1 | import { slugify } from "../helper"; 2 | import { Model } from "./model.model"; 3 | 4 | class App { 5 | name: string; 6 | models: Model[]; 7 | 8 | constructor(name: string, models: Model[] = []) { 9 | this.name = slugify(name); 10 | this.models = models; 11 | } 12 | } 13 | 14 | export { App }; 15 | -------------------------------------------------------------------------------- /server/src/models/field.model.ts: -------------------------------------------------------------------------------- 1 | interface CommonFieldOptions { 2 | name: string; 3 | fieldType: string; 4 | isNull?: boolean; 5 | isBlank?: boolean; 6 | editable?: boolean; 7 | helpText?: string; 8 | isPrimaryKey?: boolean; 9 | isUnique?: boolean; 10 | } 11 | 12 | const defaultOptions: CommonFieldOptions = { 13 | name: "default_field", 14 | fieldType: "TextField", 15 | isNull: false, 16 | isBlank: false, 17 | isPrimaryKey: false, 18 | isUnique: false, 19 | }; 20 | 21 | class Field { 22 | fieldType: string; 23 | name: string; 24 | isNull?: boolean; 25 | isBlank?: boolean; 26 | isRequired?: boolean; 27 | isUnique?: boolean; 28 | isPrimaryKey?: boolean; 29 | 30 | constructor(commonFields: CommonFieldOptions) { 31 | this.fieldType = commonFields.fieldType; 32 | this.name = commonFields.name; 33 | this.isNull = commonFields.isNull || defaultOptions.isNull; 34 | this.isBlank = commonFields.isBlank || defaultOptions.isBlank; 35 | this.isUnique = commonFields.isUnique || defaultOptions.isUnique; 36 | this.isPrimaryKey = 37 | commonFields.isPrimaryKey || defaultOptions.isPrimaryKey; 38 | } 39 | 40 | public formatFieldName(): string { 41 | return this.name 42 | .toLowerCase() 43 | .replace(" ", "_") 44 | .replace(/[^a-z|_]/gi, ""); 45 | } 46 | 47 | public commonOptionsToString(): string { 48 | const options = [ 49 | `null=${this.boolToString(this.isNull)}`, 50 | `blank=${this.boolToString(this.isBlank)}`, 51 | `unique=${this.boolToString(this.isUnique)}`, 52 | `primary_key=${this.boolToString(this.isPrimaryKey)}`, 53 | ]; 54 | return options.join(","); 55 | } 56 | 57 | public boolToString(bool: boolean | undefined | null) { 58 | if (!bool) return "False"; 59 | return bool == true ? "True" : "False"; 60 | } 61 | 62 | public createArgString(name: string, value: any, qutoes: boolean): string | null { 63 | if (!name || !value) { 64 | return null; 65 | } 66 | 67 | return qutoes ? `${name}='${value}'` : `${name}=${value}`; 68 | } 69 | } 70 | 71 | export { Field, CommonFieldOptions }; 72 | -------------------------------------------------------------------------------- /server/src/models/fields/__test__/no-arg-field.model.test.ts: -------------------------------------------------------------------------------- 1 | import { NoArgField } from '../no-arg-field.model'; 2 | import { CommonFieldOptions } from '../../field.model'; 3 | 4 | 5 | it("creates duration field string correctly with no args", () => { 6 | const commonFields: CommonFieldOptions = { 7 | name: 'no_arg_field', 8 | fieldType: 'DurationField' 9 | }; 10 | 11 | const durationField = new NoArgField(commonFields); 12 | const commonOptionsStr = 13 | "null=False,blank=False,unique=False,primary_key=False"; 14 | const fieldStr = `${commonFields.name} = models.${commonFields.fieldType}(${commonOptionsStr})`; 15 | expect(durationField.toString()).toEqual(fieldStr); 16 | }); 17 | 18 | it("creates auto field string correctly with no args", () => { 19 | const commonFields: CommonFieldOptions = { 20 | name: "no_arg_field", 21 | fieldType: "AutoField", 22 | }; 23 | 24 | const autoField = new NoArgField(commonFields); 25 | const commonOptionsStr = 26 | "null=False,blank=False,unique=False,primary_key=False"; 27 | const fieldStr = `${commonFields.name} = models.${commonFields.fieldType}(${commonOptionsStr})`; 28 | expect(autoField.toString()).toEqual(fieldStr); 29 | }); 30 | 31 | it("creates big auto field string correctly with no args", () => { 32 | const commonFields: CommonFieldOptions = { 33 | name: "no_arg_field", 34 | fieldType: "BigAutoField", 35 | }; 36 | 37 | const autoField = new NoArgField(commonFields); 38 | const commonOptionsStr = 39 | "null=False,blank=False,unique=False,primary_key=False"; 40 | const fieldStr = `${commonFields.name} = models.${commonFields.fieldType}(${commonOptionsStr})`; 41 | expect(autoField.toString()).toEqual(fieldStr); 42 | }); 43 | 44 | it("creates small auto field string correctly with no args", () => { 45 | const commonFields: CommonFieldOptions = { 46 | name: "no_arg_field", 47 | fieldType: "SmallAutoField", 48 | }; 49 | 50 | const autoField = new NoArgField(commonFields); 51 | const commonOptionsStr = 52 | "null=False,blank=False,unique=False,primary_key=False"; 53 | const fieldStr = `${commonFields.name} = models.${commonFields.fieldType}(${commonOptionsStr})`; 54 | expect(autoField.toString()).toEqual(fieldStr); 55 | }); 56 | 57 | it("creates no arg field string correctly with args", () => { 58 | const commonFields: CommonFieldOptions = { 59 | name: "no_arg_field", 60 | fieldType: "SmallAutoField", 61 | isNull: true, 62 | isPrimaryKey: true 63 | }; 64 | 65 | const autoField = new NoArgField(commonFields); 66 | const commonOptionsStr = 67 | "null=True,blank=False,unique=False,primary_key=True"; 68 | const fieldStr = `${commonFields.name} = models.${commonFields.fieldType}(${commonOptionsStr})`; 69 | expect(autoField.toString()).toEqual(fieldStr); 70 | }); 71 | -------------------------------------------------------------------------------- /server/src/models/fields/__test__/number-field.model.test.ts: -------------------------------------------------------------------------------- 1 | import { NumberField } from '../number-field.model'; 2 | 3 | it("doesnt write default value when it is null", () => { 4 | const commonFields = { 5 | name: "number_field", 6 | fieldType: "IntegerField" 7 | }; 8 | 9 | const intField = new NumberField(commonFields, null, false); 10 | 11 | expect(intField.toString()).not.toContain(`default=null`); 12 | }); 13 | -------------------------------------------------------------------------------- /server/src/models/fields/boolean-field.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, CommonFieldOptions } from "../field.model"; 2 | 3 | export class BooleanField extends Field { 4 | defaultValue: boolean; 5 | hasDefault: boolean; 6 | 7 | constructor(commonFields: CommonFieldOptions, defaultValue: boolean, hasDefault: boolean) { 8 | super(commonFields); 9 | this.defaultValue = defaultValue; 10 | this.hasDefault = hasDefault; 11 | } 12 | 13 | public toString(): string { 14 | const fieldName = this.formatFieldName(); 15 | const args = [ 16 | this.commonOptionsToString() 17 | ]; 18 | 19 | const defaultValueStr = this.createArgString( 20 | "default", 21 | this.boolToString(this.defaultValue), 22 | false 23 | ); 24 | 25 | if (this.hasDefault && defaultValueStr) { 26 | args.push(defaultValueStr); 27 | } 28 | 29 | return `${fieldName} = models.BooleanField(${args.join(",")})`; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/src/models/fields/decimal-field.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, CommonFieldOptions } from "../field.model"; 2 | 3 | export class DecimalField extends Field { 4 | maxDigits?: number | null; 5 | decimalPlaces?: number | null; 6 | defaultValue: string; 7 | hasDefault: boolean; 8 | 9 | constructor( 10 | commonFields: CommonFieldOptions, 11 | maxDigits: number | undefined, 12 | decimalPlaces: number | undefined, 13 | defaultValue: string, 14 | hasDefault: boolean, 15 | ) { 16 | super(commonFields); 17 | this.maxDigits = maxDigits; 18 | this.decimalPlaces = decimalPlaces; 19 | this.defaultValue = defaultValue ? defaultValue.toString() : ""; 20 | this.hasDefault = hasDefault; 21 | } 22 | 23 | public toString(): string { 24 | const fieldName = this.formatFieldName(); 25 | 26 | const args = [ 27 | this.createArgString("max_digits", this.maxDigits, false), 28 | this.createArgString("decimal_places", this.decimalPlaces, false), 29 | this.commonOptionsToString(), 30 | ]; 31 | 32 | const defaultValueStr = this.createArgString( 33 | "default", 34 | this.defaultValue, 35 | true 36 | ); 37 | if (defaultValueStr && this.hasDefault) { 38 | args.push(defaultValueStr); 39 | } 40 | 41 | return `${fieldName} = models.DecimalField(${args.join(",")})`; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /server/src/models/fields/dt-field.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, CommonFieldOptions } from "../field.model"; 2 | 3 | export class DtField extends Field { 4 | autoNow: boolean; 5 | autoNowAdd: boolean; 6 | 7 | constructor( 8 | commonFields: CommonFieldOptions, 9 | autoNow: boolean, 10 | autoNowAdd: boolean 11 | ) { 12 | super(commonFields); 13 | this.autoNow = autoNow; 14 | this.autoNowAdd = autoNowAdd; 15 | } 16 | 17 | public toString(): string { 18 | const fieldName = this.formatFieldName(); 19 | const args = [ 20 | this.createArgString("auto_now", this.boolToString(this.autoNow), false), 21 | this.createArgString( 22 | "auto_now_add", 23 | this.boolToString(this.autoNowAdd), 24 | false 25 | ), 26 | super.commonOptionsToString(), 27 | ]; 28 | return `${fieldName} = models.${this.fieldType}(${args.join(",")})`; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/src/models/fields/file-field.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, CommonFieldOptions } from "../field.model"; 2 | 3 | export class FileField extends Field { 4 | maxLength: number | undefined; 5 | uploadTo?: string | undefined; 6 | 7 | constructor( 8 | commonFields: CommonFieldOptions, 9 | maxLength: number | undefined, 10 | uploadTo: string | undefined 11 | ) { 12 | super(commonFields); 13 | this.maxLength = maxLength; 14 | this.uploadTo = uploadTo; 15 | } 16 | 17 | public toString(): string { 18 | const fieldName = this.formatFieldName(); 19 | const args = [ 20 | this.createArgString("max_length", this.maxLength, false), 21 | this.createArgString("upload_to", this.uploadTo, true), 22 | this.commonOptionsToString() 23 | ]; 24 | return `${fieldName} = models.FileField(${args.join(",")})`; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/src/models/fields/foreign-key-field.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, CommonFieldOptions } from '../field.model'; 2 | 3 | export class ForeignKeyField extends Field { 4 | fkModel: string; 5 | onDelete: string; 6 | 7 | constructor( 8 | commonFields: CommonFieldOptions, 9 | fkModel: string, 10 | onDelete: string 11 | ) { 12 | super(commonFields); 13 | this.fkModel = fkModel; 14 | this.onDelete = onDelete; 15 | } 16 | 17 | public toString(): string { 18 | const fieldName = this.formatFieldName(); 19 | const args = [ 20 | `'${this.fkModel}'`, 21 | this.createArgString("on_delete", this.onDelete, true), 22 | this.commonOptionsToString(), 23 | ]; 24 | 25 | return `${fieldName} = models.ForeignKey(${args.join(",")})`; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /server/src/models/fields/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./boolean-field.model"; 2 | export * from "./decimal-field.model"; 3 | export * from "./file-field.model"; 4 | export * from "./foreign-key-field.model"; 5 | 6 | export * from "./string-field.model"; 7 | export * from "./number-field.model"; 8 | export * from "./no-arg-field.model"; 9 | export * from "./dt-field.model"; 10 | -------------------------------------------------------------------------------- /server/src/models/fields/no-arg-field.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, CommonFieldOptions } from "../field.model"; 2 | 3 | export class NoArgField extends Field { 4 | constructor(commonFields: CommonFieldOptions) { 5 | super(commonFields); 6 | } 7 | 8 | public toString(): string { 9 | const fieldName = this.formatFieldName(); 10 | const fieldType = this.fieldType; 11 | const args = [ 12 | this.commonOptionsToString() 13 | ]; 14 | 15 | return `${fieldName} = models.${fieldType}(${args.join(",")})`; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/src/models/fields/number-field.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, CommonFieldOptions } from "../field.model"; 2 | 3 | export class NumberField extends Field { 4 | defaultValue?: number | null; 5 | hasDefault: boolean; 6 | 7 | constructor(commonFields: CommonFieldOptions, defaultValue: number | null, hasDefault: boolean) { 8 | super(commonFields); 9 | this.defaultValue = defaultValue ? defaultValue : null; 10 | this.hasDefault = hasDefault; 11 | } 12 | 13 | public toString(): string { 14 | const fieldName = this.formatFieldName(); 15 | const fieldType = this.fieldType; 16 | const defaultValueStr = this.createArgString("default", this.defaultValue, false); 17 | 18 | const args = [ 19 | this.commonOptionsToString(), 20 | ]; 21 | 22 | if (defaultValueStr && this.hasDefault) { 23 | args.push(defaultValueStr); 24 | } 25 | return `${fieldName} = models.${fieldType}(${args./*filter(Boolean).*/join(",")})`; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /server/src/models/fields/string-field.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, CommonFieldOptions } from "../field.model"; 2 | 3 | export class StringField extends Field { 4 | maxLength: number | undefined; 5 | defaultValue?: string; 6 | hasDefault?: boolean; 7 | 8 | constructor( 9 | commonFields: CommonFieldOptions, 10 | maxLength: number | undefined, 11 | defaultValue: string | undefined, 12 | hasDefault?: boolean | undefined 13 | ) { 14 | super(commonFields); 15 | this.maxLength = maxLength; 16 | this.defaultValue = defaultValue ? defaultValue.toString() : ""; 17 | this.hasDefault = hasDefault; 18 | } 19 | 20 | public toString(): string { 21 | const fieldName = this.formatFieldName(); 22 | const fieldType = this.fieldType; 23 | const defaultValue = this.createArgString("default", this.defaultValue, true); 24 | 25 | const args = [ 26 | this.commonOptionsToString(), 27 | ]; 28 | 29 | if (this.hasDefault && defaultValue) { 30 | args.push(defaultValue); 31 | } 32 | 33 | if (fieldType !== "TextField") { 34 | args.push(this.createArgString("max_length", this.maxLength, false)!); 35 | } 36 | 37 | return `${fieldName} = models.${fieldType}(${args.join(',')})`; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/src/models/model.model.ts: -------------------------------------------------------------------------------- 1 | import { Field } from './field.model'; 2 | 3 | export class Model { 4 | name: string; 5 | depth: number; 6 | fields: Field[]; 7 | 8 | constructor(name: string, fields: Field[] = [], depth: number = 0) { 9 | this.name = name; 10 | this.fields = fields; 11 | this.depth = depth; 12 | } 13 | 14 | public fieldsToString(): string { 15 | return this.fields.map((f) => "\t" + f.toString()).join("\n"); 16 | } 17 | 18 | public formatModelName(modelName: string): string { 19 | return modelName 20 | .replace(/\w\S*/g, function (txt) { 21 | return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); 22 | }) 23 | .replace(/[^a-z]/gi, ""); 24 | } 25 | 26 | public getSerializerName(): string { 27 | const modelName = this.formatModelName(this.name); 28 | return `${modelName}Serializer`; 29 | } 30 | 31 | public getListApiName(): string { 32 | const modelName = this.formatModelName(this.name); 33 | return `${modelName}List`; 34 | } 35 | 36 | public getDetailApiName(): string { 37 | const modelName = this.formatModelName(this.name); 38 | return `${modelName}Detail`; 39 | } 40 | 41 | public getPath(): string { 42 | return this.name 43 | .toString() 44 | .toLowerCase() 45 | .replace(/\s+/g, "-") // Replace spaces with - 46 | .replace(/[^\w\-]+/g, "") // Remove all non-word chars 47 | .replace(/\-\-+/g, "-") // Replace multiple - with single - 48 | .replace(/^-+/, "") // Trim - from start of text 49 | .replace(/-+$/, ""); // Trim - from end of text 50 | } 51 | 52 | public toString(): string { 53 | const modelName = this.formatModelName(this.name); 54 | const fields = this.fieldsToString(); 55 | return `\nclass ${modelName}(models.Model):\n${fields}\n`; 56 | } 57 | 58 | public toSerializerString(): string { 59 | const modelName = this.formatModelName(this.name); 60 | 61 | return `class ${this.getSerializerName()}(serializers.ModelSerializer): 62 | class Meta: 63 | model = models.${modelName} 64 | fields = '__all__' 65 | depth = ${this.depth}`; 66 | } 67 | 68 | public toListApiString(): string { 69 | const modelName = this.formatModelName(this.name); 70 | 71 | return `class ${this.getListApiName()}(generics.ListCreateAPIView): 72 | queryset = models.${modelName}.objects.all() 73 | serializer_class = serializers.${this.getSerializerName()} 74 | filter_backends = [filters.SearchFilter, filters.OrderingFilter] 75 | search_fields = [${this.fields 76 | .map((field) => `'${field.formatFieldName()}'`) 77 | .join(",")}]`; 78 | } 79 | 80 | public toDetailApiString(): string { 81 | const modelName = this.formatModelName(this.name); 82 | 83 | return `class ${this.getDetailApiName()}(generics.RetrieveUpdateDestroyAPIView): 84 | queryset = models.${modelName}.objects.all() 85 | serializer_class = serializers.${this.getSerializerName()} 86 | lookup_url_kwarg = 'id'`; 87 | } 88 | 89 | public getListApiPath(): string { 90 | return `path('${this.getPath()}/', api.${this.getListApiName()}.as_view()),`; 91 | } 92 | 93 | public getDetailApiPath(): string { 94 | return `path('${this.getPath()}//', api.${this.getDetailApiName()}.as_view()),`; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /server/src/models/project.model.ts: -------------------------------------------------------------------------------- 1 | import { App } from "./app.model"; 2 | import { CommonFieldOptions } from "./field.model"; 3 | import { slugify } from "../helper"; 4 | 5 | const projectsRoot = "./projects"; 6 | 7 | class Project { 8 | name: string; 9 | app: App; 10 | port: number; 11 | dbUser: string; 12 | dbPassword: string; 13 | timestamp: number; 14 | 15 | constructor( 16 | name: string, 17 | port: number = 7080, 18 | dbUser: string = "postgres", 19 | dbPassword: string = "postgres" 20 | ) { 21 | this.name = slugify(name); 22 | this.port = port; 23 | this.dbUser = dbUser; 24 | this.dbPassword = dbPassword; 25 | this.app = new App("dummy"); 26 | this.timestamp = new Date().getTime(); 27 | } 28 | 29 | 30 | public get projectName() { 31 | return `${this.name}_${this.timestamp}`; 32 | } 33 | 34 | public get projectDir() { 35 | return `${projectsRoot}/${this.projectName}`; 36 | } 37 | } 38 | 39 | interface ProjectJson { 40 | name: string; 41 | port: number; 42 | app: { 43 | name: string; 44 | models: { 45 | name: string; 46 | fields: { 47 | commonOptions: CommonFieldOptions; 48 | hasDefault?: boolean; 49 | defaultValue?: any; 50 | maxLength?: number; 51 | model?: string; 52 | onDelete?: string; 53 | autoNow?: boolean; 54 | autoNowAdd?: boolean; 55 | maxDigits?: number; 56 | decimalPlaces?: number; 57 | uploadTo?: string; 58 | }[]; 59 | }[]; 60 | }; 61 | } 62 | 63 | export { Project, ProjectJson, projectsRoot }; 64 | -------------------------------------------------------------------------------- /server/src/routes/project.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import express, { Request, Response } from "express"; 3 | 4 | import { Project } from "../models/project.model"; 5 | import { parseJsonProject } from "../helper"; 6 | import { BashHelper } from "../bash-helper.model"; 7 | import { 8 | djangoRequirements, 9 | dockerComposeContent, 10 | dockerfileContent, 11 | djangoAdmin, 12 | djangoSerializers, 13 | djangoApi, 14 | djangoAppUrls, 15 | djangoProjectUrls, 16 | djangoSettings, 17 | djangoUtils, 18 | } from "../templates"; 19 | 20 | const router = express.Router(); 21 | 22 | router.post("/api/project", async (req: Request, res: Response) => { 23 | let project: Project = new Project(""); // avoid TS 'non-defined' 24 | try { 25 | project = parseJsonProject(req.body); 26 | } catch (err) { 27 | res.status(500).send({ message: "Error while parsing JSON." }); 28 | } 29 | 30 | // Project create 31 | await BashHelper.createDirectory(project.projectDir, true); 32 | await BashHelper.createDjangoProject(project.projectDir, project.name); 33 | 34 | // requirements.txt 35 | await BashHelper.createFile(project.projectDir, "requirements.txt"); 36 | await BashHelper.writeToFile( 37 | project.projectDir + "/requirements.txt", 38 | djangoRequirements 39 | ); 40 | 41 | // Dockerfile 42 | await BashHelper.createFile(project.projectDir, "Dockerfile"); 43 | await BashHelper.writeToFile( 44 | project.projectDir + "/Dockerfile", 45 | dockerfileContent(project.port) 46 | ); 47 | 48 | // docker-compose.yml 49 | await BashHelper.createFile(project.projectDir, "docker-compose.yml"); 50 | await BashHelper.writeToFile( 51 | project.projectDir + "/docker-compose.yml", 52 | dockerComposeContent(project.port) 53 | ); 54 | 55 | // settings.py 56 | const settingsFileName = "settings.py"; 57 | await BashHelper.createFile( 58 | project.projectDir + "/" + project.name, 59 | settingsFileName 60 | ); 61 | await BashHelper.writeToFile( 62 | project.projectDir + "/" + project.name + "/" + settingsFileName, 63 | djangoSettings(project.name, project.app.name) 64 | ); 65 | 66 | // urls.py 67 | const projectUrlsFileName = "urls.py"; 68 | await BashHelper.createFile( 69 | project.projectDir + "/" + project.name, 70 | projectUrlsFileName 71 | ); 72 | await BashHelper.writeToFile( 73 | project.projectDir + "/" + project.name + "/" + projectUrlsFileName, 74 | djangoProjectUrls(project.app.name) 75 | ); 76 | 77 | /* App */ 78 | const djangoApp = project.app; 79 | const djangoAppDir = project.projectDir + "/" + djangoApp.name; 80 | await BashHelper.createDirectory(djangoAppDir); 81 | await BashHelper.createDjangoApp(djangoAppDir, djangoApp.name); 82 | 83 | // admin.py 84 | await BashHelper.createFile(djangoAppDir, "admin.py"); 85 | await BashHelper.writeToFile( 86 | djangoAppDir + "/admin.py", 87 | djangoAdmin(djangoApp.models) 88 | ); 89 | 90 | // serializers.py 91 | const serializersFileName = "serializers.py"; 92 | await BashHelper.createFile(djangoAppDir, serializersFileName); 93 | await BashHelper.writeToFile( 94 | djangoAppDir + "/" + serializersFileName, 95 | djangoSerializers(djangoApp.models) 96 | ); 97 | 98 | // api.py 99 | const apiFileName = "api.py"; 100 | await BashHelper.createFile(djangoAppDir, apiFileName); 101 | await BashHelper.writeToFile( 102 | djangoAppDir + "/" + apiFileName, 103 | djangoApi(djangoApp.models) 104 | ); 105 | 106 | // urls.py 107 | const urlsFileName = "urls.py"; 108 | await BashHelper.createFile(djangoAppDir, urlsFileName); 109 | await BashHelper.writeToFile( 110 | djangoAppDir + "/" + urlsFileName, 111 | djangoAppUrls(djangoApp.models) 112 | ); 113 | 114 | // JWT 115 | const utilsFileName = "utils.py"; 116 | await BashHelper.createFile(djangoAppDir, utilsFileName); 117 | await BashHelper.writeToFile(djangoAppDir + "/" + utilsFileName, djangoUtils); 118 | 119 | /* Models */ 120 | const modelsFilePath = djangoAppDir + "/models.py"; 121 | djangoApp.models.forEach(async (model) => { 122 | await BashHelper.writeToFile(modelsFilePath, model.toString(), true); 123 | }); 124 | 125 | // format PEP8 126 | await BashHelper.formatProjectWithAutopep8(project.projectDir); 127 | 128 | // compress 129 | await BashHelper.compressDirectory(project.projectDir); 130 | 131 | try { 132 | res.status(201).sendFile(`${project.projectName}.zip`, { 133 | root: path.join(__dirname, "../../projects"), 134 | dotfiles: "deny", 135 | headers: { 136 | "x-timestamp": Date.now(), 137 | "x-sent": true, 138 | }, 139 | }); 140 | } catch (err) { 141 | res.status(500).send({ message: "Error while sending the zip file." }); 142 | } 143 | }); 144 | 145 | router.get("/api/project", async (req: Request, res: Response) => { 146 | res.send({ message: "GET on this route is not allowed." }); 147 | }); 148 | 149 | export { router as projectRouter }; 150 | -------------------------------------------------------------------------------- /server/src/routes/status.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | 3 | const router = express.Router(); 4 | 5 | router.get("/api/status", async (req: Request, res: Response) => { 6 | res.status(200).send({ ok: true }); 7 | }); 8 | 9 | export { router as statusRouter }; 10 | -------------------------------------------------------------------------------- /server/src/templates/django-admin.ts: -------------------------------------------------------------------------------- 1 | import { Model } from '../models/model.model'; 2 | 3 | const djangoAdmin = function (models: Model[]) { 4 | return ` 5 | from django.contrib import admin 6 | from . import models 7 | 8 | ${models 9 | .map((model) => model.formatModelName(model.name)) 10 | .map((name) => `admin.site.register(models.${name})`) 11 | .join("\n")} 12 | `; 13 | }; 14 | 15 | export { djangoAdmin }; 16 | -------------------------------------------------------------------------------- /server/src/templates/django-api.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "../models/model.model"; 2 | 3 | const djangoApi = function (models: Model[]) { 4 | return ` 5 | from django.http import JsonResponse 6 | from django.db import transaction 7 | 8 | from rest_framework import generics, filters 9 | from rest_framework.response import Response 10 | from django_filters.rest_framework import DjangoFilterBackend 11 | 12 | from . import models 13 | from . import serializers 14 | 15 | ${models.map((model) => model.toListApiString()).join("\n")} 16 | 17 | ${models.map((model) => model.toDetailApiString()).join("\n")} 18 | `; 19 | }; 20 | 21 | export { djangoApi }; 22 | -------------------------------------------------------------------------------- /server/src/templates/django-app-urls.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "../models/model.model"; 2 | 3 | const djangoAppUrls = function (models: Model[]) { 4 | return ` 5 | from django.urls import path 6 | from rest_framework_jwt.views import obtain_jwt_token, refresh_jwt_token 7 | 8 | from . import views 9 | from . import api 10 | 11 | urlpatterns = [ 12 | # Auth 13 | path('token-auth', obtain_jwt_token), 14 | path('token-refresh', refresh_jwt_token), 15 | 16 | # API 17 | ${models.map((model) => model.getListApiPath()).join("\n")} 18 | ${models.map((model) => model.getDetailApiPath()).join("\n")} 19 | ] 20 | `; 21 | }; 22 | 23 | export { djangoAppUrls }; 24 | -------------------------------------------------------------------------------- /server/src/templates/django-project-urls.ts: -------------------------------------------------------------------------------- 1 | const djangoProjectUrls = function(appName: string) { 2 | return ` 3 | from django.conf.urls import url, include 4 | from django.contrib import admin 5 | from rest_framework_swagger.views import get_swagger_view 6 | 7 | schema_view = get_swagger_view(title='Swagger') 8 | 9 | urlpatterns = [ 10 | url(r'^admin/', admin.site.urls), 11 | url(r'^api/', include('${appName}.urls')), 12 | url(r'^swagger/', schema_view), 13 | ] 14 | `; 15 | } 16 | 17 | export { djangoProjectUrls }; -------------------------------------------------------------------------------- /server/src/templates/django-requirements.ts: -------------------------------------------------------------------------------- 1 | const djangoRequirements = ` 2 | Django==2.2.12 3 | djangorestframework==3.9.0 4 | django-cors-headers==2.4.0 5 | djangorestframework-jwt 6 | psycopg2 7 | xlrd 8 | Pillow 9 | django-filter 10 | django_rest_swagger==2.2.0 11 | `; 12 | 13 | export { djangoRequirements }; 14 | -------------------------------------------------------------------------------- /server/src/templates/django-serializers.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "../models/model.model"; 2 | 3 | const djangoSerializers = function (models: Model[]) { 4 | return ` 5 | from rest_framework import serializers 6 | from django.contrib.auth.models import User 7 | from . import models 8 | 9 | ${models 10 | .map((model) => model.toSerializerString()) 11 | .join("\n")} 12 | `; 13 | }; 14 | 15 | export { djangoSerializers }; 16 | -------------------------------------------------------------------------------- /server/src/templates/django-settings.ts: -------------------------------------------------------------------------------- 1 | const djangoSettings = function ( 2 | projectName: string, 3 | appName: string, 4 | corsWhitelist: string[] = [], 5 | pageSize: number = 25, 6 | jwtExpiration: number = 3600 * 12 7 | ) { 8 | return ` 9 | import os 10 | import datetime 11 | 12 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 13 | 14 | SECRET_KEY = 'qh9s+kq4ef_y3mv9x_&6ik=2&s-=l=!8*m^r$+7sd^7awsfy^@' 15 | 16 | # SECURITY WARNING: don't run with debug turned on in production! 17 | DEBUG = True 18 | ALLOWED_HOSTS = ["*"] 19 | 20 | 21 | # Application definition 22 | 23 | INSTALLED_APPS = [ 24 | 'django.contrib.admin', 25 | 'django.contrib.auth', 26 | 'django.contrib.contenttypes', 27 | 'django.contrib.sessions', 28 | 'django.contrib.messages', 29 | 'django.contrib.staticfiles', 30 | 'django_filters', 31 | 'rest_framework', 32 | 'rest_framework.authtoken', 33 | 'corsheaders', 34 | 'rest_framework_swagger', 35 | '${appName}' 36 | ] 37 | 38 | MIDDLEWARE = [ 39 | 'corsheaders.middleware.CorsMiddleware', 40 | 'django.middleware.security.SecurityMiddleware', 41 | 'django.contrib.sessions.middleware.SessionMiddleware', 42 | 'django.middleware.common.CommonMiddleware', 43 | 'django.middleware.csrf.CsrfViewMiddleware', 44 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 45 | 'django.contrib.messages.middleware.MessageMiddleware', 46 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 47 | ] 48 | 49 | ROOT_URLCONF = '${projectName}.urls' 50 | 51 | TEMPLATES = [ 52 | { 53 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 54 | 'DIRS': [], 55 | 'APP_DIRS': True, 56 | 'OPTIONS': { 57 | 'context_processors': [ 58 | 'django.template.context_processors.debug', 59 | 'django.template.context_processors.request', 60 | 'django.contrib.auth.context_processors.auth', 61 | 'django.contrib.messages.context_processors.messages', 62 | ], 63 | }, 64 | }, 65 | ] 66 | 67 | WSGI_APPLICATION = '${projectName}.wsgi.application' 68 | 69 | 70 | # Database 71 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 72 | 73 | DATABASES = { 74 | 'default': { 75 | 'ENGINE': 'django.db.backends.postgresql', 76 | 'NAME': 'postgres', 77 | 'USER': os.environ['POSTGRES_USER'], 78 | 'HOST': 'db', 79 | 'PORT': 5432, 80 | 'PASSWORD': os.environ['POSTGRES_PASSWORD'] 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | # Internationalization 104 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 105 | 106 | LANGUAGE_CODE = 'en-US' 107 | 108 | TIME_ZONE = 'Europe/Istanbul' 109 | 110 | USE_I18N = True 111 | 112 | USE_L10N = True 113 | 114 | USE_TZ = True 115 | 116 | 117 | # Static files (CSS, JavaScript, Images) 118 | # https://docs.djangoproject.com/en/2.1/howto/static-files/ 119 | 120 | STATIC_URL = '/static/' 121 | 122 | STATIC_URL = '/static/' 123 | STATIC_ROOT = '/var/www/yonetim.shop-wash.co/staticfiles/' 124 | 125 | MEDIA_URL = '/media/' 126 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 127 | 128 | # Cors Rules 129 | 130 | CORS_ORIGIN_WHITELIST = ( 131 | ${corsWhitelist.map((adr) => `'${adr}'`).join(",")} 132 | ) 133 | 134 | # REST settings 135 | 136 | REST_FRAMEWORK = { 137 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 138 | 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', 139 | 'rest_framework.authentication.SessionAuthentication', 140 | 'rest_framework.authentication.BasicAuthentication', 141 | ), 142 | 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], 143 | 'PAGE_SIZE': ${pageSize}, 144 | 'TEST_REQUEST_DEFAULT_FORMAT': 'json' 145 | } 146 | 147 | # API Auth 148 | JWT_AUTH = { 149 | 'JWT_ALLOW_REFRESH': True, 150 | 'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=${jwtExpiration}), 151 | 'JWT_PAYLOAD_HANDLER': 152 | '${appName}.utils.jwt_payload_handler', 153 | } 154 | `; 155 | }; 156 | 157 | export { djangoSettings }; 158 | -------------------------------------------------------------------------------- /server/src/templates/django-utils.ts: -------------------------------------------------------------------------------- 1 | const djangoUtils = ` 2 | import warnings 3 | from calendar import timegm 4 | from datetime import datetime 5 | 6 | from rest_framework_jwt.compat import get_username, get_username_field 7 | from rest_framework_jwt.settings import api_settings 8 | 9 | 10 | def jwt_payload_handler(user): 11 | username_field = get_username_field() 12 | username = get_username(user) 13 | 14 | warnings.warn( 15 | 'The following fields will be removed in the future: ' 16 | '\`email\` and \`user_id\`. ', 17 | DeprecationWarning 18 | ) 19 | 20 | payload = {'id': user.pk, 'email': user.email, 'name': user.name, 'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA, 'admin': user.is_staff, 'active': user.is_active} 21 | 22 | # Include original issued at time for a brand new token, 23 | # to allow token refresh 24 | if api_settings.JWT_ALLOW_REFRESH: 25 | payload['orig_iat'] = timegm( 26 | datetime.utcnow().utctimetuple() 27 | ) 28 | 29 | if api_settings.JWT_AUDIENCE is not None: 30 | payload['aud'] = api_settings.JWT_AUDIENCE 31 | 32 | if api_settings.JWT_ISSUER is not None: 33 | payload['iss'] = api_settings.JWT_ISSUER 34 | 35 | return payload 36 | 37 | `; 38 | 39 | export { djangoUtils } 40 | -------------------------------------------------------------------------------- /server/src/templates/docker-compose.ts: -------------------------------------------------------------------------------- 1 | const dockerComposeContent = function (port: number = 8080, dbUser: string = "postgres", dbPassword: string = "postgres") { 2 | return ` 3 | version: "3" 4 | 5 | services: 6 | db: 7 | image: postgres 8 | environment: 9 | POSTGRES_USER: ${dbUser} 10 | POSTGRES_PASSWORD: ${dbPassword} 11 | dj: 12 | build: . 13 | environment: 14 | POSTGRES_USER: ${dbUser} 15 | POSTGRES_PASSWORD: ${dbPassword} 16 | command: bash -c "python manage.py makemigrations && python manage.py migrate && python manage.py runserver 0.0.0.0:${port}" 17 | volumes: 18 | - ./:/code 19 | ports: 20 | - "${port}:${port}" 21 | depends_on: 22 | - db 23 | `; 24 | }; 25 | 26 | export { dockerComposeContent }; 27 | -------------------------------------------------------------------------------- /server/src/templates/docker.ts: -------------------------------------------------------------------------------- 1 | const dockerfileContent = function (port: number = 8080) { 2 | return ` 3 | FROM python:3 4 | ENV PYTHONUNBUFFERED 1 5 | COPY requirements.txt / 6 | RUN pip install -r /requirements.txt 7 | RUN mkdir /code 8 | WORKDIR /code 9 | COPY . /code/ 10 | EXPOSE ${port} 11 | `; 12 | }; 13 | 14 | export { dockerfileContent }; 15 | -------------------------------------------------------------------------------- /server/src/templates/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./django-admin"; 2 | export * from "./django-api"; 3 | export * from "./django-app-urls"; 4 | export * from "./django-project-urls"; 5 | export * from "./django-requirements"; 6 | export * from "./django-serializers"; 7 | export * from "./django-settings"; 8 | export * from "./django-utils"; 9 | export * from "./docker"; 10 | export * from "./docker-compose"; 11 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./dist", /* Concatenate and emit output to single file. */ 15 | "outDir": "build", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | "baseUrl": ".", /* Base directory to resolve non-absolute module names. */ 44 | "paths": { 45 | "*": [ 46 | "node_modules/*" 47 | ] 48 | }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | 67 | /* Advanced Options */ 68 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 69 | }, 70 | "include": [ 71 | "src/**/*" 72 | ], 73 | } 74 | --------------------------------------------------------------------------------