├── .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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------