├── .coveragerc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST ├── README.md ├── assets ├── Common │ ├── djangologo.png │ ├── index.html-tpl │ ├── urls.py-tpl │ └── views.py-tpl ├── reactjs │ ├── App.css-tpl │ ├── App.js-tpl │ ├── babel.config.json-tpl │ ├── index.js-tpl │ ├── package.json-tpl │ ├── reactlogo.png │ └── webpack.config.js-tpl └── reactts │ ├── App.css-tpl │ ├── App.tsx-tpl │ ├── babel.config.json-tpl │ ├── index.tsx-tpl │ ├── module.d.ts-tpl │ ├── package.json-tpl │ ├── reactlogo.png │ ├── tsconfig.json-tpl │ └── webpack.config.js-tpl ├── django_webpack_dev_server ├── __init__.py ├── apps.py ├── management │ ├── __init__.py │ ├── commands │ │ ├── __init__.py │ │ └── generate.py │ ├── constants.py │ ├── generator.py │ └── template_constants │ │ ├── __init__.py │ │ ├── development │ │ ├── __init__.py │ │ ├── development_constants.py │ │ ├── reactjs_constants.py │ │ └── reactts_constants.py │ │ └── production │ │ ├── __init__.py │ │ ├── production_constants.py │ │ ├── reactjs_constants.py │ │ └── reactts_constants.py └── migrations │ └── __init__.py ├── pytest.ini ├── requirements.txt ├── requirements ├── requirements-dev.txt ├── requirements-lint.txt └── requirements-testing.txt ├── runtests.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── management │ ├── __init__.py │ ├── commands │ │ ├── __init__.py │ │ └── test_generate.py │ └── test_generator.py └── test_settings.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = django_webpack_dev_server -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test: 10 | runs-on: ${{ matrix.os }} 11 | 12 | strategy: 13 | matrix: 14 | python: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14.0-alpha.2'] 15 | os: [windows-latest, ubuntu-20.04, macos-latest] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Setup Python 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python }} 24 | 25 | - name: Install the Python Dependencies 26 | run: pip install tox 27 | 28 | - name: Run Tox for ${{ matrix.python }} 29 | run: tox -e py 30 | 31 | - name: Upload coverage to Codecov 32 | uses: codecov/codecov-action@v2 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore byte compiled files created by Python 2 | __pycache__/ 3 | 4 | # ignore vscode folder 5 | .vscode/ 6 | 7 | # ignore the virtual environment 8 | env/ 9 | 10 | # ignore the build folder produced after running setup.py 11 | build/ 12 | 13 | # ignore the dist folder produced after running setup.py 14 | dist/ 15 | 16 | # ignore the django mysite project used to test this package 17 | mysite/ 18 | 19 | # ignore the frontend django app 20 | frontend/ 21 | 22 | django_webpack_dev_server.egg-info/ 23 | 24 | # ignore the html files of the coverage 25 | htmlcov/ 26 | 27 | # ignore the coverage.xml file 28 | coverage.xml 29 | 30 | # ignore the todo text file 31 | Todo.txt 32 | 33 | # ignore the sqlite file 34 | db.sqlite3 35 | 36 | # ignore the .pytest_cache folder 37 | .pytest_cache/ 38 | 39 | # ignore the .tox folder 40 | .tox/ 41 | 42 | # ignore the environment variables file 43 | .env 44 | 45 | # ignore the .coverage file 46 | .coverage 47 | 48 | # ignore the manage.py file 49 | manage.py -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-docstring-first 6 | - id: trailing-whitespace 7 | - id: check-yaml 8 | - repo: https://github.com/psf/black 9 | rev: 25.1.0 10 | hooks: 11 | - id: black 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jiten Sidhpura 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | README.rst 3 | setup.cfg 4 | setup.py 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-webpack-dev-server 2 | 3 | [![PyPI version](https://badge.fury.io/py/django-webpack-dev-server.svg)](https://badge.fury.io/py/django-webpack-dev-server) 4 | [![codecov](https://codecov.io/gh/Jitensid/django-webpack-dev-server/branch/main/graph/badge.svg?token=D952NCAC8I)](https://codecov.io/gh/Jitensid/django-webpack-dev-server) 5 | [![Requirements Status](https://requires.io/github/Jitensid/django-webpack-dev-server/requirements.svg?branch=main)](https://requires.io/github/Jitensid/django-webpack-dev-server/requirements/?branch=main) 6 | [![CI](https://github.com/Jitensid/django-webpack-dev-server/actions/workflows/main.yml/badge.svg)](https://github.com/Jitensid/django-webpack-dev-server/actions/workflows/main.yml) 7 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/Jitensid/django-webpack-dev-server/main.svg)](https://results.pre-commit.ci/latest/github/Jitensid/django-webpack-dev-server/main) 8 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 9 | [![License MIT](https://img.shields.io/github/license/Jitensid/django-webpack-dev-server?color=purple)](https://github.com/Jitensid/django-webpack-dev-server/blob/main/LICENSE) 10 | 11 | Django Webpack Dev Server is a command line Django reusable app to setup configuration files for React. It uses webpack and webpack_dev_server to bundle your frontend code. 12 | 13 | [![Watch the Demo](https://user-images.githubusercontent.com/46622106/139619283-39d53e00-25d6-4f3b-aee9-e47403564ca3.gif)](https://www.youtube.com/watch?v=6c-lPkKzI_E) 14 | 15 | 16 | ## Installation 17 | 18 | Install using pip 19 | 20 | `pip install django-webpack-dev-server` 21 | 22 | ## Package Supports 23 | 24 | 1. React (Javascript) 25 | 2. React (Typescript) 26 | 27 | ## Quick start 28 | 29 | 1. Add 'django_webpack_dev_server' to your INSTALLED_APPS in 30 | settings.py like this: 31 | 32 | ```python 33 | INSTALLED_APPS = [ 34 | ... 35 | 'django_webpack_dev_server', 36 | ] 37 | ``` 38 | 39 | 2. Default django app name is frontend and template is javascript. You can provide your name and template by running
`python manage.py generate react --app_name your_app_name --template (javascript/typescript)` 40 | 41 | 3. Run `python manage.py generate react` to create a django app with the default app_name and template. 42 | 43 | 4. Add the new django app to your INSTALLED_APPS setting like in step 1. 44 | 45 | 5. Add the path for the new django app in the urlpatterns of the project's urls.py like this: 46 | 47 | ```python 48 | from django.urls import path, include 49 | 50 | path("", include("your_app_name.urls")), 51 | ``` 52 | 53 | 6. Run `python manage.py runserver` to start the django's development 54 | server. 55 | 56 | 7. `cd` into the newly created django app and run `npm start` and go to 57 | (). 58 | 59 | 8. Run `npm run build` to create a production build of your frontend code. 60 | 61 | ## Important Links 62 | 63 | 1. [Demo Video of the Package](https://www.youtube.com/watch?v=6c-lPkKzI_E) 64 | 65 | 2. [Check out Tech with Tim's Youtube Playlist for a detailed explanation in building Django and React Full Stack App.](https://www.youtube.com/watch?v=JD-age0BPVo&list=PLzMcBGfZo4-kCLWnGmK0jUBmGLaJxvi4j) 66 | 67 | 3. [Webpack Documentation](https://webpack.js.org/concepts/) 68 | 69 | ## Contributions 70 | 71 | If you find an issue or have a new feature then feel free to make a Pull Request. Your Contributions are always welcomed. 72 | 73 | ### Running the Package in Development Mode 74 | 75 | 1. You can run the package in development mode by setting the "SOFTWARE_ENVIRONMENT_MODE" environment variable equal to "development". 76 | 77 | 2. Now you can load assets from your local system and the new changes can be tested. 78 | 79 | ## License 80 | 81 | This project is provided under the [MIT License](https://github.com/Jitensid/django-webpack-dev-server/blob/main/LICENSE). 82 | -------------------------------------------------------------------------------- /assets/Common/djangologo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/c47e5208dc87ed4a9550674c7374764e97a9fb86/assets/Common/djangologo.png -------------------------------------------------------------------------------- /assets/Common/index.html-tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Django and React Web Application 8 | {% load static %} 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/Common/urls.py-tpl: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | import $app_name.views as views 3 | 4 | # All urls will point to same HTML template because it is SPA application 5 | urlpatterns = [ 6 | path('', views.index, name='index'), 7 | ] 8 | -------------------------------------------------------------------------------- /assets/Common/views.py-tpl: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | def index(request): 5 | return render(request, "$app_name/index.html") 6 | -------------------------------------------------------------------------------- /assets/reactjs/App.css-tpl: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | background-color: black; 4 | min-height: 100vh; 5 | } 6 | 7 | .App-logo { 8 | height: 40vmin; 9 | animation: App-logo-spin infinite 10s linear; 10 | } 11 | 12 | .django-logo { 13 | margin-top: 20vh; 14 | height: 40vmin; 15 | } 16 | 17 | @keyframes App-logo-spin { 18 | from { 19 | transform: rotate(0deg); 20 | } 21 | to { 22 | transform: rotate(360deg); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /assets/reactjs/App.js-tpl: -------------------------------------------------------------------------------- 1 | import reactlogo from './reactlogo.png'; 2 | import djangologo from './djangologo.png'; 3 | import './App.css'; 4 | 5 | function App() { 6 | return ( 7 |
8 |
9 | django 10 | react 11 |
12 |
13 | ); 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /assets/reactjs/babel.config.json-tpl: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env"], 4 | ["@babel/preset-react",{ 5 | "runtime": "automatic" 6 | }] 7 | ], 8 | "plugins": ["@babel/plugin-proposal-class-properties"] 9 | } 10 | -------------------------------------------------------------------------------- /assets/reactjs/index.js-tpl: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | const appDiv = document.getElementById('app'); 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | appDiv 12 | ); 13 | 14 | if (module.hot) { 15 | module.hot.accept(); 16 | } 17 | -------------------------------------------------------------------------------- /assets/reactjs/package.json-tpl: -------------------------------------------------------------------------------- 1 | { 2 | "name": "$app_name", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --mode development", 8 | "build": "webpack --mode production" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@babel/core": "^7.16.0", 14 | "@babel/preset-env": "^7.16.4", 15 | "@babel/preset-react": "^7.16.0", 16 | "@babel/plugin-proposal-class-properties": "^7.16.0", 17 | "babel-loader": "^8.2.3", 18 | "css-loader": "^6.5.1", 19 | "file-loader": "^6.2.0", 20 | "react": "^17.0.2", 21 | "react-dom": "^17.0.2", 22 | "sass": "^1.49.0", 23 | "sass-loader": "^12.3.0", 24 | "style-loader": "^3.3.1", 25 | "webpack": "^5.64.4", 26 | "webpack-cli": "^4.9.1", 27 | "webpack-dev-server": "^4.6.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /assets/reactjs/reactlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/c47e5208dc87ed4a9550674c7374764e97a9fb86/assets/reactjs/reactlogo.png -------------------------------------------------------------------------------- /assets/reactjs/webpack.config.js-tpl: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: ['./src'], 6 | output: { 7 | path: path.resolve(__dirname, './static/$app_name'), 8 | filename: '[name].js', 9 | publicPath: '/static/$app_name/', 10 | }, 11 | resolve: { 12 | alias: { 13 | 'react-dom': 'react-dom/profiling', 14 | 'scheduler/tracing': 'scheduler/tracing-profiling', 15 | }, 16 | extensions: ['.jsx', '.js'], 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.(js|jsx)$$/, 22 | exclude: /node_modules/, 23 | use: [ 24 | { 25 | loader: 'babel-loader', 26 | }, 27 | ], 28 | }, 29 | { 30 | test: /\.css$$/, 31 | exclude: /node_modules/, 32 | use: [ 33 | { 34 | loader: 'style-loader', 35 | }, 36 | { 37 | loader: 'css-loader', 38 | }, 39 | ], 40 | }, 41 | { 42 | test: /\.(scss|sass)$$/, 43 | exclude: /node_modules/, 44 | use: [ 45 | { 46 | loader: 'style-loader', 47 | }, 48 | { 49 | loader: 'css-loader', 50 | }, 51 | { 52 | loader: 'sass-loader', 53 | }, 54 | ], 55 | }, 56 | { 57 | test: /\.(jpe?g|png|gif|svg|mp4|mp3)$$/, 58 | use: [ 59 | { 60 | loader: 'file-loader', 61 | options: { 62 | outputPath: '', 63 | }, 64 | }, 65 | ], 66 | }, 67 | ], 68 | }, 69 | optimization: { 70 | minimize: true, 71 | }, 72 | devServer: { 73 | proxy: { 74 | '!/static/$app_name/**': { 75 | target: 'http://localhost:8000', // points to django dev server 76 | changeOrigin: true, 77 | }, 78 | }, 79 | open: true, 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /assets/reactts/App.css-tpl: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | background-color: black; 4 | min-height: 100vh; 5 | } 6 | 7 | .App-logo { 8 | height: 40vmin; 9 | animation: App-logo-spin infinite 10s linear; 10 | } 11 | 12 | .django-logo { 13 | margin-top: 20vh; 14 | height: 40vmin; 15 | } 16 | 17 | @keyframes App-logo-spin { 18 | from { 19 | transform: rotate(0deg); 20 | } 21 | to { 22 | transform: rotate(360deg); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /assets/reactts/App.tsx-tpl: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import djangologo from './djangologo.png'; 3 | import reactlogo from './reactlogo.png'; 4 | 5 | function App() { 6 | return ( 7 |
8 |
9 | django 10 | react 11 |
12 |
13 | ); 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /assets/reactts/babel.config.json-tpl: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | ["@babel/preset-react",{ 5 | "runtime": "automatic" 6 | }], 7 | "@babel/preset-typescript" 8 | ], 9 | "plugins": ["@babel/plugin-proposal-class-properties"] 10 | } 11 | -------------------------------------------------------------------------------- /assets/reactts/index.tsx-tpl: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | const appDiv = document.getElementById('app'); 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | appDiv 12 | ); 13 | 14 | if (module.hot) { 15 | module.hot.accept(); 16 | } -------------------------------------------------------------------------------- /assets/reactts/module.d.ts-tpl: -------------------------------------------------------------------------------- 1 | declare module '*.jpg' { 2 | const value: string; 3 | export default value; 4 | } 5 | 6 | declare module '*.jpeg' { 7 | const value: string; 8 | export default value; 9 | } 10 | 11 | declare module '*.png' { 12 | const value: string; 13 | export default value; 14 | } 15 | 16 | declare module '*.gif' { 17 | const value: string; 18 | export default value; 19 | } 20 | 21 | declare module '*.svg' { 22 | const value: string; 23 | export default value; 24 | } 25 | 26 | declare module '*.mp4' { 27 | const value: string; 28 | export default value; 29 | } 30 | 31 | declare module '*.mp3' { 32 | const value: string; 33 | export default value; 34 | } -------------------------------------------------------------------------------- /assets/reactts/package.json-tpl: -------------------------------------------------------------------------------- 1 | { 2 | "name": "$app_name", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --mode development", 8 | "build": "webpack --mode production" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@babel/core": "^7.16.0", 14 | "@babel/plugin-proposal-class-properties": "^7.16.0", 15 | "@babel/preset-env": "^7.16.4", 16 | "@babel/preset-react": "^7.16.0", 17 | "@babel/preset-typescript": "^7.16.0", 18 | "@types/node": "^16.11.21", 19 | "@types/react": "^17.0.38", 20 | "@types/react-dom": "^17.0.11", 21 | "@types/webpack-env": "^1.16.3", 22 | "babel-loader": "^8.2.3", 23 | "css-loader": "^6.5.1", 24 | "file-loader": "^6.2.0", 25 | "fork-ts-checker-webpack-plugin": "^6.5.0", 26 | "react": "^17.0.2", 27 | "react-dom": "^17.0.2", 28 | "sass": "^1.49.0", 29 | "sass-loader": "^12.3.0", 30 | "style-loader": "^3.3.1", 31 | "ts-loader": "^9.2.6", 32 | "typescript": "^4.5.5", 33 | "webpack": "^5.64.4", 34 | "webpack-cli": "^4.9.1", 35 | "webpack-dev-server": "^4.6.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /assets/reactts/reactlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/c47e5208dc87ed4a9550674c7374764e97a9fb86/assets/reactts/reactlogo.png -------------------------------------------------------------------------------- /assets/reactts/tsconfig.json-tpl: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": false , 21 | "jsx": "react-jsx", 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } -------------------------------------------------------------------------------- /assets/reactts/webpack.config.js-tpl: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: ['./src/index.tsx'], 7 | output: { 8 | path: path.resolve(__dirname, './static/$app_name'), 9 | filename: '[name].js', 10 | publicPath: '/static/$app_name/', 11 | }, 12 | resolve: { 13 | alias: { 14 | 'react-dom': 'react-dom/profiling', 15 | 'scheduler/tracing': 'scheduler/tracing-profiling', 16 | }, 17 | extensions: ['.tsx', '.ts', '.jsx', '.js'], 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.(js|jsx)$$/, 23 | exclude: /node_modules/, 24 | use: [ 25 | { 26 | loader: 'babel-loader', 27 | }, 28 | ], 29 | }, 30 | { 31 | test: /\.(ts|tsx)$$/, 32 | exclude: /node_modules/, 33 | use: [ 34 | { 35 | loader: 'ts-loader', 36 | options: { 37 | transpileOnly: true, 38 | }, 39 | }, 40 | ], 41 | }, 42 | { 43 | test: /\.css$$/, 44 | exclude: /node_modules/, 45 | use: [ 46 | { 47 | loader: 'style-loader', 48 | }, 49 | { 50 | loader: 'css-loader', 51 | }, 52 | ], 53 | }, 54 | { 55 | test: /\.(scss|sass)$$/, 56 | exclude: /node_modules/, 57 | use: [ 58 | { 59 | loader: 'style-loader', 60 | }, 61 | { 62 | loader: 'css-loader', 63 | }, 64 | { 65 | loader: 'sass-loader', 66 | }, 67 | ], 68 | }, 69 | { 70 | test: /\.(jpe?g|png|gif|svg|mp4|mp3)$$/, 71 | use: [ 72 | { 73 | loader: 'file-loader', 74 | options: { 75 | outputPath: '', 76 | }, 77 | }, 78 | ], 79 | }, 80 | ], 81 | }, 82 | optimization: { 83 | minimize: true, 84 | }, 85 | devServer: { 86 | proxy: { 87 | '!/static/$app_name/**': { 88 | target: 'http://localhost:8000', // points to django dev server 89 | changeOrigin: true, 90 | }, 91 | }, 92 | open: true, 93 | }, 94 | plugins: [ 95 | new ForkTsCheckerWebpackPlugin(), 96 | ], 97 | }; 98 | -------------------------------------------------------------------------------- /django_webpack_dev_server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/c47e5208dc87ed4a9550674c7374764e97a9fb86/django_webpack_dev_server/__init__.py -------------------------------------------------------------------------------- /django_webpack_dev_server/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DjangoWebpackDevServerConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | 7 | name = "django_webpack_dev_server" 8 | -------------------------------------------------------------------------------- /django_webpack_dev_server/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/c47e5208dc87ed4a9550674c7374764e97a9fb86/django_webpack_dev_server/management/__init__.py -------------------------------------------------------------------------------- /django_webpack_dev_server/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/c47e5208dc87ed4a9550674c7374764e97a9fb86/django_webpack_dev_server/management/commands/__init__.py -------------------------------------------------------------------------------- /django_webpack_dev_server/management/commands/generate.py: -------------------------------------------------------------------------------- 1 | # Contains the code for the generate command 2 | 3 | from django.core.management.base import BaseCommand 4 | from django_webpack_dev_server.management.generator import Generator 5 | 6 | 7 | class Command(BaseCommand): 8 | help = "Creates a django app which has the frontend configuration" 9 | 10 | def add_arguments(self, parser): 11 | # adding subparser for react 12 | subparsers = parser.add_subparsers( 13 | help="Command to create a django app which has the frontend configuration", 14 | dest="frontend_library_or_framework", 15 | ) 16 | 17 | subparsers.required = True 18 | 19 | # add a parser for react because it can be javascript or typescript 20 | react_parser = subparsers.add_parser( 21 | "react", 22 | help="Command to have django app with React for Frontend Development", 23 | ) 24 | 25 | language_choices = ["javascript", "typescript"] 26 | # add the optional app_name argument with the default value as frontend 27 | react_parser.add_argument( 28 | "--app_name", help="Frontend Django App Name", default="frontend" 29 | ) 30 | 31 | # add the optional template argument with the default value as javascript 32 | # template argument can be either javascript or typescript 33 | react_parser.add_argument( 34 | "--template", 35 | help="Language for the React Project", 36 | default="javascript", 37 | choices=language_choices, 38 | ) 39 | 40 | def show_success_message(self, message): 41 | """ 42 | :param message: message to be shown in stdout with success style 43 | """ 44 | self.stdout.write(self.style.SUCCESS(message)) 45 | 46 | def handle(self, *args, **options): 47 | # get the django app_name and the frontend_library_or_framework 48 | # specified by the user 49 | app_name = options["app_name"] 50 | frontend_library_or_framework = options["frontend_library_or_framework"] 51 | 52 | # if the frontend_library_or_framework is react 53 | if frontend_library_or_framework == "react": 54 | # fetch the template argument 55 | template = options["template"] 56 | 57 | # append it to the frontend_library_or_framework 58 | # react and javascript -> react_javascript for React Javascript 59 | # react and typescript -> react_typescript for React Typescript 60 | frontend_library_or_framework += "_" + template 61 | 62 | # Create a Generator object and pass app_name and frontend_library_or_framework 63 | app_generator = Generator( 64 | app_name=app_name, 65 | frontend_library_or_framework=frontend_library_or_framework, 66 | ) 67 | # start building the new django app 68 | app_generator.generate() 69 | 70 | # show success messages to the user after the command is successfully 71 | # executed 72 | self.show_success_message("\n\nYour App is Ready !!!") 73 | self.show_success_message( 74 | f"Add {app_name} in the INSTALLED_APPS list in the project's settings.py file" 75 | ) 76 | self.show_success_message( 77 | f"Add {app_name}.urls in urlpatterns of the project's urls.py" 78 | ) 79 | self.show_success_message( 80 | "Start the Django Server and cd into your app and run npm start" 81 | ) 82 | -------------------------------------------------------------------------------- /django_webpack_dev_server/management/constants.py: -------------------------------------------------------------------------------- 1 | from django_webpack_dev_server.management.template_constants.development import ( 2 | development_constants, 3 | ) 4 | from django_webpack_dev_server.management.template_constants.production import ( 5 | production_constants, 6 | ) 7 | 8 | # To Identify if the operating system is windows based or not 9 | WINDOWS_OS_IDENTIFIER = "win32" 10 | 11 | APP_DIRECTORY_NAME = "APP" 12 | SRC_DIRECTORY_NAME = "src" 13 | TEMPLATES_DIRECTORY_NAME = "templates" 14 | 15 | COMMON_ASSETS_DIRECTORY = "Common" 16 | 17 | # Store the development template filenames with frontend library or framework as key 18 | DEVELOPMENT_TEMPLATE_FILES_DICT = { 19 | "react_javascript": development_constants.DEV_ALL_REACTJS_TEMPLATE_FILES, 20 | "react_typescript": development_constants.DEV_ALL_REACTTS_TEMPLATE_FILES, 21 | } 22 | 23 | 24 | # Store template filenames with frontend library or framework as key 25 | PROD_TEMPLATE_FILES_DICT = { 26 | "react_javascript": production_constants.PROD_ALL_REACTJS_TEMPLATE_FILES, 27 | "react_typescript": production_constants.PROD_ALL_REACTTS_TEMPLATE_FILES, 28 | } 29 | 30 | # show relevent error messages to the user 31 | COMMAND_ERROR_MESSAGES_DICT = { 32 | "INVALID_APP_NAME_ERROR_MESSAGE": "App Name should be alphanumeric only", 33 | "SYSTEM_ERROR_MESSAGE": "Seems like node or npm is not available in your system", 34 | "STATICFILES_DIRS_MISSING_ERROR_MESSAGE": "STATICFILES_DIRS attribute is missing in the django settings file", 35 | "NPM_INSTALLATION_ERROR_MESSAGE": "There were some errors while installing dependencies", 36 | } 37 | -------------------------------------------------------------------------------- /django_webpack_dev_server/management/generator.py: -------------------------------------------------------------------------------- 1 | # necessary imports 2 | import os 3 | import queue 4 | import shlex 5 | import shutil 6 | import subprocess 7 | import sys 8 | import threading 9 | from string import Template 10 | 11 | import progressbar 12 | import requests 13 | from django.core import management 14 | from django.core.management.base import CommandError 15 | 16 | # import the constants module 17 | from django_webpack_dev_server.management import constants 18 | from dotenv import load_dotenv 19 | 20 | # load the environment variables 21 | load_dotenv() 22 | 23 | 24 | class Generator: 25 | """ 26 | Base Class to create a django app with the necessary frontend configuration 27 | """ 28 | 29 | # set the values of app_name, frontend_library_or_framework in the constructor 30 | def __init__(self, app_name, frontend_library_or_framework): 31 | """ 32 | Constructor to set up the app_name and frontend_library_or_framework variables of the class 33 | :param app_name str: Name of the django app with frontend configuration 34 | :param frontend_library_or_framework str: Denotes the configuration to setup in the django app. 35 | """ 36 | 37 | # app_name is the name of the django app which will have the frontend configuration 38 | self.app_name = app_name 39 | 40 | # frontend_library_or_framework denotes the frontend framework or library requested from the command line 41 | self.frontend_library_or_framework = frontend_library_or_framework 42 | 43 | # static_directory_name is the name of the the static directory inside the django app 44 | self.static_directory_name = "static" 45 | 46 | # set the shell_parameter according to the operating system of the user 47 | # shell_parameter = True for windows based system 48 | # shell_parameter = False for non windows based system 49 | self.shell_parameter = sys.platform == constants.WINDOWS_OS_IDENTIFIER 50 | 51 | def validate_appname(self): 52 | """ 53 | Validates the app name of the django app provided by the user via commandline 54 | """ 55 | # if appname contains non alphanumeric characters then raise error and inform the user 56 | if not self.app_name.isalnum(): 57 | raise CommandError( 58 | constants.COMMAND_ERROR_MESSAGES_DICT["INVALID_APP_NAME_ERROR_MESSAGE"] 59 | ) 60 | 61 | def check_system_requirements(self): 62 | """ 63 | Checks if system has node and npm installed with the help of the 64 | subprocess module 65 | """ 66 | # command to check node in system 67 | command_for_node = "node -v" 68 | 69 | # command to check npm in system 70 | command_for_npm = "npm -v" 71 | 72 | # checks whether node is installed by getting node version 73 | subprocess_node_command = subprocess.run( 74 | shlex.split(command_for_node), 75 | capture_output=True, 76 | shell=self.shell_parameter, 77 | ) 78 | 79 | # checks whether npm is installed by getting npm version 80 | subprocess_npm_command = subprocess.run( 81 | shlex.split(command_for_npm), 82 | capture_output=True, 83 | shell=self.shell_parameter, 84 | ) 85 | 86 | # if either of the commands fail then raise an error and inform the user 87 | if ( 88 | subprocess_node_command.returncode != 0 89 | or subprocess_npm_command.returncode != 0 90 | ): 91 | raise CommandError( 92 | constants.COMMAND_ERROR_MESSAGES_DICT["SYSTEM_ERROR_MESSAGE"] 93 | ) 94 | 95 | def create_required_directories(self): 96 | """ 97 | Creates templates directory and static directory inside the django 98 | app which will have the frontend configuration 99 | """ 100 | 101 | # create the path for the templates directory 102 | templates_directory_path = os.path.join( 103 | os.getcwd(), 104 | self.app_name, 105 | constants.TEMPLATES_DIRECTORY_NAME, 106 | self.app_name, 107 | ) 108 | 109 | # create the path for the static directory 110 | static_directory_path = os.path.join( 111 | os.getcwd(), self.app_name, self.static_directory_name, self.app_name 112 | ) 113 | 114 | # create the path for the src directory 115 | src_directory_path = os.path.join( 116 | os.getcwd(), self.app_name, constants.SRC_DIRECTORY_NAME 117 | ) 118 | 119 | # create the necessary directories inside the newly created django app 120 | try: 121 | os.makedirs(templates_directory_path) 122 | os.makedirs(static_directory_path) 123 | os.makedirs(src_directory_path) 124 | 125 | except OSError as error: 126 | raise CommandError(error) 127 | 128 | def check_if_file_is_text_document(self, filename): 129 | """ 130 | Check whether file is of form textdocument or an media file with the help of file extension 131 | :param filename str: name of the file to check 132 | :return is_text_document bool: Whether the file is text document or a media file 133 | """ 134 | 135 | is_text_document = True 136 | 137 | # get the extension of the file from the filename 138 | _, file_extension = os.path.splitext(filename) 139 | 140 | # list of non text document file extensions 141 | non_text_document_file_extensions = [".png"] 142 | 143 | # if the file extension is in the extensions list then return False 144 | if file_extension in non_text_document_file_extensions: 145 | return not is_text_document 146 | 147 | # return the boolean value 148 | return is_text_document 149 | 150 | def get_target_path_of_template_file(self, filename, directory_type): 151 | """ 152 | Finds the path where the file will be placed after modifications 153 | :param filename str: name of the file to get the path 154 | :param directory_type str: name of the directory where file will be stored 155 | :return target_filepath str: final filepath of the file after it is substituted with the parameters 156 | """ 157 | 158 | # initialize the target_filepath as the current working directory 159 | target_filepath = os.getcwd() 160 | 161 | # append the specific path as per the directory in which the file would be stored 162 | if directory_type == constants.APP_DIRECTORY_NAME: 163 | target_filepath = os.path.join(target_filepath, self.app_name) 164 | 165 | elif directory_type == constants.SRC_DIRECTORY_NAME: 166 | target_filepath = os.path.join( 167 | target_filepath, self.app_name, constants.SRC_DIRECTORY_NAME 168 | ) 169 | 170 | elif directory_type == constants.TEMPLATES_DIRECTORY_NAME: 171 | target_filepath = os.path.join( 172 | target_filepath, 173 | self.app_name, 174 | constants.TEMPLATES_DIRECTORY_NAME, 175 | self.app_name, 176 | ) 177 | 178 | # append the filename into the target_filepath 179 | target_filepath = os.path.join(target_filepath, filename) 180 | 181 | # return the target_filepath 182 | return target_filepath 183 | 184 | def download_template_files(self): 185 | """ 186 | Download template files from the Git Repository and substitute the 187 | necessary parameters 188 | """ 189 | 190 | # get all template files as per the requirement 191 | template_files = constants.PROD_TEMPLATE_FILES_DICT.get( 192 | self.frontend_library_or_framework 193 | ) 194 | 195 | # replace the parameters with appropriate values 196 | substitute_parameters = {"app_name": self.app_name} 197 | 198 | # iterate all the tempate filenames 199 | for directory_type, filename, download_url in template_files: 200 | # get the path where the file will be stored 201 | target_filepath = self.get_target_path_of_template_file( 202 | filename, directory_type 203 | ) 204 | 205 | # Download the files from the Git Repository 206 | download_file = requests.get(download_url, stream=True) 207 | 208 | # write the contents of the file in the appropriate location 209 | with open(target_filepath, "wb") as target_file: 210 | try: 211 | target_file.write(download_file.content) 212 | 213 | except OSError as error: 214 | raise CommandError(error) 215 | 216 | # if file is not a type of text document then do not perform 217 | # string templating for it 218 | if not self.check_if_file_is_text_document(filename): 219 | continue 220 | 221 | # open the file and perform templating to replace the necessary parameters 222 | with open(target_filepath, "r") as target_file: 223 | # read the file contents and substitute the parameters 224 | source_file = Template(target_file.read()) 225 | modified_file_contents = source_file.substitute(substitute_parameters) 226 | 227 | # write the updated file in the appropriate location 228 | with open(target_filepath, "w") as target_file: 229 | try: 230 | target_file.write(modified_file_contents) 231 | 232 | except OSError as error: 233 | raise CommandError(error) 234 | 235 | def load_assets_from_local(self): 236 | """ 237 | A development only method executed when SOFTWARE_ENVIRONMENT_MODE == development 238 | It loads the assets files from local system instead of downloading from the 239 | Git Repository 240 | """ 241 | 242 | # get all template files as per the requirement 243 | template_files = constants.DEVELOPMENT_TEMPLATE_FILES_DICT.get( 244 | self.frontend_library_or_framework 245 | ) 246 | 247 | # replace the parameters with appropriate values 248 | substitute_parameters = {"app_name": self.app_name} 249 | 250 | # iterate all the tempate filenames 251 | for directory_type, filename, local_asset_file_path in template_files: 252 | # get the path where the file will be stored 253 | target_filepath = self.get_target_path_of_template_file( 254 | filename, directory_type 255 | ) 256 | 257 | # copy the template file from the local assets directory to the target path 258 | shutil.copy(local_asset_file_path, target_filepath) 259 | 260 | # if file is not a type of text document then do not template that file 261 | if not self.check_if_file_is_text_document(filename): 262 | continue 263 | 264 | # open the files and perform templating to replace the necessary parameters 265 | with open(target_filepath, "r") as target_file: 266 | source_file = Template(target_file.read()) 267 | modified_file_contents = source_file.substitute(substitute_parameters) 268 | 269 | # write the updated file in the appropriate location 270 | with open(target_filepath, "w") as target_file: 271 | try: 272 | target_file.write(modified_file_contents) 273 | 274 | except OSError as error: 275 | raise CommandError(error) 276 | 277 | def install_dependencies(self, thread_queue): 278 | """ 279 | Installs the node dependencies by executing npm install command 280 | :param thread_queue Queue: Queue which will pass the status of the command 281 | to main thread 282 | """ 283 | # set the current working directory to the newly created django app 284 | os.chdir(self.app_name) 285 | 286 | # command to install node dependencies 287 | command_for_npm_install = "npm install" 288 | 289 | # execute the npm install command for installing dependencies 290 | command = subprocess.Popen( 291 | shlex.split(command_for_npm_install), 292 | shell=self.shell_parameter, 293 | stdout=subprocess.PIPE, 294 | stderr=subprocess.STDOUT, 295 | ) 296 | 297 | # get any data genrated by the command and display it on stdout 298 | while True: 299 | # read output lines one line at a time 300 | realtime_output = command.stdout.readline() 301 | 302 | # if output line is empty then terminate the loop 303 | if realtime_output == "" or command.poll() is not None: 304 | break 305 | elif realtime_output: 306 | # display messages in stdout 307 | print(realtime_output.strip().decode()) 308 | sys.stdout.flush() 309 | 310 | # set the working directory to the original value 311 | os.chdir("../") 312 | # return the status of the installation command into the main thread 313 | thread_queue.put(command.returncode) 314 | 315 | def install_dependencies_and_show_progress_bar(self): 316 | """ 317 | Install the necessary node dependencies and display a progress bar 318 | while the installation command executes 319 | """ 320 | # setting the progress bar message and type 321 | widgets = ["Installing Node Dependencies ", progressbar.AnimatedMarker()] 322 | 323 | # create an indefinite progressbar because time for installing dependencies is not equal for all users 324 | bar = progressbar.ProgressBar( 325 | widgets=widgets, max_value=progressbar.UnknownLength, redirect_stdout=True 326 | ) 327 | bar.start() 328 | 329 | # thread_queue is used to fetch the output of the function executed in the other thread 330 | thread_queue = queue.Queue() 331 | count = 0 332 | 333 | # create a thread which will execute the npm install command 334 | thread1 = threading.Thread( 335 | target=self.install_dependencies, args=[thread_queue], name="thread1" 336 | ) 337 | 338 | # start the installation command by starting the thread 339 | thread1.start() 340 | 341 | # display the progress until the other thread is alive 342 | while thread1.is_alive(): 343 | count += 1 344 | bar.update(count) 345 | 346 | # fetching status code of the command executed in other thread 347 | status = thread_queue.get() 348 | 349 | # if command failed then inform the user 350 | if status != 0: 351 | raise CommandError( 352 | constants.COMMAND_ERROR_MESSAGES_DICT["NPM_INSTALLATION_ERROR_MESSAGE"] 353 | ) 354 | 355 | def generate(self): 356 | """ 357 | Generates a django app with the selected frontend library or framework 358 | Driver method which will execute the other methods 359 | """ 360 | self.validate_appname() 361 | self.check_system_requirements() 362 | management.call_command("startapp", self.app_name) 363 | self.create_required_directories() 364 | 365 | # In development mode the assets will be loaded from the local system 366 | if os.environ.get("SOFTWARE_ENVIRONMENT_MODE") == "development": 367 | print("Loading the Assets Locally") 368 | self.load_assets_from_local() 369 | 370 | # In production mode the assets will be downloaded from the Git Repository 371 | else: 372 | self.download_template_files() 373 | 374 | self.install_dependencies_and_show_progress_bar() 375 | -------------------------------------------------------------------------------- /django_webpack_dev_server/management/template_constants/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/c47e5208dc87ed4a9550674c7374764e97a9fb86/django_webpack_dev_server/management/template_constants/__init__.py -------------------------------------------------------------------------------- /django_webpack_dev_server/management/template_constants/development/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/c47e5208dc87ed4a9550674c7374764e97a9fb86/django_webpack_dev_server/management/template_constants/development/__init__.py -------------------------------------------------------------------------------- /django_webpack_dev_server/management/template_constants/development/development_constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django_webpack_dev_server.management.template_constants.development.reactjs_constants import ( 4 | DEV_REACTJS_TEMPLATE_FILES, 5 | ) 6 | 7 | from django_webpack_dev_server.management.template_constants.development.reactts_constants import ( 8 | DEV_REACTTS_TEMPLATE_FILES, 9 | ) 10 | 11 | DEVELOPMENT_ASSETS_DIRECTORY_PATH = os.path.join(os.getcwd(), "assets") 12 | 13 | APP_DIRECTORY_NAME = "APP" 14 | SRC_DIRECTORY_NAME = "src" 15 | TEMPLATES_DIRECTORY_NAME = "templates" 16 | COMMON_ASSETS_DIRECTORY = "Common" 17 | REACTJS = "reactjs" 18 | 19 | # Paths of the common template files which are loaded locally 20 | DEV_COMMON_TEMPLATES_PATHS_DICT = { 21 | "urls.py-tpl": os.path.join( 22 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, COMMON_ASSETS_DIRECTORY, "urls.py-tpl" 23 | ), 24 | "views.py-tpl": os.path.join( 25 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, COMMON_ASSETS_DIRECTORY, "views.py-tpl" 26 | ), 27 | "index.html-tpl": os.path.join( 28 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, COMMON_ASSETS_DIRECTORY, "index.html-tpl" 29 | ), 30 | "djangologo.png": os.path.join( 31 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, COMMON_ASSETS_DIRECTORY, "djangologo.png" 32 | ), 33 | } 34 | 35 | # Development template files which are common for any frontend library or framework 36 | DEV_COMMON_TEMPLATE_FILES = [ 37 | ( 38 | APP_DIRECTORY_NAME, 39 | "urls.py", 40 | os.path.join( 41 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, COMMON_ASSETS_DIRECTORY, "urls.py-tpl" 42 | ), 43 | ), 44 | ( 45 | APP_DIRECTORY_NAME, 46 | "views.py", 47 | os.path.join( 48 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, COMMON_ASSETS_DIRECTORY, "views.py-tpl" 49 | ), 50 | ), 51 | ( 52 | SRC_DIRECTORY_NAME, 53 | "djangologo.png", 54 | os.path.join( 55 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, COMMON_ASSETS_DIRECTORY, "djangologo.png" 56 | ), 57 | ), 58 | ( 59 | TEMPLATES_DIRECTORY_NAME, 60 | "index.html", 61 | os.path.join( 62 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, COMMON_ASSETS_DIRECTORY, "index.html-tpl" 63 | ), 64 | ), 65 | ] 66 | 67 | DEV_ALL_REACTJS_TEMPLATE_FILES = DEV_COMMON_TEMPLATE_FILES + DEV_REACTJS_TEMPLATE_FILES 68 | 69 | DEV_ALL_REACTTS_TEMPLATE_FILES = DEV_COMMON_TEMPLATE_FILES + DEV_REACTTS_TEMPLATE_FILES 70 | -------------------------------------------------------------------------------- /django_webpack_dev_server/management/template_constants/development/reactjs_constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | DEVELOPMENT_ASSETS_DIRECTORY_PATH = os.path.join(os.getcwd(), "assets") 4 | 5 | APP_DIRECTORY_NAME = "APP" 6 | SRC_DIRECTORY_NAME = "src" 7 | TEMPLATES_DIRECTORY_NAME = "templates" 8 | COMMON_ASSETS_DIRECTORY = "Common" 9 | REACTJS = "reactjs" 10 | 11 | # stores the paths of the reactjs template files which are loaded locally 12 | DEV_REACTJS_TEMPLATES_PATHS_DICT = { 13 | "package.json-tpl": os.path.join( 14 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, REACTJS, "package.json-tpl" 15 | ), 16 | "webpack.config.js-tpl": os.path.join( 17 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, REACTJS, "webpack.config.js-tpl" 18 | ), 19 | "babel.config.json-tpl": os.path.join( 20 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, REACTJS, "babel.config.json-tpl" 21 | ), 22 | "App.js-tpl": os.path.join( 23 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, REACTJS, "App.js-tpl" 24 | ), 25 | "index.js-tpl": os.path.join( 26 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, REACTJS, "index.js-tpl" 27 | ), 28 | "App.css-tpl": os.path.join( 29 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, REACTJS, "App.css-tpl" 30 | ), 31 | "reactlogo.png": os.path.join( 32 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, REACTJS, "reactlogo.png" 33 | ), 34 | } 35 | 36 | # Development template files for reactjs configuration 37 | DEV_REACTJS_TEMPLATE_FILES = [ 38 | ( 39 | APP_DIRECTORY_NAME, 40 | "package.json", 41 | DEV_REACTJS_TEMPLATES_PATHS_DICT["package.json-tpl"], 42 | ), 43 | ( 44 | APP_DIRECTORY_NAME, 45 | "webpack.config.js", 46 | DEV_REACTJS_TEMPLATES_PATHS_DICT["webpack.config.js-tpl"], 47 | ), 48 | ( 49 | APP_DIRECTORY_NAME, 50 | "babel.config.json", 51 | DEV_REACTJS_TEMPLATES_PATHS_DICT["babel.config.json-tpl"], 52 | ), 53 | (SRC_DIRECTORY_NAME, "App.js", DEV_REACTJS_TEMPLATES_PATHS_DICT["App.js-tpl"]), 54 | (SRC_DIRECTORY_NAME, "index.js", DEV_REACTJS_TEMPLATES_PATHS_DICT["index.js-tpl"]), 55 | (SRC_DIRECTORY_NAME, "App.css", DEV_REACTJS_TEMPLATES_PATHS_DICT["App.css-tpl"]), 56 | ( 57 | SRC_DIRECTORY_NAME, 58 | "reactlogo.png", 59 | DEV_REACTJS_TEMPLATES_PATHS_DICT["reactlogo.png"], 60 | ), 61 | ] 62 | -------------------------------------------------------------------------------- /django_webpack_dev_server/management/template_constants/development/reactts_constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | DEVELOPMENT_ASSETS_DIRECTORY_PATH = os.path.join(os.getcwd(), "assets") 4 | 5 | APP_DIRECTORY_NAME = "APP" 6 | SRC_DIRECTORY_NAME = "src" 7 | TEMPLATES_DIRECTORY_NAME = "templates" 8 | COMMON_ASSETS_DIRECTORY = "Common" 9 | REACTTS = "reactts" 10 | 11 | # stores the paths of the reactjs template files which are loaded locally 12 | DEV_REACTTS_TEMPLATES_PATHS_DICT = { 13 | "package.json-tpl": os.path.join( 14 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, REACTTS, "package.json-tpl" 15 | ), 16 | "webpack.config.js-tpl": os.path.join( 17 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, REACTTS, "webpack.config.js-tpl" 18 | ), 19 | "babel.config.json-tpl": os.path.join( 20 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, REACTTS, "babel.config.json-tpl" 21 | ), 22 | "tsconfig.json-tpl": os.path.join( 23 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, REACTTS, "tsconfig.json-tpl" 24 | ), 25 | "module.d.ts-tpl": os.path.join( 26 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, REACTTS, "module.d.ts-tpl" 27 | ), 28 | "App.tsx-tpl": os.path.join( 29 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, REACTTS, "App.tsx-tpl" 30 | ), 31 | "index.tsx-tpl": os.path.join( 32 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, REACTTS, "index.tsx-tpl" 33 | ), 34 | "App.css-tpl": os.path.join( 35 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, REACTTS, "App.css-tpl" 36 | ), 37 | "reactlogo.png": os.path.join( 38 | DEVELOPMENT_ASSETS_DIRECTORY_PATH, REACTTS, "reactlogo.png" 39 | ), 40 | } 41 | 42 | # Development template files for reactjs configuration 43 | DEV_REACTTS_TEMPLATE_FILES = [ 44 | ( 45 | APP_DIRECTORY_NAME, 46 | "package.json", 47 | DEV_REACTTS_TEMPLATES_PATHS_DICT["package.json-tpl"], 48 | ), 49 | ( 50 | APP_DIRECTORY_NAME, 51 | "webpack.config.js", 52 | DEV_REACTTS_TEMPLATES_PATHS_DICT["webpack.config.js-tpl"], 53 | ), 54 | ( 55 | APP_DIRECTORY_NAME, 56 | "babel.config.json", 57 | DEV_REACTTS_TEMPLATES_PATHS_DICT["babel.config.json-tpl"], 58 | ), 59 | ( 60 | APP_DIRECTORY_NAME, 61 | "tsconfig.json", 62 | DEV_REACTTS_TEMPLATES_PATHS_DICT["tsconfig.json-tpl"], 63 | ), 64 | ( 65 | SRC_DIRECTORY_NAME, 66 | "module.d.ts", 67 | DEV_REACTTS_TEMPLATES_PATHS_DICT["module.d.ts-tpl"], 68 | ), 69 | (SRC_DIRECTORY_NAME, "App.tsx", DEV_REACTTS_TEMPLATES_PATHS_DICT["App.tsx-tpl"]), 70 | ( 71 | SRC_DIRECTORY_NAME, 72 | "index.tsx", 73 | DEV_REACTTS_TEMPLATES_PATHS_DICT["index.tsx-tpl"], 74 | ), 75 | (SRC_DIRECTORY_NAME, "App.css", DEV_REACTTS_TEMPLATES_PATHS_DICT["App.css-tpl"]), 76 | ( 77 | SRC_DIRECTORY_NAME, 78 | "reactlogo.png", 79 | DEV_REACTTS_TEMPLATES_PATHS_DICT["reactlogo.png"], 80 | ), 81 | ] 82 | -------------------------------------------------------------------------------- /django_webpack_dev_server/management/template_constants/production/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/c47e5208dc87ed4a9550674c7374764e97a9fb86/django_webpack_dev_server/management/template_constants/production/__init__.py -------------------------------------------------------------------------------- /django_webpack_dev_server/management/template_constants/production/production_constants.py: -------------------------------------------------------------------------------- 1 | from django_webpack_dev_server.management.template_constants.production.reactjs_constants import ( 2 | PROD_REACTJS_TEMPLATE_FILES, 3 | ) 4 | 5 | from django_webpack_dev_server.management.template_constants.production.reactts_constants import ( 6 | PROD_REACTTS_TEMPLATE_FILES, 7 | ) 8 | 9 | APP_DIRECTORY_NAME = "APP" 10 | SRC_DIRECTORY_NAME = "src" 11 | TEMPLATES_DIRECTORY_NAME = "templates" 12 | 13 | 14 | COMMON_TEMPLATES_URLS_DICT = { 15 | "urls.py-tpl": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/Common/urls.py-tpl", 16 | "views.py-tpl": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/Common/views.py-tpl", 17 | "index.html-tpl": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/Common/index.html-tpl", 18 | "djangologo.png": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/Common/djangologo.png", 19 | } 20 | 21 | # Template files that will be common for any frontend library or framework 22 | COMMON_TEMPLATE_FILES = [ 23 | (APP_DIRECTORY_NAME, "urls.py", COMMON_TEMPLATES_URLS_DICT["urls.py-tpl"]), 24 | (APP_DIRECTORY_NAME, "views.py", COMMON_TEMPLATES_URLS_DICT["views.py-tpl"]), 25 | ( 26 | SRC_DIRECTORY_NAME, 27 | "djangologo.png", 28 | COMMON_TEMPLATES_URLS_DICT["djangologo.png"], 29 | ), 30 | ( 31 | TEMPLATES_DIRECTORY_NAME, 32 | "index.html", 33 | COMMON_TEMPLATES_URLS_DICT["index.html-tpl"], 34 | ), 35 | ] 36 | 37 | PROD_ALL_REACTJS_TEMPLATE_FILES = COMMON_TEMPLATE_FILES + PROD_REACTJS_TEMPLATE_FILES 38 | 39 | PROD_ALL_REACTTS_TEMPLATE_FILES = COMMON_TEMPLATE_FILES + PROD_REACTTS_TEMPLATE_FILES 40 | -------------------------------------------------------------------------------- /django_webpack_dev_server/management/template_constants/production/reactjs_constants.py: -------------------------------------------------------------------------------- 1 | APP_DIRECTORY_NAME = "APP" 2 | SRC_DIRECTORY_NAME = "src" 3 | TEMPLATES_DIRECTORY_NAME = "templates" 4 | 5 | # Production React Js Template files having Github Repository Links 6 | REACTJS_TEMPLATES_URLS_DICT = { 7 | "package.json-tpl": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/reactjs/package.json-tpl", 8 | "webpack.config.js-tpl": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/reactjs/webpack.config.js-tpl", 9 | "babel.config.json-tpl": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/reactjs/babel.config.json-tpl", 10 | "App.js-tpl": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/reactjs/App.js-tpl", 11 | "index.js-tpl": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/reactjs/index.js-tpl", 12 | "App.css-tpl": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/reactjs/App.css-tpl", 13 | "reactlogo.png": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/reactjs/reactlogo.png", 14 | } 15 | 16 | # Template files specific to react js configuration 17 | PROD_REACTJS_TEMPLATE_FILES = [ 18 | ( 19 | APP_DIRECTORY_NAME, 20 | "package.json", 21 | REACTJS_TEMPLATES_URLS_DICT["package.json-tpl"], 22 | ), 23 | ( 24 | APP_DIRECTORY_NAME, 25 | "webpack.config.js", 26 | REACTJS_TEMPLATES_URLS_DICT["webpack.config.js-tpl"], 27 | ), 28 | ( 29 | APP_DIRECTORY_NAME, 30 | "babel.config.json", 31 | REACTJS_TEMPLATES_URLS_DICT["babel.config.json-tpl"], 32 | ), 33 | (SRC_DIRECTORY_NAME, "App.js", REACTJS_TEMPLATES_URLS_DICT["App.js-tpl"]), 34 | (SRC_DIRECTORY_NAME, "index.js", REACTJS_TEMPLATES_URLS_DICT["index.js-tpl"]), 35 | (SRC_DIRECTORY_NAME, "App.css", REACTJS_TEMPLATES_URLS_DICT["App.css-tpl"]), 36 | (SRC_DIRECTORY_NAME, "reactlogo.png", REACTJS_TEMPLATES_URLS_DICT["reactlogo.png"]), 37 | ] 38 | -------------------------------------------------------------------------------- /django_webpack_dev_server/management/template_constants/production/reactts_constants.py: -------------------------------------------------------------------------------- 1 | APP_DIRECTORY_NAME = "APP" 2 | SRC_DIRECTORY_NAME = "src" 3 | TEMPLATES_DIRECTORY_NAME = "templates" 4 | 5 | # Production React Ts Template files having Github Repository Links 6 | REACTTS_TEMPLATES_URLS_DICT = { 7 | "package.json-tpl": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/reactts/package.json-tpl", 8 | "webpack.config.js-tpl": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/reactts/webpack.config.js-tpl", 9 | "babel.config.json-tpl": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/reactts/babel.config.json-tpl", 10 | "tsconfig.json-tpl": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/reactts/tsconfig.json-tpl", 11 | "module.d.ts-tpl": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/reactts/module.d.ts-tpl", 12 | "App.tsx-tpl": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/reactts/App.tsx-tpl", 13 | "index.tsx-tpl": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/reactts/index.tsx-tpl", 14 | "App.css-tpl": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/reactts/App.css-tpl", 15 | "reactlogo.png": "https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/main/assets/reactts/reactlogo.png", 16 | } 17 | 18 | # Template files specific to react ts configuration 19 | PROD_REACTTS_TEMPLATE_FILES = [ 20 | ( 21 | APP_DIRECTORY_NAME, 22 | "package.json", 23 | REACTTS_TEMPLATES_URLS_DICT["package.json-tpl"], 24 | ), 25 | ( 26 | APP_DIRECTORY_NAME, 27 | "webpack.config.js", 28 | REACTTS_TEMPLATES_URLS_DICT["webpack.config.js-tpl"], 29 | ), 30 | ( 31 | APP_DIRECTORY_NAME, 32 | "babel.config.json", 33 | REACTTS_TEMPLATES_URLS_DICT["babel.config.json-tpl"], 34 | ), 35 | ( 36 | APP_DIRECTORY_NAME, 37 | "tsconfig.json", 38 | REACTTS_TEMPLATES_URLS_DICT["tsconfig.json-tpl"], 39 | ), 40 | (SRC_DIRECTORY_NAME, "module.d.ts", REACTTS_TEMPLATES_URLS_DICT["module.d.ts-tpl"]), 41 | (SRC_DIRECTORY_NAME, "App.tsx", REACTTS_TEMPLATES_URLS_DICT["App.tsx-tpl"]), 42 | (SRC_DIRECTORY_NAME, "index.tsx", REACTTS_TEMPLATES_URLS_DICT["index.tsx-tpl"]), 43 | (SRC_DIRECTORY_NAME, "App.css", REACTTS_TEMPLATES_URLS_DICT["App.css-tpl"]), 44 | (SRC_DIRECTORY_NAME, "reactlogo.png", REACTTS_TEMPLATES_URLS_DICT["reactlogo.png"]), 45 | ] 46 | -------------------------------------------------------------------------------- /django_webpack_dev_server/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/c47e5208dc87ed4a9550674c7374764e97a9fb86/django_webpack_dev_server/migrations/__init__.py -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = tests.test_settings -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | -r requirements/requirements-dev.txt 3 | -r requirements/requirements-lint.txt 4 | -r requirements/requirements-testing.txt 5 | -------------------------------------------------------------------------------- /requirements/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | python-dotenv 2 | progressbar2 3 | requests 4 | pre-commit -------------------------------------------------------------------------------- /requirements/requirements-lint.txt: -------------------------------------------------------------------------------- 1 | black -------------------------------------------------------------------------------- /requirements/requirements-testing.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-django 3 | coverage -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import django 5 | from django.conf import settings 6 | from django.test.utils import get_runner 7 | 8 | if __name__ == "__main__": 9 | os.environ["DJANGO_SETTINGS_MODULE"] = "tests.test_settings" 10 | django.setup() 11 | TestRunner = get_runner(settings) 12 | test_runner = TestRunner() 13 | failures = test_runner.run_tests(["tests"]) 14 | sys.exit(bool(failures)) 15 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description_file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | package_description = ( 4 | "A Django app to setup configuration files for React in a Django Project." 5 | ) 6 | 7 | # to ignore the directories while creating wheel distribution 8 | exclude_directories_for_wheel_distribution = ["tests*", "mysite"] 9 | 10 | keywords_list = [ 11 | "django", 12 | "react", 13 | "webpack5", 14 | "webpack_dev_server", 15 | "command-line", 16 | "project-generator", 17 | "template-generator", 18 | "javascript", 19 | "typescript", 20 | ] 21 | 22 | setup( 23 | name="django-webpack-dev-server", 24 | version="1.0.0", 25 | packages=find_packages(exclude=exclude_directories_for_wheel_distribution), 26 | license="MIT", 27 | author="Jiten Sidhpura", 28 | author_email="jitensidhpura2000@gmail.com", 29 | description=package_description, 30 | long_description=open("README.md").read(), 31 | long_description_content_type="text/markdown", 32 | url="https://github.com/Jitensid/django-webpack-dev-server", 33 | download_url="https://github.com/Jitensid/django-webpack-dev-server/archive/refs/tags/1.0.0.tar.gz", 34 | install_requires=["requests", "progressbar2", "python-dotenv"], 35 | keywords=keywords_list, 36 | classifiers=[ 37 | "Intended Audience :: Developers", 38 | "Topic :: Software Development :: Code Generators", 39 | "Framework :: Django", 40 | "License :: OSI Approved :: MIT License", 41 | "Operating System :: OS Independent", 42 | "Programming Language :: Python :: 3.6", 43 | "Programming Language :: Python :: 3.7", 44 | "Programming Language :: Python :: 3.8", 45 | "Programming Language :: Python :: 3.9", 46 | "Programming Language :: Python :: 3.10", 47 | ], 48 | ) 49 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/c47e5208dc87ed4a9550674c7374764e97a9fb86/tests/__init__.py -------------------------------------------------------------------------------- /tests/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/c47e5208dc87ed4a9550674c7374764e97a9fb86/tests/management/__init__.py -------------------------------------------------------------------------------- /tests/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jitensid/django-webpack-dev-server/c47e5208dc87ed4a9550674c7374764e97a9fb86/tests/management/commands/__init__.py -------------------------------------------------------------------------------- /tests/management/commands/test_generate.py: -------------------------------------------------------------------------------- 1 | from django.core.management import call_command 2 | from unittest import mock 3 | from django_webpack_dev_server.management.generator import Generator 4 | 5 | 6 | class TestCommand: 7 | """ 8 | Test Class for testing the Command Class defined in the generate.py 9 | """ 10 | 11 | @mock.patch.object(Generator, "generate") 12 | def test_command(self, mocked_Generator_generate): 13 | """ 14 | Function to test the methods of the Command Class 15 | """ 16 | # call the management command to test 17 | call_command("generate", "react") 18 | 19 | # assert that the generate method of the Generator class is called 20 | assert mocked_Generator_generate.called == True 21 | -------------------------------------------------------------------------------- /tests/management/test_generator.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import queue 4 | from unittest import mock 5 | 6 | import pytest 7 | from django.core.management.base import CommandError 8 | from django_webpack_dev_server.management import constants 9 | from django_webpack_dev_server.management.generator import Generator 10 | 11 | 12 | # A pytest fixture used to create a Generator object 13 | @pytest.fixture 14 | def app_generator(): 15 | # Create the Generator object 16 | app_generator = Generator( 17 | app_name="frontend", frontend_library_or_framework="react_javascript" 18 | ) 19 | 20 | # return the Generator object to the test 21 | return app_generator 22 | 23 | 24 | class TestGeneratorClass: 25 | """ 26 | Test Class for testing the Generator Class defined in generator.py 27 | """ 28 | 29 | def test_validate_app_name(self): 30 | """ 31 | Function to test the validate_app_name method of the Generator Class 32 | """ 33 | 34 | # Create the Generator object and pass an invalid app_name to test 35 | app_generator = Generator( 36 | app_name="frontend@@@", frontend_library_or_framework="react_javascript" 37 | ) 38 | 39 | # Check if CommandError is raised when the method is called with invalid app_name 40 | with pytest.raises(CommandError) as CommandErrorException: 41 | app_generator.validate_appname() 42 | 43 | # assert the message of the exception 44 | assert ( 45 | str(CommandErrorException.value) 46 | == constants.COMMAND_ERROR_MESSAGES_DICT["INVALID_APP_NAME_ERROR_MESSAGE"] 47 | ) 48 | 49 | @mock.patch("subprocess.run") 50 | def test_check_system_requirements(self, mocked_subprocess_run, app_generator): 51 | """ 52 | Function to test the check_system_requirements method of the Generator Class 53 | """ 54 | 55 | # mock the subprocess run method and set response to 0 56 | # Response 0 means that the command ran successfully 57 | mocked_subprocess_run.return_value.returncode = 0 58 | 59 | # call the method to test 60 | app_generator.check_system_requirements() 61 | 62 | # assert that the subprocess run method was called 2 times 63 | assert mocked_subprocess_run.call_count == 2 64 | 65 | # Reset the mock 66 | mocked_subprocess_run.reset_mock() 67 | 68 | # mock the subprocess run method and set response to 1 69 | # Response 1 means that the command did not ran successfully 70 | mocked_subprocess_run.return_value.returncode = 1 71 | 72 | # Check if CommandError is raised when the method is called 73 | with pytest.raises(CommandError) as CommandErrorException: 74 | # call the method to test 75 | app_generator.check_system_requirements() 76 | 77 | # assert the message of the exception 78 | assert ( 79 | str(CommandErrorException.value) 80 | == constants.COMMAND_ERROR_MESSAGES_DICT["SYSTEM_ERROR_MESSAGE"] 81 | ) 82 | 83 | # assert that the subprocess run method was called 2 times 84 | assert mocked_subprocess_run.call_count == 2 85 | 86 | @mock.patch("os.makedirs") 87 | def test_create_required_directories(self, mocked_os_makedirs, app_generator): 88 | """ 89 | Function to test the create_required_directories method of the Generator Class 90 | """ 91 | 92 | # call the method to test 93 | app_generator.create_required_directories() 94 | 95 | # assert that the os makedirs method is called 3 times 96 | assert mocked_os_makedirs.call_count == 3 97 | 98 | # reset the mock 99 | mocked_os_makedirs.reset_mock() 100 | 101 | # Check if CommandError is raised when the method is called 102 | with pytest.raises(CommandError) as CommandErrorException: 103 | exception_message = "Error while creating directories" 104 | 105 | # Raise OSError when the os makedirs method is called to 106 | mocked_os_makedirs.side_effect = OSError(exception_message) 107 | 108 | # call the method to test 109 | app_generator.create_required_directories() 110 | 111 | # assert that the os makedirs method is called 1 time 112 | assert mocked_os_makedirs.call_count == 1 113 | 114 | # assert the message of the exception 115 | assert str(CommandErrorException.value) == exception_message 116 | 117 | def test_check_if_file_is_text_document(self, app_generator): 118 | """ 119 | Function to test the check_if_file_is_text_document method of the Generator Class 120 | """ 121 | 122 | # file of type text document 123 | text_document_filename = "generator.py" 124 | 125 | # file of type non text document 126 | non_text_document_filename = "reactlogo.png" 127 | 128 | # assert True if file is of text document type 129 | assert ( 130 | # call the method to test 131 | app_generator.check_if_file_is_text_document(text_document_filename) 132 | == True 133 | ) 134 | 135 | # assert False if file is not of text document type 136 | assert ( 137 | # call the method to test 138 | app_generator.check_if_file_is_text_document(non_text_document_filename) 139 | == False 140 | ) 141 | 142 | def test_get_target_path_of_template_file(self, app_generator): 143 | """ 144 | Function to test the get_target_path_of_template_file method of the Generator Class 145 | """ 146 | filename = "webpack.config.js-tpl" 147 | 148 | # create the path for a while that will be stored under APP directory 149 | target_app_directory_filepath = os.path.join( 150 | os.getcwd(), app_generator.app_name 151 | ) 152 | 153 | # create the path for a while that will be stored under src directory 154 | target_src_directory_filepath = os.path.join( 155 | os.getcwd(), app_generator.app_name, constants.SRC_DIRECTORY_NAME 156 | ) 157 | 158 | # create the path for a while that will be stored under templates directory 159 | target_templates_directory_filepath = os.path.join( 160 | os.getcwd(), 161 | app_generator.app_name, 162 | constants.TEMPLATES_DIRECTORY_NAME, 163 | app_generator.app_name, 164 | ) 165 | 166 | # create the final path for a particular file 167 | target_filepath = os.path.join(target_app_directory_filepath, filename) 168 | 169 | # assert the file path created and returned by the class method 170 | assert ( 171 | # call the method to test 172 | app_generator.get_target_path_of_template_file( 173 | filename, constants.APP_DIRECTORY_NAME 174 | ) 175 | == target_filepath 176 | ) 177 | 178 | # create the final path for a particular file 179 | target_filepath = os.path.join(target_src_directory_filepath, filename) 180 | 181 | # assert the file path created and returned by the class method 182 | assert ( 183 | # call the method to test 184 | app_generator.get_target_path_of_template_file( 185 | filename, constants.SRC_DIRECTORY_NAME 186 | ) 187 | == target_filepath 188 | ) 189 | 190 | # create the final path for a particular file 191 | target_filepath = os.path.join(target_templates_directory_filepath, filename) 192 | 193 | # assert the file path created and returned by the class method 194 | assert ( 195 | # call the method to test 196 | app_generator.get_target_path_of_template_file( 197 | filename, constants.TEMPLATES_DIRECTORY_NAME 198 | ) 199 | == target_filepath 200 | ) 201 | 202 | @mock.patch("requests.get") 203 | @mock.patch("builtins.open", new_callable=mock.mock_open, read_data="My data") 204 | def test_download_template_files( 205 | self, mocked_builtins_open, mocked_requests_get, app_generator 206 | ): 207 | """ 208 | Function to test the download_template_files method of the Generator Class 209 | """ 210 | 211 | # call the method to test 212 | app_generator.download_template_files() 213 | 214 | # assert the number of times the file open is called 215 | assert mocked_builtins_open.call_count == 29 216 | 217 | # assert the number of times the get request is made 218 | assert mocked_requests_get.call_count == 11 219 | 220 | # reset the mocks 221 | mocked_builtins_open.reset_mock() 222 | mocked_requests_get.reset_mock() 223 | 224 | # Check if CommandError is raised when the method is called 225 | with pytest.raises(CommandError) as CommandErrorException: 226 | exception_message = "Error while writing in the file" 227 | 228 | # Raise OSError during the file write operation 229 | mocked_builtins_open.return_value.write.side_effect = OSError( 230 | exception_message 231 | ) 232 | 233 | # call the method to test 234 | app_generator.download_template_files() 235 | 236 | # assert the message of the exception 237 | assert str(CommandErrorException.value) == exception_message 238 | 239 | mocked_builtins_open.reset_mock() 240 | mocked_requests_get.reset_mock() 241 | 242 | # Check if CommandError is raised when the method is called 243 | with pytest.raises(CommandError) as CommandErrorException: 244 | exception_message = "Error while writing in the file" 245 | 246 | # Raise OSError during the file write operation 247 | mocked_builtins_open.return_value.write.side_effect = [ 248 | "No Error for 1st call", 249 | OSError(exception_message), 250 | ] 251 | 252 | # call the method to test 253 | app_generator.download_template_files() 254 | 255 | # assert the message of the exception 256 | assert str(CommandErrorException.value) == exception_message 257 | 258 | @mock.patch("builtins.open", new_callable=mock.mock_open, read_data="My data") 259 | @mock.patch("shutil.copy") 260 | def test_load_assets_from_local( 261 | self, mocked_shutil_copy, mocked_builtins_open, app_generator 262 | ): 263 | """ 264 | Function to test the load_assets_from_local method of the Generator Class 265 | """ 266 | 267 | # call the method to test 268 | app_generator.load_assets_from_local() 269 | 270 | # assert that shutil copy and open functions are called 271 | assert mocked_builtins_open.called == True 272 | assert mocked_shutil_copy.called == True 273 | 274 | # reset the mocks 275 | mocked_shutil_copy.reset_mock() 276 | mocked_builtins_open.reset_mock() 277 | 278 | # Check if CommandError is raised when the method is called 279 | with pytest.raises(CommandError) as CommandErrorException: 280 | exception_message = "Error while writing in the file" 281 | 282 | # Raise OSError during the file write operation 283 | mocked_builtins_open.return_value.write.side_effect = OSError( 284 | exception_message 285 | ) 286 | 287 | # call the method to test 288 | app_generator.load_assets_from_local() 289 | 290 | # assert the message of the exception 291 | assert str(CommandErrorException.value) == exception_message 292 | # assert that shutil copy and open functions are called 293 | assert mocked_builtins_open.called == True 294 | assert mocked_shutil_copy.called == True 295 | 296 | @mock.patch("subprocess.Popen") 297 | @mock.patch("os.chdir") 298 | def test_install_dependencies( 299 | self, 300 | mocked_os_chdir, 301 | mocked_subprocess_popen, 302 | app_generator, 303 | ): 304 | """ 305 | Function to test the test_install_dependencies method of the Generator Class 306 | """ 307 | 308 | # create a queue object which is passed to a thread 309 | thread_queue = queue.Queue() 310 | 311 | # 0 means that command ran successfully 312 | command_return_code = 0 313 | 314 | # mocked the subprocess and set the returncode value to command_return_code 315 | mocked_subprocess_popen.return_value.returncode = command_return_code 316 | 317 | # mocked the subprocess and set the stdout value to some non empty output 318 | mocked_subprocess_popen.return_value.stdout = io.BytesIO(b"Command\nOutput") 319 | 320 | # mocked the subprocess and set the return value of poll method to None and then non-empty output 321 | mocked_subprocess_popen.return_value.poll.side_effect = [ 322 | None, 323 | "Non-empty Output", 324 | ] 325 | 326 | # call the method to test 327 | app_generator.install_dependencies(thread_queue) 328 | 329 | # assert that os chdir method is called 2 times 330 | assert mocked_os_chdir.call_count == 2 331 | # assert that subprocess popen method is called 1 time 332 | assert mocked_subprocess_popen.call_count == 1 333 | 334 | # assert that the queue receives the value from subprocess Popen 335 | # and value equal to the command_return_code 336 | assert thread_queue.get() == command_return_code 337 | 338 | @mock.patch("queue.Queue") 339 | @mock.patch("threading.Thread") 340 | @mock.patch("progressbar.ProgressBar.update") 341 | def test_install_dependencies_and_show_progress_bar( 342 | self, 343 | mocked_progressbar_ProgressBar_update, 344 | mocked_threading_thread, 345 | mocked_queue_Queue, 346 | app_generator, 347 | ): 348 | """ 349 | Function to test the install_dependencies_and_show_progress_bar method of the Generator Class 350 | """ 351 | 352 | # different values for thread is_alive method 353 | thread_is_alive_values = [True, True, True, False] 354 | 355 | # set the mocked threading thread is_alive method with the thread_is_alive_values list 356 | mocked_threading_thread.return_value.is_alive.side_effect = ( 357 | thread_is_alive_values 358 | ) 359 | 360 | # set the mocked queue with the value 0 361 | # 0 implies installation was successful 362 | mocked_queue_Queue.return_value.get.return_value = 0 363 | 364 | # call the method to test 365 | app_generator.install_dependencies_and_show_progress_bar() 366 | 367 | # assert that the mocked threading thread was called 1 time 368 | assert mocked_threading_thread.call_count == 1 369 | 370 | # assert that the mocked progress bar was updated to the number 371 | # of values present in the thread_is_alive_values list 372 | assert mocked_progressbar_ProgressBar_update.call_count == len( 373 | thread_is_alive_values 374 | ) 375 | 376 | # Reset the mocks 377 | mocked_threading_thread.reset_mock() 378 | mocked_queue_Queue.reset_mock() 379 | mocked_progressbar_ProgressBar_update.reset_mock() 380 | 381 | # set the mocked threading thread is_alive method with the thread_is_alive_values list 382 | mocked_threading_thread.return_value.is_alive.side_effect = ( 383 | thread_is_alive_values 384 | ) 385 | 386 | # set the mocked queue with the value 1 387 | # 1 implies installation was not successful 388 | mocked_queue_Queue.return_value.get.return_value = 1 389 | 390 | # Check if CommandError is raised when the method is called 391 | with pytest.raises(CommandError) as CommandErrorException: 392 | # call the method to test 393 | app_generator.install_dependencies_and_show_progress_bar() 394 | 395 | # assert that the mocked threading thread was called 1 time 396 | assert mocked_threading_thread.call_count == 1 397 | 398 | # assert that the mocked progress bar was updated to the number 399 | # of values present in the thread_is_alive_values list 400 | assert mocked_progressbar_ProgressBar_update.call_count == len( 401 | thread_is_alive_values 402 | ) 403 | 404 | # assert the message of the exception 405 | assert ( 406 | str(CommandErrorException.value) 407 | == constants.COMMAND_ERROR_MESSAGES_DICT["NPM_INSTALLATION_ERROR_MESSAGE"] 408 | ) 409 | 410 | @mock.patch.object( 411 | Generator, "install_dependencies_and_show_progress_bar", return_value=None 412 | ) 413 | @mock.patch.object(Generator, "load_assets_from_local", return_value=None) 414 | @mock.patch.object(Generator, "create_required_directories", return_value=None) 415 | @mock.patch("django.core.management.call_command") 416 | @mock.patch.object(Generator, "check_system_requirements", return_value=None) 417 | def test_generate_in_development_mode( 418 | self, 419 | mocked_generator_check_system_requirements, 420 | mocked_management_call_command, 421 | mocked_create_required_directories, 422 | mocked_load_assets_from_local, 423 | mocked_install_dependencies_and_show_progress_bar, 424 | app_generator, 425 | ): 426 | """ 427 | Function to test the generate method of the Generator Class 428 | """ 429 | 430 | # call the method to test 431 | app_generator.generate() 432 | 433 | # assert that all the methods of the Generator class are called 434 | assert mocked_generator_check_system_requirements.called == True 435 | assert mocked_create_required_directories.called == True 436 | mocked_management_call_command.assert_called_once_with( 437 | "startapp", app_generator.app_name 438 | ) 439 | assert mocked_load_assets_from_local.called == True 440 | assert mocked_install_dependencies_and_show_progress_bar.called == True 441 | 442 | @mock.patch.object( 443 | Generator, "install_dependencies_and_show_progress_bar", return_value=None 444 | ) 445 | @mock.patch.object(Generator, "download_template_files", return_value=None) 446 | @mock.patch.object(Generator, "create_required_directories", return_value=None) 447 | @mock.patch("django.core.management.call_command") 448 | @mock.patch.object(Generator, "check_system_requirements", return_value=None) 449 | def test_generate_in_production_mode( 450 | self, 451 | mocked_generator_check_system_requirements, 452 | mocked_management_call_command, 453 | mocked_create_required_directories, 454 | mocked_download_template_files, 455 | mocked_install_dependencies_and_show_progress_bar, 456 | app_generator, 457 | ): 458 | """ 459 | Function to test the generate method of the Generator Class 460 | """ 461 | 462 | # set the environment variable SOFTWARE_ENVIRONMENT_MODE to production 463 | # to test that the download_template_files method is called or not 464 | 465 | # create the monkeypatch object 466 | monkeypatch = pytest.MonkeyPatch() 467 | 468 | # set the environment variable SOFTWARE_ENVIRONMENT_MODE to production 469 | monkeypatch.setenv("SOFTWARE_ENVIRONMENT_MODE", "production") 470 | 471 | # assert that the environment variable has changed 472 | assert os.environ["SOFTWARE_ENVIRONMENT_MODE"] == "production" 473 | 474 | # call the method to test 475 | app_generator.generate() 476 | 477 | # assert that all the methods of the Generator class are called 478 | assert mocked_generator_check_system_requirements.called == True 479 | assert mocked_create_required_directories.called == True 480 | mocked_management_call_command.assert_called_once_with( 481 | "startapp", app_generator.app_name 482 | ) 483 | assert mocked_download_template_files.called == True 484 | assert mocked_install_dependencies_and_show_progress_bar.called == True 485 | -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | # add the django reusable app into the installed_apps list 2 | # By this the command can be tested 3 | INSTALLED_APPS = [ 4 | "django_webpack_dev_server", 5 | ] 6 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36,py37,py38,py39,py310 3 | skip_missing_interpreters = true 4 | 5 | [testenv] 6 | commands = 7 | coverage run -m pytest 8 | coverage xml 9 | 10 | deps = -rrequirements.txt 11 | setenv = 12 | SOFTWARE_ENVIRONMENT_MODE = development --------------------------------------------------------------------------------