├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── README.md ├── build.py ├── client ├── index.html.in ├── main.js ├── package.json └── static │ ├── bower.json │ ├── config.js │ ├── electron.js │ ├── images │ ├── battlenet.png │ ├── brushed_alu_dark.png │ ├── dark_embroidery.png │ ├── dark_fish_skin.png │ ├── dark_mosaic.png │ ├── dark_wood.png │ ├── footer_lodyas.png │ ├── hypnotize.png │ ├── inflicted.png │ ├── px_by_Gre3g.png │ ├── rubber_grip.png │ └── squares.png │ ├── modules │ └── djoser │ │ ├── djoser.main.js │ │ └── djoser.service.js │ ├── router.js │ ├── services │ └── User.js │ ├── states │ ├── Login │ │ ├── Login.html │ │ ├── Login.js │ │ ├── Login.less │ │ └── login-form │ │ │ ├── login-form.html │ │ │ ├── login-form.js │ │ │ └── login-form.less │ ├── Root.less │ ├── Signup │ │ ├── Signup.html │ │ ├── Signup.js │ │ ├── Signup.less │ │ └── signup-form │ │ │ ├── signup-form.html │ │ │ ├── signup-form.js │ │ │ └── signup-form.less │ ├── dashboard │ │ ├── dashboard.html │ │ ├── dashboard.js │ │ └── dashboard.less │ ├── root.html │ └── root.js │ ├── styles │ ├── bootstrap.less │ ├── imports.less │ └── site.less │ └── widgets │ └── input-v │ ├── input-v.html │ ├── input-v.js │ └── input-v.less ├── run.py ├── server ├── manage.py ├── profiles │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_profile_rank.py │ │ ├── 0003_auto_20171119_0224.py │ │ ├── 0004_auto_20181126_0310.py │ │ ├── 0005_remove_profile_comment.py │ │ └── __init__.py │ ├── models.py │ └── serializers.py ├── requirements.txt └── server │ ├── __init__.py │ ├── overrides │ └── serializers.py │ ├── settings.py │ ├── urls.py │ ├── views.py │ └── wsgi.py ├── setup.py └── shared.py /.gitignore: -------------------------------------------------------------------------------- 1 | server/env/* 2 | server/db.sqlite3 3 | server/static/* 4 | server/2fa.settings 5 | client/index.html 6 | client/index.electron.html 7 | client/static/styles/imports.less 8 | client/node_modules/* 9 | client/static/bower_components/* 10 | *.pyc 11 | *.css -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.venvPath": "server\\env", 3 | "python.pythonPath": "server\\env\\Scripts\\python.exe", 4 | "search.exclude": { 5 | "**/node_modules": false, 6 | "**/bower_components": false 7 | }, 8 | "python.linting.pylintEnabled": true, 9 | "python.linting.enabled": true, 10 | "python.linting.lintOnSave": true, 11 | "python.formatting.provider": "autopep8", 12 | "python.linting.pylintArgs": [ 13 | "--load-plugins", 14 | "pylint_django" 15 | ], 16 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Run Application", 8 | "type": "shell", 9 | "command": "python", 10 | "args": [ 11 | "run.py" 12 | ] 13 | }, 14 | { 15 | "label": "Run Server", 16 | "type": "shell", 17 | "command": "python", 18 | "args": [ 19 | "run.py", 20 | "--server" 21 | ] 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django/AngularJS/Electron Application 2 | 3 | ### Setup 4 | 5 | Prior to developing, you may need to run `setup.py`. 6 | 7 | This requires: 8 | * Python 3.4 9 | * Node.js (for `npm`) 10 | 11 | The setup script installs: 12 | * `virtualenv` 13 | * `bower` 14 | * `lessc` 15 | 16 | For the server, this will: 17 | * initialize a virtual environment 18 | * update the environment PIP to the latest version, 19 | * perform Django migrations 20 | * create a Django super user 21 | 22 | For the client, this will: 23 | * initialize NPM and `node_modules/`. 24 | * initialize Bower and `bower_components/`. 25 | 26 | ### Running 27 | 28 | Once the development environment is setup, the application can be started by running `run.py`. This should start a Django server and an Electron app. The Django server can be reached in a browser at the address `http://localhost:8080/`. 29 | 30 | ### Building 31 | 32 | The build process currently involves: 33 | 34 | * Renders distinct HTML index files for the Web and Electron applications. 35 | * Creating an `imports.less` file for client style definitions. 36 | * Compiling the `site.less` into a final `site.css` file. 37 | 38 | If `--electron` is specified, the Electron client binary package is built instead. 39 | 40 | ### Packaging the Electron Client Application 41 | 42 | The `build.py` script can be invoked with the `--electron` option to build the Electron client application binary package. This builds a binary package named by the `client/package.json` metadata file. 43 | 44 | ``` 45 | python build.py --electron 46 | ``` 47 | 48 | The packaging of an Electron app into a binary (such as `.exe` on Windows) can be done by using the `electron-packager` tool. This is installed by `setup.py` with the command: 49 | 50 | ``` 51 | npm install electron-packager -g 52 | ``` 53 | 54 | When used, this tool creates a directory such as `django-angular-electron-win32-x64/` in the current working directory. This directory would contain a similarly named `django-angular-electron.exe` executable. The tool can be used as: 55 | 56 | ``` 57 | electron-packager .\client "django-angular-electron" --platform=win32 --arch=x64 58 | ``` 59 | 60 | * The `--platform` option can be one or more of `darwin`, `linux`, `mas`, `win32` (comma-delimited if multiple). It defaults to the host platform. 61 | * The `--arch` option can be one or more of `ia32`, `x64`, `armv7l`, `arm64`, `mips64el` (comma-delimited if multiple). Defaults to the host architecture. 62 | -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from jinja2 import Template 3 | import os 4 | import logging 5 | import argparse 6 | import json 7 | 8 | from shared import * 9 | 10 | 11 | # The root directory of the client (directory of this file). 12 | CLIENT_DIR = os.path.join(ROOT_DIR, 'client') 13 | 14 | # Directory with important LESS files. 15 | LESS_DIR = os.path.join(CLIENT_DIR, 'static', 'styles') 16 | LESS_IMPORTS_PATH = os.path.join(LESS_DIR, 'imports.less') 17 | 18 | # Directories to search for LESS imports. 19 | LESS_IMPORT_DIRS = [ 20 | os.path.join(CLIENT_DIR, 'static', 'states'), 21 | os.path.join(CLIENT_DIR, 'static', 'widgets') 22 | ] 23 | 24 | # HTML index files are generated from a Jinja2 template. 25 | TEMPLATE_INDEX_PATH = os.path.join(CLIENT_DIR, 'index.html.in') 26 | WEB_INDEX_PATH = os.path.join(CLIENT_DIR, 'index.html') 27 | ELECTRON_INDEX_PATH = os.path.join(CLIENT_DIR, 'index.electron.html') 28 | 29 | 30 | """ 31 | This script should build the entire project. 32 | 33 | Client application build steps: 34 | 1. Render index templates with Jinja2 (for web and Electron). 35 | 2. Create {imports.less} file with client LESS imports. 36 | 3. Compiles {site.less} into final {site.css} file. 37 | 38 | Server application build steps: 39 | None (yet). 40 | """ 41 | 42 | 43 | def build_electron_app(application_name): 44 | """Builds the Electron client application. 45 | Raises: 46 | RuntimeError: The `electron-packager` command failed. 47 | """ 48 | set_directory('') # Go to the root directory. 49 | run_command('electron-packager .\client "%s"' % application_name) 50 | 51 | 52 | def render_html_templates(): 53 | """ 54 | Renders HTML index template with Jinja2. 55 | The {index.html.in} file is rendered, generating two files: 56 | * {index.html} 57 | * {index.electron.html} 58 | """ 59 | # Delete previous output files if they exist. 60 | if os.path.exists(WEB_INDEX_PATH): 61 | os.remove(WEB_INDEX_PATH) 62 | if os.path.exists(ELECTRON_INDEX_PATH): 63 | os.remove(ELECTRON_INDEX_PATH) 64 | 65 | with open(TEMPLATE_INDEX_PATH) as template_file: 66 | contents = template_file.read() 67 | 68 | # Render web based HTML index. 69 | rendered = Template(contents).render(static='/static', base='/') 70 | with open(WEB_INDEX_PATH, 'w+') as index_file: 71 | index_file.write(rendered) 72 | logging.info('Created %s.' % WEB_INDEX_PATH) 73 | 74 | # Render Electron based HTML index. 75 | rendered = Template(contents).render( 76 | static='static', base=os.path.join(CLIENT_DIR, '')) 77 | with open(ELECTRON_INDEX_PATH, 'w+') as index_elec_file: 78 | index_elec_file.write(rendered) 79 | logging.info('Created %s.' % ELECTRON_INDEX_PATH) 80 | 81 | 82 | def find_less_imports(): 83 | """ 84 | Search {LESS_IMPORT_DIRS} for LESS dependencies. 85 | Returns: 86 | List of LESS file paths that were found. 87 | """ 88 | imports = [] 89 | for importdir in LESS_IMPORT_DIRS: 90 | for (folder, subfolders, files) in os.walk(importdir): 91 | for file in files: 92 | name, ext = os.path.splitext(file) 93 | if ext == '.less': 94 | imports.append(os.path.join(folder, file)) 95 | return imports 96 | 97 | 98 | def create_imports_less(): 99 | """ 100 | Creates final {imports.less} file. 101 | """ 102 | imports = find_less_imports() 103 | if os.path.exists(LESS_IMPORTS_PATH): 104 | os.remove(LESS_IMPORTS_PATH) 105 | with open(LESS_IMPORTS_PATH, 'w+') as imports_file: 106 | for less in imports: 107 | less_path = less.replace( 108 | os.path.join(CLIENT_DIR, 'static'), '..') 109 | less_path = less_path.replace("\\", "/") 110 | imports_file.write("@import \"%s\";\n" % less_path) 111 | logging.info('Created %s.' % LESS_IMPORTS_PATH) 112 | 113 | 114 | def compile_less_styles(): 115 | """ 116 | Compile LESS styles into single CSS file. 117 | Raises: 118 | RuntimeError - The {lessc} command failed. 119 | """ 120 | less_path = os.path.join(LESS_DIR, 'site.less') 121 | css_path = os.path.join(LESS_DIR, 'site.css') 122 | run_command('lessc %s %s' % (less_path, css_path)) 123 | 124 | 125 | def build(electron_app=False): 126 | """ 127 | Build the project. 128 | """ 129 | if electron_app: 130 | # This takes some time, be careful how often we build. 131 | package_json_path = os.path.join(ROOT_DIR, 'client', 'package.json') 132 | with open(package_json_path) as package_json: 133 | metadata = json.load(package_json) 134 | build_electron_app(metadata['name']) 135 | else: 136 | render_html_templates() 137 | create_imports_less() 138 | compile_less_styles() 139 | 140 | 141 | def clean(): 142 | """ 143 | Cleans build files from the project. 144 | """ 145 | # These are created by `render_html_templates`. 146 | if os.path.isfile(ELECTRON_INDEX_PATH): 147 | os.remove(ELECTRON_INDEX_PATH) 148 | logging.info('Deleted %s.' % ELECTRON_INDEX_PATH) 149 | if os.path.isfile(WEB_INDEX_PATH): 150 | os.remove(WEB_INDEX_PATH) 151 | logging.info('Deleted %s.' % WEB_INDEX_PATH) 152 | # This is created by `create_imports_less`. 153 | if os.path.isfile(LESS_IMPORTS_PATH): 154 | os.remove(LESS_IMPORTS_PATH) 155 | logging.info('Deleted %s.' % LESS_IMPORTS_PATH) 156 | 157 | 158 | if __name__ == '__main__': 159 | logging.basicConfig( 160 | level=logging.INFO, 161 | format='%(levelname)s:%(message)s' 162 | ) 163 | parser = argparse.ArgumentParser(description='Builds the project.') 164 | parser.add_argument('--clean', action='store_true') 165 | parser.add_argument('--electron', action='store_true') 166 | args = parser.parse_args() 167 | if args.clean: 168 | clean() 169 | else: 170 | build(electron_app=args.electron) 171 | -------------------------------------------------------------------------------- /client/index.html.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | App Title 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | var electron = require('electron') 2 | var app = electron.app; 3 | var BrowserWindow = electron.BrowserWindow; 4 | var window = null; 5 | 6 | app.on('window-all-closed', function() { 7 | if (process.platform != 'darwin') 8 | app.quit(); 9 | }); 10 | 11 | app.on('ready', function() { 12 | window = new BrowserWindow({width: 800, height: 600}); 13 | window.loadURL('file://' + __dirname + '/index.electron.html'); 14 | window.on('closed', function() { 15 | window = null; 16 | }); 17 | }); -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-application", 3 | "version": "0.1.0", 4 | "main": "main.js", 5 | "scripts": { 6 | "start": "electron .", 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "dependencies": { 10 | "electron": "^4.0.0", 11 | "jquery": "^3.2.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/static/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "static", 3 | "authors": [ 4 | "Steve " 5 | ], 6 | "description": "", 7 | "main": "", 8 | "license": "MIT", 9 | "homepage": "", 10 | "private": true, 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ], 18 | "dependencies": { 19 | "angular": "~1.6.7", 20 | "angular-resource": "~1.6.7", 21 | "angular-cookies": "~1.6.7", 22 | "angular-ui-router": "~1.0.15", 23 | "jquery": "^3.2.1", 24 | "moment": "^2.19.2", 25 | "font-awesome": "~5.0.8", 26 | "bootstrap": "~4.0.0" 27 | }, 28 | "resolutions": { 29 | "angular": "~1.6.7", 30 | "angular-resource": "~1.6.7", 31 | "angular-cookies": "~1.6.7", 32 | "bootstrap": "~4.0.0", 33 | "angular-ui-router": "~1.0.15", 34 | "font-awesome": "~5.0.8" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client/static/config.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | angular 5 | .module("app", [ 6 | "ui.router", 7 | "ngResource", 8 | "ngCookies", 9 | "djoser" 10 | ]) 11 | .config(Config) 12 | .run(Run); 13 | 14 | Config.$inject = ["$httpProvider", "$resourceProvider", "$locationProvider"]; 15 | Run.$inject = ["$rootScope", "$state", "$location", "$transitions", "$q", "$djoser"]; 16 | 17 | function Config ($httpProvider, $resourceProvider, $locationProvider) { 18 | $httpProvider.defaults.xsrfCookieName = "csrftoken"; 19 | $httpProvider.defaults.xsrfHeaderName = "X-CSRFToken"; 20 | $resourceProvider.defaults.stripTrailingSlashes = false; 21 | $locationProvider.html5Mode({ enabled: true, requireBase: false }); 22 | } 23 | 24 | function Run ($rootScope, $state, $location, $transitions, $q, $djoser) { 25 | // These functions can be used by any AngularJS templates. 26 | $rootScope.moment = moment; 27 | $rootScope.link = $state.go; 28 | $rootScope.isElectron = isElectron; // Returns true if Electron is running. 29 | $rootScope.formatDate = function (str) { return moment(str).format('LL'); }; 30 | $rootScope.formatDateTime = function (str) { return moment(str).format('LLL'); }; 31 | 32 | var initialLoading = $q.defer(); 33 | $rootScope.user = null; 34 | // Load current session (if it exists). 35 | $djoser.current().$promise.then( 36 | function (data) { 37 | $rootScope.user = data; 38 | initialLoading.resolve(); 39 | }, 40 | function (error) { 41 | initialLoading.resolve(); 42 | } 43 | ); 44 | 45 | $transitions.onBefore({ to: "**" }, function ($state) { 46 | return $q(function (resolve, reject) { 47 | // Wait for the initial load, only needs to be done once. 48 | initialLoading.promise.then(function() { 49 | if ($state.to().protected && !$rootScope.user) 50 | // Prevent protected states without registered user. 51 | resolve($state.router.stateService.target("root.login")); 52 | else if ($rootScope.user && $state.to().registration) 53 | // Prevent registration states when registered. 54 | resolve($state.router.stateService.target("root.dashboard")); 55 | else 56 | // Proceed normally. 57 | resolve(); 58 | }); 59 | }); 60 | }); 61 | } 62 | 63 | })(); -------------------------------------------------------------------------------- /client/static/electron.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | if (typeof require === 'function') 4 | var remote = require('electron').remote; 5 | 6 | function registerCloseControls(className) { 7 | // Registers the Window close control events. 8 | if (remote) { 9 | let elements = document.getElementsByClassName(className); 10 | for (let i = 0; i < elements.length; i++) 11 | elements[i].addEventListener('click', function (event) { 12 | var window = remote.getCurrentWindow(); 13 | window.close(); 14 | }); 15 | } 16 | } 17 | 18 | function isElectron() { 19 | if (typeof require !== 'function') return false; 20 | if (typeof window !== 'object') return false; 21 | try { 22 | const electron = require('electron'); 23 | if (typeof electron !== 'object') return false; 24 | } catch (e) { 25 | return false; 26 | } 27 | return true; 28 | } 29 | -------------------------------------------------------------------------------- /client/static/images/battlenet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steve2/django-angular-electron/8aab612393cf5a6807a74d1d32a801be637a58ca/client/static/images/battlenet.png -------------------------------------------------------------------------------- /client/static/images/brushed_alu_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steve2/django-angular-electron/8aab612393cf5a6807a74d1d32a801be637a58ca/client/static/images/brushed_alu_dark.png -------------------------------------------------------------------------------- /client/static/images/dark_embroidery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steve2/django-angular-electron/8aab612393cf5a6807a74d1d32a801be637a58ca/client/static/images/dark_embroidery.png -------------------------------------------------------------------------------- /client/static/images/dark_fish_skin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steve2/django-angular-electron/8aab612393cf5a6807a74d1d32a801be637a58ca/client/static/images/dark_fish_skin.png -------------------------------------------------------------------------------- /client/static/images/dark_mosaic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steve2/django-angular-electron/8aab612393cf5a6807a74d1d32a801be637a58ca/client/static/images/dark_mosaic.png -------------------------------------------------------------------------------- /client/static/images/dark_wood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steve2/django-angular-electron/8aab612393cf5a6807a74d1d32a801be637a58ca/client/static/images/dark_wood.png -------------------------------------------------------------------------------- /client/static/images/footer_lodyas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steve2/django-angular-electron/8aab612393cf5a6807a74d1d32a801be637a58ca/client/static/images/footer_lodyas.png -------------------------------------------------------------------------------- /client/static/images/hypnotize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steve2/django-angular-electron/8aab612393cf5a6807a74d1d32a801be637a58ca/client/static/images/hypnotize.png -------------------------------------------------------------------------------- /client/static/images/inflicted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steve2/django-angular-electron/8aab612393cf5a6807a74d1d32a801be637a58ca/client/static/images/inflicted.png -------------------------------------------------------------------------------- /client/static/images/px_by_Gre3g.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steve2/django-angular-electron/8aab612393cf5a6807a74d1d32a801be637a58ca/client/static/images/px_by_Gre3g.png -------------------------------------------------------------------------------- /client/static/images/rubber_grip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steve2/django-angular-electron/8aab612393cf5a6807a74d1d32a801be637a58ca/client/static/images/rubber_grip.png -------------------------------------------------------------------------------- /client/static/images/squares.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steve2/django-angular-electron/8aab612393cf5a6807a74d1d32a801be637a58ca/client/static/images/squares.png -------------------------------------------------------------------------------- /client/static/modules/djoser/djoser.main.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | angular 5 | .module("djoser", []) 6 | .config(Setup) 7 | .run(Initialization) 8 | .service('djoser.interceptor', Interceptor) 9 | .value('settings', { 10 | token: null, 11 | tokenKey: 'auth_token' 12 | }); 13 | 14 | Initialization.$inject = ['$cookies', 'settings']; 15 | function Initialization ($cookies, settings) { 16 | settings.token = $cookies.get(settings.tokenKey); 17 | } 18 | 19 | Setup.$inject = ['$httpProvider']; 20 | function Setup ($httpProvider) { 21 | $httpProvider.interceptors.push('djoser.interceptor'); 22 | } 23 | 24 | Interceptor.$inject = ['$cookies', 'settings']; 25 | function Interceptor ($cookies, settings) { 26 | var service = this; 27 | service.request = function (config) { 28 | if (settings.token !== undefined && settings.token != null) 29 | // Only append the header if the token is available. 30 | config.headers['Authorization'] = 'Token ' + settings.token; 31 | return config; 32 | }; 33 | } 34 | 35 | })(); -------------------------------------------------------------------------------- /client/static/modules/djoser/djoser.service.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | angular 5 | .module("djoser") 6 | .factory('$djoser', djoser); 7 | 8 | djoser.$inject = ['$rootScope', '$resource', '$cookies', '$q', 'settings']; 9 | 10 | function djoser ($rootScope, $resource, $cookies, $q, settings) { 11 | 12 | let endpoints = { 13 | login: $resource(_get_url("/api/auth/token/create/"), {}, {}), 14 | logout: $resource(_get_url("/api/auth/token/destroy/"), {}, {}), 15 | current: $resource(_get_url("/api/auth/me/"), {}, {}), 16 | create: $resource(_get_url("/api/auth/users/create/"), {}, {}), 17 | delete: $resource(_get_url("/api/auth/users/delete/"), {}, {}), 18 | activate: $resource(_get_url("/api/auth/users/activate/"), {}, {}), 19 | changePassword: $resource(_get_url("/api/auth/password/"), {}, {}) 20 | }; 21 | 22 | return { 23 | // User management. 24 | current: endpoints.current.get, 25 | register: _register, 26 | delete: endpoints.delete.save, 27 | activate: endpoints.activate.save, 28 | changePassword: endpoints.changePassword.save, 29 | 30 | // Token authentication. 31 | login: _login, 32 | logout: _logout 33 | }; 34 | 35 | // --------------------------------------------------------------------- 36 | 37 | function _get_url(uri) { 38 | if (isElectron()) 39 | return 'http://localhost:8080' + uri; 40 | else 41 | return uri; 42 | } 43 | 44 | function _login(username, password) { 45 | var response = endpoints.login.save( 46 | { username: username, password: password }); 47 | var deferred = $q.defer(); 48 | response.$promise.then( 49 | function (data) { 50 | $cookies.put(settings.tokenKey, data.auth_token); 51 | settings.token = data.auth_token; 52 | endpoints.current.get().$promise.then( 53 | function (data) { 54 | $rootScope.user = data; 55 | deferred.resolve(data); 56 | }, 57 | function (error) { 58 | deferred.reject(error); 59 | }); 60 | }, 61 | function (error) { 62 | deferred.reject(error); 63 | }); 64 | deferred.$promise = deferred.promise; 65 | return deferred; 66 | } 67 | 68 | function _logout() { 69 | var response = endpoints.logout.save(); 70 | response.$promise.then( 71 | function (data) { 72 | $cookies.remove(settings.tokenKey); 73 | settings.token = null; 74 | $rootScope.user = null; 75 | }, 76 | function (error) {}); 77 | return response; 78 | } 79 | 80 | function _register(username, email, password) { 81 | var response = endpoints.create.save( 82 | { username: username, email: email, password: password }); 83 | var deferred = $q.defer(); 84 | response.$promise.then( 85 | function (data) { 86 | deferred.resolve(data); 87 | }, 88 | function (error ){ 89 | deferred.reject(error); 90 | }); 91 | deferred.$promise = deferred.promise; 92 | return deferred; 93 | } 94 | } 95 | 96 | })(); -------------------------------------------------------------------------------- /client/static/router.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | angular 5 | .module("app") 6 | .config(Router); 7 | 8 | Router.$inject = ["$stateProvider", "$urlRouterProvider", "$locationProvider"]; 9 | 10 | // There are currently three types of states. 11 | // 1) States that logged in users shouldn't see (Login, Signup, etc.). 12 | // 2) States that only logged in users should see (Profile, etc.). 13 | // 3) States that are neither of the above. 14 | // 15 | // These are categorized by additional properties added to state objects. 16 | // "registration": Corresponds with situation (1) above. 17 | // "protected": Corresponds with situation (2) above. 18 | // : Corresponds with situation (3) above. 19 | // 20 | 21 | function Router ($stateProvider, $urlRouterProvider, $locationProvider) { 22 | 23 | // Default state. 24 | $urlRouterProvider.otherwise("/dashboard/"); 25 | 26 | // State definitions. 27 | $stateProvider 28 | .state("root", { 29 | abstract: true, 30 | templateUrl: "static/states/root.html", 31 | controller: "RootController as root" 32 | }) 33 | .state("root.signup", { 34 | url: "/signup/", 35 | templateUrl: "static/states/signup/signup.html", 36 | controller: "SignupController as signup", 37 | registration: true 38 | }) 39 | .state("root.login", { 40 | url: "/login/", 41 | templateUrl: "static/states/login/login.html", 42 | controller: "LoginController as login", 43 | registration: true 44 | }) 45 | .state("root.dashboard", { 46 | url: "/dashboard/", 47 | templateUrl: "static/states/dashboard/dashboard.html", 48 | controller: "DashboardController as dashboard", 49 | protected: true 50 | }); 51 | } 52 | 53 | })(); -------------------------------------------------------------------------------- /client/static/services/User.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | angular 5 | .module("app") 6 | .factory("User", UserService); 7 | 8 | UserService.$inject = ['$rootScope', '$q']; 9 | function UserService ($rootScope, $q) { 10 | 11 | return { 12 | socialAccount: _find_social_account, 13 | }; 14 | 15 | function _find_social_account(userObject, provider) { 16 | if (!('socialaccount_set' in userObject)) 17 | return null; 18 | for (let index in userObject['socialaccount_set']) 19 | if (userObject['socialaccount_set'][index].provider == provider) 20 | return userObject['socialaccount_set'][index]; 21 | return null; 22 | } 23 | 24 | } 25 | 26 | })(); -------------------------------------------------------------------------------- /client/static/states/Login/Login.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |
8 |
9 |
-------------------------------------------------------------------------------- /client/static/states/Login/Login.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | angular 5 | .module("app") 6 | .controller("LoginController", LoginController); 7 | 8 | LoginController.$inject = ["$timeout", "$element", "$state", "$stateParams", "$djoser"]; 9 | 10 | function LoginController ($timeout, $element, $state, $stateParams, $djoser) { 11 | var ct = this; 12 | 13 | 14 | 15 | } 16 | 17 | })(); -------------------------------------------------------------------------------- /client/static/states/Login/Login.less: -------------------------------------------------------------------------------- 1 | 2 | #login-root { 3 | .body { 4 | .canvas { 5 | center { 6 | vertical-align: middle; 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /client/static/states/Login/login-form/login-form.html: -------------------------------------------------------------------------------- 1 |
2 |
Logging in...
3 | 4 |
5 | 6 |
7 |

Login

8 |
9 |
10 |
11 | {{ message }} 12 |
13 |
14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 |
25 |
26 | 29 |
30 | Don't have credentials? Click here to register. 31 |
32 |
33 |
34 |
-------------------------------------------------------------------------------- /client/static/states/Login/login-form/login-form.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | angular 5 | .module("app") 6 | .directive("loginForm", LoginForm); 7 | 8 | LoginForm.$inject = []; 9 | function LoginForm () { 10 | return { 11 | restrict: 'E', 12 | scope: {}, 13 | controller: LoginFormController, 14 | templateUrl: "static/states/login/login-form/login-form.html" 15 | }; 16 | } 17 | 18 | LoginFormController.$inject = ['$scope', '$state', '$stateParams', '$djoser']; 19 | function LoginFormController ($scope, $state, $stateParams, $djoser) { 20 | var ct = $scope; 21 | 22 | ct.loading = false; 23 | ct.error = null; 24 | ct.login = _login; 25 | ct.twitchLogin = _twitchLogin; 26 | 27 | function _login(username, password) { 28 | ct.loading = true; 29 | $djoser.login(username, password).$promise.then( 30 | function (data) { 31 | $state.transitionTo('root.dashboard', $stateParams, { reload: true }); 32 | }, 33 | function (error) { 34 | ct.loading = false; 35 | ct.error = error.data; 36 | } 37 | ); 38 | } 39 | 40 | function _twitchLogin() { 41 | ct.loading = true; 42 | // location.assign("http://localhost:8080/accounts/twitch/login/?next=/"); 43 | } 44 | 45 | 46 | } 47 | 48 | })(); -------------------------------------------------------------------------------- /client/static/states/Login/login-form/login-form.less: -------------------------------------------------------------------------------- 1 | 2 | @input-text-size: 18pt; 3 | @input-bottom-space: 15px; 4 | @input-width: 400px; 5 | 6 | login-form { 7 | .wrapper { 8 | input { 9 | text-align: center; 10 | max-width: @input-width; 11 | margin-bottom: @input-bottom-space; 12 | font-size: @input-text-size; 13 | } 14 | .control { 15 | input.login { 16 | font-size: (@input-text-size - 2pt); 17 | padding: 15px 30px; 18 | } 19 | } 20 | .third-party-login { 21 | display: inline-block; 22 | min-width: 300px; 23 | background: @control-background; 24 | border-radius: 50px; 25 | padding: 10px; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /client/static/states/Root.less: -------------------------------------------------------------------------------- 1 | 2 | #root { 3 | height: 100%; 4 | & > div { 5 | height: 100%; 6 | & > div { 7 | height: 100%; 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /client/static/states/Signup/Signup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |
8 |
9 |
-------------------------------------------------------------------------------- /client/static/states/Signup/Signup.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | angular 5 | .module("app") 6 | .controller("SignupController", SignupController); 7 | 8 | SignupController.$inject = ["$timeout", "$element"]; 9 | 10 | function SignupController ($timeout, $element) { 11 | var ct = this; 12 | 13 | 14 | } 15 | 16 | })(); -------------------------------------------------------------------------------- /client/static/states/Signup/Signup.less: -------------------------------------------------------------------------------- 1 | 2 | #signup-root { 3 | .body { 4 | .canvas { 5 | center { 6 | vertical-align: middle; 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /client/static/states/Signup/signup-form/signup-form.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
Creating user...
4 | 5 |
6 | 7 |
8 | 9 |

Signup

10 |
11 | 12 |
13 |
14 | {{ message }} 15 |
16 |
17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 30 |
31 | Already have credentials? Click here to login. 32 |
33 |
34 |
35 |
-------------------------------------------------------------------------------- /client/static/states/Signup/signup-form/signup-form.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | angular 5 | .module("app") 6 | .directive("signupForm", SignupForm); 7 | 8 | SignupForm.$inject = []; 9 | function SignupForm () { 10 | return { 11 | restrict: 'E', 12 | scope: {}, 13 | controller: SignupFormController, 14 | templateUrl: "static/states/signup/signup-form/signup-form.html" 15 | }; 16 | } 17 | 18 | SignupFormController.$inject = ['$scope', '$state', '$stateParams', '$djoser']; 19 | function SignupFormController ($scope, $state, $stateParams, $djoser) { 20 | var ct = $scope; 21 | 22 | ct.loading = false; 23 | ct.signup = _signup; 24 | 25 | _reset_errors(); 26 | 27 | 28 | function _reset_errors () { 29 | ct.errors = { 30 | email: null, 31 | username: null, 32 | password: null, 33 | password2: null 34 | }; 35 | } 36 | 37 | function _login (username, password) { 38 | _reset_errors(); 39 | $djoser.login(username, password).$promise.then( 40 | function (data) { 41 | $state.transitionTo('root.dashboard', $stateParams, { reload: true }); 42 | }, 43 | function (error) { 44 | ct.loading = false; 45 | ct.error = error.data; 46 | } 47 | ) 48 | } 49 | 50 | function _signup (username, email, password, confirmPassword) { 51 | _reset_errors(); 52 | ct.loading = true; 53 | 54 | if (password != confirmPassword) { 55 | ct.errors.password2 = "This does not match your password."; 56 | ct.loading = false; 57 | return; 58 | } 59 | 60 | $djoser.register(username, email, password).$promise.then( 61 | function (data) { 62 | _login(username, password); 63 | }, 64 | function (error) { 65 | ct.loading = false; 66 | if (error.data.username != null) 67 | ct.errors.username = error.data.username[0]; 68 | if (error.data.password != null) 69 | ct.errors.password = error.data.password[0]; 70 | if (error.data.email != null) 71 | ct.errors.email = error.data.email[0]; 72 | } 73 | ); 74 | } 75 | } 76 | 77 | })(); -------------------------------------------------------------------------------- /client/static/states/Signup/signup-form/signup-form.less: -------------------------------------------------------------------------------- 1 | @input-text-size: 18pt; 2 | @input-bottom-space: 15px; 3 | @input-width: 400px; 4 | 5 | signup-form { 6 | .wrapper { 7 | input { 8 | text-align: center; 9 | max-width: @input-width; 10 | margin-bottom: @input-bottom-space; 11 | font-size: @input-text-size; 12 | } 13 | } 14 | .control { 15 | button.signup { 16 | font-size: (@input-text-size - 2pt); 17 | padding: 15px 30px; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /client/static/states/dashboard/dashboard.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | 12 |
13 |
14 |
15 |
16 |
17 |
18 |

{{ user.username }} 19 | 20 | 21 | (you) 22 | 23 | 24 |

25 |
26 | Joined {{ dashboard.timeFromNow(user.date_joined) }} 27 | on {{ dashboard.formatDate(user.date_joined) }} 28 |
29 |
30 | 33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
Logging out...
46 | 47 |
48 |
49 |
50 |
51 | -------------------------------------------------------------------------------- /client/static/states/dashboard/dashboard.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | angular 5 | .module("app") 6 | .controller("DashboardController", DashboardController); 7 | 8 | DashboardController.$inject = ["$timeout", "$http", "$state", "$stateParams", "$djoser"]; 9 | function DashboardController ($timeout, $http, $state, $stateParams, $djoser) { 10 | var ct = this; 11 | 12 | ct.loading = null; 13 | ct.logout = _logout; 14 | 15 | ct.timeFromNow = function (date) { 16 | return moment(date).fromNow(); 17 | }; 18 | 19 | ct.formatDate = function (date) { 20 | return moment(date).format('LL'); 21 | }; 22 | 23 | function _logout() { 24 | ct.loading = {}; 25 | ct.loading.logout = true; 26 | $djoser.logout().$promise.then( 27 | function (data) { 28 | $state.transitionTo($state.current, $stateParams, { reload: true }); 29 | }, 30 | function (error) { 31 | ct.loading.logout = false; 32 | } 33 | ); 34 | } 35 | } 36 | 37 | })(); -------------------------------------------------------------------------------- /client/static/states/dashboard/dashboard.less: -------------------------------------------------------------------------------- 1 | 2 | @top-bg-stripe-1: #222; 3 | @top-bg-stripe-2: #252525; 4 | 5 | #dashboard-root { 6 | .top { 7 | .bg-striped(@top-bg-stripe-1, @top-bg-stripe-2); 8 | .avatar { 9 | .fa-user-circle { 10 | font-size: 101pt; 11 | text-shadow: 0 5px 15px black; 12 | } 13 | .frame { 14 | .td { 15 | vertical-align: middle; 16 | } 17 | } 18 | } 19 | } 20 | .information { 21 | h6 { 22 | & > small { 23 | color: darken(@text-color, 50%); 24 | } 25 | } 26 | } 27 | .you { 28 | font-size: 20pt; 29 | color: darken(@text-color, 40%); 30 | .relative { 31 | left: 5px; 32 | top: 10px; 33 | } 34 | } 35 | .loading.table { 36 | position: absolute; 37 | .loading.tr { 38 | .loading.td { 39 | vertical-align: middle; 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /client/static/states/root.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
-------------------------------------------------------------------------------- /client/static/states/root.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | angular 5 | .module("app") 6 | .controller("RootController", RootController); 7 | 8 | RootController.$inject = ["$timeout", "$element"]; 9 | 10 | function RootController ($timeout, $element) { 11 | var ct = this; 12 | 13 | } 14 | 15 | })(); -------------------------------------------------------------------------------- /client/static/styles/bootstrap.less: -------------------------------------------------------------------------------- 1 | // Custom theme for Bootstrap. 2 | // Should contain all necessary overrides for Bootstrap styles. 3 | 4 | .btn { 5 | &:hover { 6 | &:not(:disabled) { 7 | border: 1px solid rgba(255, 255, 255, 0.4); 8 | } 9 | } 10 | } 11 | 12 | .form-control { 13 | color: @text-color; 14 | background: @control-background; 15 | border: 1px solid @border-primary-color; 16 | height: auto; 17 | &:focus { 18 | color: @text-color; 19 | background: darken(@control-background, 3%); 20 | } 21 | } 22 | 23 | .col, .col-1, .col-10, .col-11, .col-12, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, 24 | .col-8, .col-9, .col-auto, .col-lg, .col-lg-1, .col-lg-10, .col-lg-11, .col-lg-12, 25 | .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-auto, 26 | .col-md, .col-md-1, .col-md-10, .col-md-11, .col-md-12, .col-md-2, .col-md-3, .col-md-4, .col-md-5, 27 | .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-auto, .col-sm, .col-sm-1, .col-sm-10, .col-sm-11, 28 | .col-sm-12, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, 29 | .col-sm-auto, .col-xl, .col-xl-1, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl-2, .col-xl-3, .col-xl-4, 30 | .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-auto { 31 | padding-right: 0px; 32 | padding-left: 0px; 33 | } 34 | 35 | .dropdown-menu { 36 | color: @text-color; 37 | background: darken(@background-color, 2%); 38 | border: 1px solid @border-primary-color; 39 | a { 40 | color: @text-color; 41 | } 42 | .dropdown-item { 43 | &:hover { 44 | background: #3e424d; 45 | cursor: pointer; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /client/static/styles/imports.less: -------------------------------------------------------------------------------- 1 | @import "../states/Root.less"; 2 | @import "../states/dashboard/dashboard.less"; 3 | @import "../states/login/Login.less"; 4 | @import "../states/login/login-form/login-form.less"; 5 | @import "../states/signup/Signup.less"; 6 | @import "../states/signup/signup-form/signup-form.less"; 7 | @import "../widgets/input-v/input-v.less"; 8 | -------------------------------------------------------------------------------- /client/static/styles/site.less: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Anton|Passion+One|Mina'); 2 | 3 | @font-family: "Mina"; 4 | @font-family-default: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; 5 | 6 | @text-color: #eee; 7 | @background: url("../images/dark_mosaic.png"); 8 | @background-color: #2f323b; 9 | @control-background: darken(@background-color, 5%); 10 | 11 | @active-color: #248ce7; 12 | @focus-color: #80bdff; 13 | 14 | @border-primary-color: #556; 15 | 16 | html, 17 | body { 18 | height: 100%; 19 | margin: 0; 20 | color: @text-color; 21 | // background: @background; 22 | background-color: @background-color; 23 | font-family: @font-family-default; 24 | 25 | & > div { 26 | height: 100%; 27 | } 28 | } 29 | 30 | h1, h2, h3, h4, h5, h6 { 31 | font-weight: 100; 32 | font-family: @font-family-default; 33 | } 34 | 35 | button { 36 | &:hover:not(:disabled) { 37 | cursor: pointer; 38 | } 39 | &:active, &:focus { 40 | outline: none !important; 41 | box-shadow: none !important; 42 | } 43 | } 44 | 45 | hr { 46 | border-top: 1px solid @border-primary-color; 47 | } 48 | 49 | input, 50 | button { 51 | color: @text-color; 52 | background: @control-background; 53 | border-color: @border-primary-color; 54 | &:focus { 55 | border-color: @focus-color; 56 | } 57 | } 58 | 59 | .table { 60 | display: table; 61 | width: 100%; 62 | height: 100%; 63 | margin-bottom: 0; 64 | & > .tr { 65 | display: table-row; 66 | & > .td { 67 | display: table-cell; 68 | } 69 | } 70 | } 71 | 72 | /* Striped background LESS function. */ 73 | .bg-striped(@color1, @color2) { 74 | background: repeating-linear-gradient( 75 | -55deg, 76 | @color1, 77 | @color1 10px, 78 | @color2 10px, 79 | @color2 20px 80 | ); 81 | } 82 | 83 | /* Common CSS Wrappers */ 84 | .relative { 85 | position: relative; 86 | } 87 | .absolute { 88 | position: absolute; 89 | } 90 | .smallcaps { 91 | font-variant: small-caps; 92 | } 93 | .uppercase { 94 | text-transform: uppercase; 95 | } 96 | .capitalize { 97 | text-transform: capitalize; 98 | } 99 | .lowercase { 100 | text-transform: lowercase; 101 | } 102 | 103 | @import "bootstrap.less"; 104 | @import "imports.less"; -------------------------------------------------------------------------------- /client/static/widgets/input-v/input-v.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 8 | 9 |
-------------------------------------------------------------------------------- /client/static/widgets/input-v/input-v.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | angular 5 | .module("app") 6 | .directive("inputV", InputValidated); 7 | 8 | InputValidated.$inject = []; 9 | function InputValidated () { 10 | return { 11 | restrict: 'E', 12 | scope: { 13 | 'text': '@', 14 | 'type': '@', 15 | 'error': '=', 16 | 'model': '=' 17 | }, 18 | controller: InputValidatedController, 19 | templateUrl: "static/widgets/input-v/input-v.html" 20 | }; 21 | } 22 | 23 | InputValidatedController.$inject = ['$scope']; 24 | function InputValidatedController ($scope) { 25 | var ct = $scope; 26 | 27 | } 28 | 29 | })(); -------------------------------------------------------------------------------- /client/static/widgets/input-v/input-v.less: -------------------------------------------------------------------------------- 1 | 2 | input-v { 3 | .text-danger { 4 | font-size: 8pt; 5 | } 6 | } -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from threading import Thread 3 | from build import build 4 | import os 5 | import argparse 6 | import logging 7 | 8 | from shared import * 9 | 10 | 11 | # The port the Django server listens on. 12 | SERVER_PORT = 8080 13 | 14 | 15 | """ 16 | Starts a Django server (on {SERVER_PORT}) and the Electron client. 17 | 18 | The web client can be viewed with a browser at `http://localhost:{SERVER_PORT}`. 19 | """ 20 | 21 | 22 | def run_server(): 23 | """ 24 | Starts the Django server. 25 | Raises: 26 | RuntimeError - Django server failed to start (see log). 27 | """ 28 | python_path = os.path.join( 29 | ROOT_DIR, 'server', 'env', 'Scripts', 'python.exe') 30 | manage_path = os.path.join(ROOT_DIR, 'server', 'manage.py') 31 | run_command('%s %s runserver %s' % (python_path, manage_path, SERVER_PORT)) 32 | 33 | 34 | def run_electron(): 35 | """ 36 | Starts the Electron client. 37 | Raises: 38 | RuntimeError - Electron client failed to start. 39 | """ 40 | electron_path = os.path.join( 41 | ROOT_DIR, 'client', 'node_modules', 'electron', 'dist', 'electron.exe') 42 | application_path = os.path.join(ROOT_DIR, 'client') 43 | run_command('%s %s' % (electron_path, application_path)) 44 | 45 | 46 | def main(server_only=False): 47 | """ 48 | Builds the project and starts a Django server and Electron client. 49 | Keyword args: 50 | server_only (bool) - If true, only the Django server is started. The 51 | project is built regardless. 52 | Raises: 53 | RuntimeError - The build process failed, the server failed to start, 54 | or the Electron client application failed to start. 55 | """ 56 | build() 57 | 58 | server_thread = Thread(target=run_server) 59 | server_thread.start() 60 | 61 | if not server_only: 62 | electron_thread = Thread(target=run_electron) 63 | electron_thread.start() 64 | 65 | 66 | if __name__ == '__main__': 67 | logging.basicConfig( 68 | level=logging.WARNING, 69 | format='%(levelname)s:%(message)s' 70 | ) 71 | parser = argparse.ArgumentParser( 72 | description='Starts the Django server and the Electron client.') 73 | parser.add_argument( 74 | '--server-only', action='store_true', dest='server_only') 75 | args = parser.parse_args() 76 | main(args.server_only) 77 | -------------------------------------------------------------------------------- /server/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /server/profiles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steve2/django-angular-electron/8aab612393cf5a6807a74d1d32a801be637a58ca/server/profiles/__init__.py -------------------------------------------------------------------------------- /server/profiles/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from django.contrib.auth.models import User 4 | from .models import Profile 5 | 6 | 7 | class ProfileInline(admin.StackedInline): 8 | model = Profile 9 | can_delete = False 10 | verbose_name_plural = 'Profile' 11 | fk_name = 'user' 12 | 13 | 14 | class CustomUserAdmin(UserAdmin): 15 | inlines = (ProfileInline,) 16 | 17 | def get_inline_instances(self, request, obj=None): 18 | if not obj: 19 | return list() 20 | return super(CustomUserAdmin, self).get_inline_instances(request, obj) 21 | 22 | 23 | admin.site.unregister(User) 24 | admin.site.register(User, CustomUserAdmin) 25 | -------------------------------------------------------------------------------- /server/profiles/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProfilesConfig(AppConfig): 5 | name = 'profiles' 6 | -------------------------------------------------------------------------------- /server/profiles/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.7 on 2017-11-15 11:18 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Profile', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('about', models.TextField(blank=True, max_length=512)), 24 | ('location', models.CharField(blank=True, max_length=32)), 25 | ('birthdate', models.DateField(blank=True, null=True)), 26 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /server/profiles/migrations/0002_profile_rank.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.7 on 2017-11-19 06:21 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('profiles', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='profile', 17 | name='rank', 18 | field=models.CharField(blank=True, default='Peon', max_length=32), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /server/profiles/migrations/0003_auto_20171119_0224.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.7 on 2017-11-19 08:24 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('profiles', '0002_profile_rank'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='profile', 17 | name='location', 18 | ), 19 | migrations.AddField( 20 | model_name='profile', 21 | name='from_location', 22 | field=models.CharField(blank=True, max_length=64), 23 | ), 24 | migrations.AddField( 25 | model_name='profile', 26 | name='live_location', 27 | field=models.CharField(blank=True, max_length=64), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /server/profiles/migrations/0004_auto_20181126_0310.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.7 on 2018-11-26 09:10 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('profiles', '0003_auto_20171119_0224'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='profile', 17 | name='rank', 18 | ), 19 | migrations.AddField( 20 | model_name='profile', 21 | name='comment', 22 | field=models.CharField(blank=True, default='Not much to say', max_length=64), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /server/profiles/migrations/0005_remove_profile_comment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.7 on 2018-11-26 23:29 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('profiles', '0004_auto_20181126_0310'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='profile', 17 | name='comment', 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /server/profiles/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steve2/django-angular-electron/8aab612393cf5a6807a74d1d32a801be637a58ca/server/profiles/migrations/__init__.py -------------------------------------------------------------------------------- /server/profiles/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | from django.db.models.signals import post_save 4 | from django.dispatch import receiver 5 | 6 | 7 | class Profile(models.Model): 8 | """Personal profile that corresponds with each User.""" 9 | user = models.OneToOneField(User, on_delete=models.CASCADE) 10 | about = models.TextField(max_length=512, blank=True) 11 | live_location = models.CharField(max_length=64, blank=True) 12 | from_location = models.CharField(max_length=64, blank=True) 13 | birthdate = models.DateField(null=True, blank=True) 14 | 15 | 16 | @receiver(post_save, sender=User) 17 | def create_user_profile(sender, instance, created, **kwargs): 18 | """Create a Profile when a User is created.""" 19 | if created: 20 | Profile.objects.create(user=instance) 21 | 22 | 23 | @receiver(post_save, sender=User) 24 | def save_user_profile(sender, instance, **kwargs): 25 | """Save a Profile when the User instance is saved.""" 26 | try: 27 | user_profile = instance.profile 28 | except: 29 | user_profile = None 30 | if user_profile is not None: 31 | user_profile.save() 32 | -------------------------------------------------------------------------------- /server/profiles/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from rest_framework import serializers 3 | from profiles.models import Profile 4 | 5 | class ProfileSerializer(serializers.ModelSerializer): 6 | """ 7 | Profile model serializer. 8 | """ 9 | class Meta: 10 | model = Profile 11 | fields = ('about', 'from_location', 'live_location', 'birthdate') 12 | 13 | class UserSerializer(serializers.ModelSerializer): 14 | """ 15 | Override the default User serializer. 16 | """ 17 | profile = ProfileSerializer() 18 | 19 | class Meta: 20 | model = User 21 | fields = ('id', 'username', 'email', 'date_joined', 'last_login', 'profile') 22 | 23 | def update(self, instance, validated_data): 24 | """Updates the User with a nested Profile.""" 25 | profile = instance.profile 26 | profile_data = validated_data.pop('profile') 27 | 28 | # Update the User object. 29 | # Note that we don't need to update 'last_login' or 'date_joined'. 30 | # We currently do not allow users to change their username. 31 | instance.email = validated_data.get('email') 32 | instance.save() 33 | 34 | # Update the Profile object separately. 35 | profile.about = profile_data.get('about', profile.about) 36 | profile.from_location = profile_data.get('from_location', profile.from_location) 37 | profile.live_location = profile_data.get('live_location', profile.live_location) 38 | profile.birthdate = profile_data.get('birthdate', profile.birthdate) 39 | profile.save() 40 | return instance -------------------------------------------------------------------------------- /server/requirements.txt: -------------------------------------------------------------------------------- 1 | astroid==1.5.3 2 | autopep8==1.4 3 | Babel==2.5.1 4 | certifi==2017.11.5 5 | chardet==3.0.4 6 | colorama==0.3.9 7 | defusedxml==0.5.0 8 | Django==1.11.15 9 | django-appconf==1.0.2 10 | django-filter==1.1.0 11 | django-formtools==2.1 12 | django-phonenumber-field==1.3.0 13 | django-templated-mail==1.0.0 14 | djangorestframework==3.7.3 15 | djoser==1.1.4 16 | idna==2.6 17 | isort==4.2.15 18 | Jinja2==2.10 19 | lazy-object-proxy==1.3.1 20 | Markdown==2.6.9 21 | MarkupSafe==1.0 22 | mccabe==0.6.1 23 | oauthlib==2.0.6 24 | olefile==0.44 25 | phonenumberslite==8.8.5 26 | pycodestyle==2.4.0 27 | pyflakes==2.0.0 28 | PyJWT==1.5.3 29 | pylint==1.7.4 30 | PySocks==1.6.7 31 | python3-openid==3.1.0 32 | pytz==2017.3 33 | qrcode==4.0.4 34 | requests==2.20.0 35 | requests-oauthlib==0.8.0 36 | six==1.11.0 37 | twilio==6.8.4 38 | urllib3==1.23 39 | wrapt==1.10.11 40 | -------------------------------------------------------------------------------- /server/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steve2/django-angular-electron/8aab612393cf5a6807a74d1d32a801be637a58ca/server/server/__init__.py -------------------------------------------------------------------------------- /server/server/overrides/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import serializers 3 | # from allauth.socialaccount.models import SocialAccount 4 | from profiles.serializers import ProfileSerializer 5 | 6 | 7 | # Get Django UserModel. 8 | # USER_MODEL = get_user_model() 9 | 10 | 11 | # class SocialAccountSerializer(serializers.ModelSerializer): 12 | # """ 13 | # Social account serializer for "allauth" library. 14 | # """ 15 | # extra_data = serializers.JSONField() 16 | # class Meta: 17 | # model = SocialAccount 18 | # fields = ('user', 'provider', 'uid', 'last_login', 'date_joined', 19 | # 'extra_data') 20 | # read_only_fields = ('user', 'provider', 'uid', 21 | # 'last_login', 'date_joined',) 22 | 23 | 24 | 25 | # class UserDetailsSerializer(serializers.ModelSerializer): 26 | # """ 27 | # User model with profile. 28 | # """ 29 | # profile = ProfileSerializer(read_only=True) 30 | # socialaccount_set = SocialAccountSerializer(read_only=True, many=True) 31 | 32 | # class Meta: 33 | # model = USER_MODEL 34 | # fields = ('pk', 'username', 'email', 'date_joined', 35 | # 'first_name', 'last_name', 'profile', 36 | # 'last_login', 'socialaccount_set') 37 | # read_only_fields = ('email',) 38 | -------------------------------------------------------------------------------- /server/server/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for server project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | PROJECT_DIR = os.path.dirname(BASE_DIR) 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'zm+81tv_9-!qzwez@w!&sz0ee-6zg_ezm%@i_paznzsfo!^7=2' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | # Login related URLs. 31 | 32 | LOGIN_URL = "/login/" 33 | LOGIN_REDIRECT_URL = "/" 34 | 35 | # Application definition 36 | 37 | INSTALLED_APPS = [ 38 | 'django.contrib.admin', 39 | 'django.contrib.auth', 40 | 'django.contrib.contenttypes', 41 | 'django.contrib.sessions', 42 | 'django.contrib.messages', 43 | 'django.contrib.staticfiles', 44 | 'django.contrib.sites', 45 | 'rest_framework', 46 | 'rest_framework.authtoken', 47 | 'djoser', 48 | 'profiles' 49 | ] 50 | 51 | MIDDLEWARE = [ 52 | 'django.middleware.security.SecurityMiddleware', 53 | 'django.contrib.sessions.middleware.SessionMiddleware', 54 | 'django.middleware.common.CommonMiddleware', 55 | 'django.middleware.csrf.CsrfViewMiddleware', 56 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 57 | 'django.contrib.messages.middleware.MessageMiddleware', 58 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 59 | ] 60 | 61 | SITE_ID = 1 62 | 63 | ROOT_URLCONF = 'server.urls' 64 | 65 | TEMPLATES = [ 66 | { 67 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 68 | 'DIRS': [ 69 | os.path.join(PROJECT_DIR, 'client'), 70 | ], 71 | 'APP_DIRS': True, 72 | 'OPTIONS': { 73 | 'context_processors': [ 74 | 'django.template.context_processors.debug', 75 | 'django.template.context_processors.request', 76 | 'django.contrib.auth.context_processors.auth', 77 | 'django.contrib.messages.context_processors.messages', 78 | ], 79 | }, 80 | }, 81 | ] 82 | 83 | 84 | WSGI_APPLICATION = 'server.wsgi.application' 85 | 86 | 87 | # Database 88 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 89 | 90 | DATABASES = { 91 | 'default': { 92 | 'ENGINE': 'django.db.backends.sqlite3', 93 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 94 | }, 95 | # 'default': { 96 | # 'ENGINE': 'django.db.backends.mysql', 97 | # 'NAME': 'djangular', 98 | # 'USER': 'root', 99 | # 'PASSWORD': 'admin', 100 | # 'HOST': 'localhost', 101 | # 'PORT': '3306', 102 | # }, 103 | } 104 | 105 | 106 | # Password validation 107 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 108 | 109 | AUTH_PASSWORD_VALIDATORS = [ 110 | { 111 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 112 | }, 113 | { 114 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 115 | }, 116 | { 117 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 118 | }, 119 | { 120 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 121 | }, 122 | ] 123 | 124 | 125 | # Internationalization 126 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 127 | 128 | LANGUAGE_CODE = 'en-us' 129 | 130 | TIME_ZONE = 'UTC' 131 | 132 | USE_I18N = True 133 | 134 | USE_L10N = True 135 | 136 | USE_TZ = True 137 | 138 | 139 | # Static files (CSS, JavaScript, Images) 140 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 141 | 142 | STATIC_URL = '/static/' 143 | STATICFILES_DIRS = [ 144 | os.path.join(PROJECT_DIR, 'client', 'static') 145 | ] 146 | 147 | # Destination directory for `collectstatic` command. 148 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 149 | 150 | 151 | # Django REST Framework Settings 152 | # http://www.django-rest-framework.org/ 153 | 154 | REST_FRAMEWORK = { 155 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 156 | 'rest_framework.authentication.TokenAuthentication', 157 | # 'rest_framework.authentication.SessionAuthentication', 158 | ) 159 | } 160 | 161 | DJOSER = { 162 | 'SERIALIZERS': { 163 | 'user': 'profiles.serializers.UserSerializer' 164 | } 165 | } -------------------------------------------------------------------------------- /server/server/urls.py: -------------------------------------------------------------------------------- 1 | """server URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url 17 | from django.contrib import admin 18 | from django.conf.urls import include 19 | from server.views import IndexTemplate 20 | 21 | urlpatterns = [ 22 | # Django Admin site. 23 | url(r'^admin/', admin.site.urls), 24 | 25 | # REST Authentication provided by `djoser`. 26 | url(r'^api/auth/', include('djoser.urls')), 27 | url(r'^api/auth/', include('djoser.urls.authtoken')), 28 | 29 | # Default to the AngularJS application template. 30 | url(r'^.*$', IndexTemplate.as_view()) 31 | ] 32 | -------------------------------------------------------------------------------- /server/server/views.py: -------------------------------------------------------------------------------- 1 | # from django.shortcuts import render 2 | from django.views.generic import TemplateView 3 | 4 | 5 | class IndexTemplate(TemplateView): 6 | """ 7 | The base template for our AngularJS application client. 8 | """ 9 | template_name = 'index.html' 10 | -------------------------------------------------------------------------------- /server/server/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for server project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from argparse import RawTextHelpFormatter 3 | import logging 4 | import argparse 5 | import os 6 | 7 | from shared import * 8 | 9 | 10 | # Path to virtualenv binaries for setup. 11 | PYTHON = os.path.join('env', 'Scripts', 'python.exe') 12 | PIP = os.path.join('env', 'Scripts', 'pip.exe') 13 | 14 | 15 | """ 16 | Setup the development environment for the project. 17 | 18 | Requirements: 19 | - python34 20 | - npm 21 | 22 | """ 23 | 24 | 25 | def init_virtual_env(subdir): 26 | """ 27 | Initializes a Python virtual environment. 28 | @returns: True if successful, False if the directory already exists. 29 | Raises: 30 | RuntimeError - Command to initialize virtual environment fails. 31 | """ 32 | if os.path.isdir(os.path.abspath(subdir)): 33 | return False 34 | run_command('virtualenv %s' % subdir) 35 | return True 36 | 37 | 38 | def upgrade_pip_version(): 39 | """ 40 | Upgrades the version of Python {pip}. 41 | Raises: 42 | RuntimeError - A setup command has failed (see console). 43 | """ 44 | run_command('%s -m pip install --upgrade pip' % PYTHON) 45 | 46 | 47 | def django_migrations(): 48 | """ 49 | Perform Django database migrations. 50 | Raises: 51 | RuntimeError - The Django {migrate} command failed (see console). 52 | """ 53 | run_command('%s manage.py migrate' % PYTHON) 54 | 55 | 56 | def create_django_superuser(): 57 | """ 58 | Creates a Django superuser for the database. 59 | Raises: 60 | RuntimeError - The Django {createsuperuser} command failed (see console). 61 | """ 62 | run_command('%s manage.py createsuperuser' % PYTHON) 63 | 64 | 65 | def main(skip_server, skip_client, skip_venv, 66 | skip_pip, skip_migrate, skip_superuser): 67 | """ 68 | Performs steps to setup the development environment. 69 | Args: 70 | skip_server (bool) - Skip the server setup. 71 | skip_client (bool) - Skip the client setup. 72 | skip_venv (bool) - Skip Python virtual environment setup. 73 | skip_pip (bool) - Skip "pip" upgrade. 74 | skip_migrate (bool) - Skip Django migration step. 75 | skip_superuser (bool) - Skip Django superuser creation. 76 | Raises: 77 | RuntimeError - A setup command failed (see console). 78 | TODO: 79 | - Make steps idempotent and remove function arguments. 80 | """ 81 | # The `virtualenv` tools create a virtual environment for the server. 82 | # This environment contains 3rd party dependencies that are needed. 83 | # If it is already installed `pip` returns successful and we continue. 84 | run_command('pip install virtualenv') 85 | 86 | # The `bower` tool is used as a package manager for the client application. 87 | # If it is already installed it will be updated and continue. 88 | run_command('npm install -g bower') 89 | 90 | # The `less` compiler is used when building client style definitions. 91 | # If it is already installed it will be updated and continue. 92 | run_command('npm install -g less') 93 | 94 | # The `electron-packager` tool builds the Electron client for distribution. 95 | # If it is already installed it will be updated and continue. 96 | run_command('npm install -g electron-packager') 97 | 98 | try: 99 | if skip_server: 100 | logging.info('Skipping server setup.') 101 | else: 102 | set_directory('server') 103 | 104 | # Initialize virtual environment. 105 | if not skip_venv: 106 | init_virtual_env('env') 107 | else: 108 | logging.info('Skipping virtual environment setup.') 109 | 110 | # Update pip to latest version. 111 | if not skip_pip: 112 | upgrade_pip_version() 113 | else: 114 | logging.info('Skipping pip update.') 115 | 116 | # Install Python packages to server virtual environment. 117 | run_command('%s install -r requirements.txt' % PIP) 118 | 119 | # Django migrations. 120 | if not skip_migrate: 121 | django_migrations() 122 | else: 123 | logging.info('Skipping Django migrations.') 124 | 125 | # Create Django superuser. 126 | if not skip_superuser: 127 | create_django_superuser() 128 | else: 129 | logging.info('Skipping Django Admin superuser creation.') 130 | 131 | # Client setup. 132 | if skip_client: 133 | logging.info('Skipping client setup.') 134 | else: 135 | set_directory('client') 136 | run_command('npm install') 137 | set_directory(os.path.join('client', 'static')) 138 | run_command('bower install') 139 | 140 | except (RuntimeError, OSError) as exception: 141 | logging.error('Setup failed due to:\n\t%s' % str(exception)) 142 | 143 | 144 | if __name__ == '__main__': 145 | logging.basicConfig( 146 | level=logging.INFO, 147 | format='%(levelname)s:%(message)s' 148 | ) 149 | description = 'Setup development environment.\n\n' \ 150 | 'If you have already run this script, it may be necessary to \n' \ 151 | 'skip virtual environment and superuser setup. This can be \n' \ 152 | 'done with options `--skip-venv` and `--skip-superuser`.' 153 | parser = argparse.ArgumentParser(description=description, 154 | formatter_class=RawTextHelpFormatter) 155 | parser.add_argument('--skip-venv', action='store_true', default=False) 156 | parser.add_argument('--skip-pip', action='store_true', default=False) 157 | parser.add_argument('--skip-migrate', action='store_true', default=False) 158 | parser.add_argument('--skip-superuser', action='store_true', default=False) 159 | parser.add_argument('--skip-server', action='store_true', default=False) 160 | parser.add_argument('--skip-client', action='store_true', default=False) 161 | args = parser.parse_args() 162 | main( 163 | skip_server=args.skip_server, 164 | skip_client=args.skip_client, 165 | skip_venv=args.skip_venv, 166 | skip_pip=args.skip_pip, 167 | skip_migrate=args.skip_migrate, 168 | skip_superuser=args.skip_superuser 169 | ) 170 | -------------------------------------------------------------------------------- /shared.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | 5 | # The root directory of the project (the directory containing this file). 6 | ROOT_DIR = os.path.dirname(os.path.realpath(__file__)) 7 | 8 | 9 | __all__ = ['ROOT_DIR', 'run_command', 'set_directory'] 10 | 11 | 12 | def set_directory(subdir): 13 | """ 14 | Sets the working directory to {subdir}. 15 | Raises: 16 | OSError - Command to change directories is not available. 17 | """ 18 | working_dir = os.path.join(ROOT_DIR, subdir) 19 | os.chdir(working_dir) 20 | print('Current directory: %s' % working_dir) 21 | 22 | 23 | def run_command(command): 24 | """ 25 | Run the specified command. 26 | Args: 27 | command (str) - Command to execute. 28 | Raises: 29 | RuntimeError - Command has non-zero exit code. 30 | """ 31 | out = os.system(command) 32 | logging.info('%s [%d]' % (command, out)) 33 | if out is not 0: 34 | raise RuntimeError('Command "%s" failed.' % command) 35 | --------------------------------------------------------------------------------