├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── angular2-client ├── .editorconfig ├── .firebaserc ├── config │ ├── helpers.js │ ├── karma-test.shim.js │ ├── karma.conf.js │ └── webpack │ │ ├── webpack.common.js │ │ ├── webpack.dev.js │ │ ├── webpack.prod.js │ │ └── webpack.test.js ├── firebase.json ├── karma.config.js ├── package.json ├── src │ ├── app │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── app.routing.ts │ │ ├── core │ │ │ ├── core.module.ts │ │ │ └── services │ │ │ │ ├── auth-guard.service.ts │ │ │ │ ├── helpers.service.ts │ │ │ │ ├── index.ts │ │ │ │ ├── movies.service.ts │ │ │ │ ├── non-auth-guard.service.ts │ │ │ │ └── user.service.ts │ │ ├── movies │ │ │ ├── components │ │ │ │ ├── comments │ │ │ │ │ ├── comments.component.html │ │ │ │ │ ├── comments.component.scss │ │ │ │ │ └── comments.component.ts │ │ │ │ ├── index.ts │ │ │ │ ├── movie-details │ │ │ │ │ ├── movie-details.component.html │ │ │ │ │ ├── movie-details.component.scss │ │ │ │ │ └── movie-details.component.ts │ │ │ │ └── movie-list │ │ │ │ │ ├── movie-list.component.html │ │ │ │ │ ├── movie-list.component.scss │ │ │ │ │ └── movie-list.component.ts │ │ │ ├── movies.module.ts │ │ │ ├── movies.routing.ts │ │ │ └── services │ │ │ │ └── movie-detail-resolver.service.ts │ │ ├── services │ │ │ └── startup.service.ts │ │ ├── shared │ │ │ ├── components │ │ │ │ ├── errorbar │ │ │ │ │ ├── errorbar.component.scss │ │ │ │ │ └── errorbar.component.ts │ │ │ │ ├── index.ts │ │ │ │ ├── loading-spinner │ │ │ │ │ ├── loading-spinner.component.scss │ │ │ │ │ └── loading-spinner.component.ts │ │ │ │ └── toolbar │ │ │ │ │ ├── toolbar.component.html │ │ │ │ │ ├── toolbar.component.scss │ │ │ │ │ └── toolbar.component.ts │ │ │ ├── pipes │ │ │ │ ├── errors-to-list.pipe.ts │ │ │ │ ├── genres-to-text.pipe.ts │ │ │ │ └── index.ts │ │ │ └── shared.module.ts │ │ └── user │ │ │ ├── components │ │ │ ├── index.ts │ │ │ ├── input-hint.component.ts │ │ │ └── password-form │ │ │ │ ├── password-form.component.html │ │ │ │ ├── password-form.component.scss │ │ │ │ └── password-form.component.ts │ │ │ ├── helpers │ │ │ └── form-validators.ts │ │ │ ├── pages │ │ │ ├── auth-home │ │ │ │ ├── auth-home.component.html │ │ │ │ ├── auth-home.component.scss │ │ │ │ └── auth-home.component.ts │ │ │ ├── edit │ │ │ │ ├── edit.component.html │ │ │ │ ├── edit.component.scss │ │ │ │ └── edit.component.ts │ │ │ ├── index.ts │ │ │ ├── login │ │ │ │ ├── login.component.html │ │ │ │ └── login.component.ts │ │ │ ├── register │ │ │ │ ├── register.component.html │ │ │ │ ├── register.component.scss │ │ │ │ └── register.component.ts │ │ │ └── user-home │ │ │ │ ├── user-home.component.scss │ │ │ │ └── user-home.component.ts │ │ │ ├── services │ │ │ └── async-form-validators.service.ts │ │ │ ├── user.module.ts │ │ │ └── user.routing.ts │ ├── assets │ │ ├── favicon.ico │ │ └── images │ │ │ ├── preview.gif │ │ │ └── star-rating.icons.svg │ ├── config.ts │ ├── index.html │ ├── main.aot.ts │ ├── main.jit.ts │ ├── polyfills.ts │ ├── styles │ │ ├── styles.scss │ │ └── variables.scss │ └── vendor.ts ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock └── django-server ├── Procfile ├── manage.py ├── movies ├── __init__.py ├── admin.py ├── apps.py ├── middlewares │ ├── __init__.py │ └── jwt_authentication.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py ├── urls.py ├── utils.py ├── validators.py └── views │ ├── __init__.py │ ├── auth.py │ ├── comments.py │ ├── movies.py │ ├── ratings.py │ └── user_data.py ├── requirements.txt └── server ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | coverage_js 16 | 17 | # ignore documentation 18 | documentation 19 | 20 | # Dependency directory 21 | node_modules 22 | typings 23 | ts-node 24 | 25 | # Users Environment Variables 26 | .lock-wscript 27 | .tsdrc 28 | .typingsrc 29 | .env 30 | 31 | #IDE configuration files 32 | .idea 33 | *.iml 34 | 35 | /tools/**/*.js 36 | dist 37 | dev 38 | docs 39 | lib 40 | test 41 | tmp 42 | build 43 | 44 | # OS X trash files 45 | .DS_Store 46 | src/client/app/shared/config/env.config.js 47 | src/client/app/shared/config/env.config.js.map 48 | 49 | # python specific 50 | __pycache__/ 51 | db.sqlite3 52 | venv/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | before_install: 5 | # using nvm to install node since python is set as main language 6 | - nvm install node 7 | - npm --version 8 | # travis specific configuration to convince shell having a screen 9 | - export CHROME_BIN=chromium-browser 10 | - export DISPLAY=:99.0 11 | - sh -e /etc/init.d/xvfb start 12 | install: 13 | - cd ./angular2-client 14 | - npm install 15 | # no need to install python requirements here since they will be installed on Heroku 16 | # - cd ../django-server && pip install -r requirements.txt 17 | before_script: 18 | - npm install -g firebase-tools 19 | # no need to install heroku-cli and plugins since I'll use travis builtin deploy for Heroku 20 | # - npm install -g heroku-cli 21 | # - heroku plugins:install heroku-builds 22 | script: 23 | - cd ../angular2-client && npm run build 24 | after_success: 25 | # we currently are in /angular-client dir 26 | - firebase deploy --token $FIREBASE_API_TOKEN --non-interactive 27 | # this could be useful to manually deploy to Heroku. Using instead Travis builtin deploy method 28 | # - cd ../django-server && heroku builds:create -a glacial-shore-18891 --include-vcs-ignore 29 | before_deploy: 30 | - cd ../django-server 31 | deploy: 32 | provider: heroku 33 | api_key: $HEROKU_API_KEY 34 | app: glacial-shore-18891 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 damnko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular2 Django | Movies 2 | [![Build Status][build-badge]][build-link] 3 | [![License][licence-badge]](/LICENSE) 4 | 5 |

6 | 7 | Angular 2 Django demo 8 |
9 | 10 | View Demo 11 | 12 |

13 | 14 | A sample Angular 2 app paired with a Django API to explore: 15 | * user registration and uthentication 16 | * form validation 17 | * protected routes 18 | * jwt tokens 19 | * django implementations 20 | * mysql implementation 21 | 22 | ## Getting Started & Initial Setup 23 | ### Cloning the repo 24 | * `fork` this repo 25 | * `clone` your fork 26 | 27 | ### Setting up Angular 2 client 28 | Install these globals with `npm install --global`: 29 | * `webpack` (`npm install --global webpack`) 30 | * `webpack-dev-server` (`npm install --global webpack-dev-server`) 31 | * `yarn` (`npm install --global yarn`) 32 | 33 | Then, from inside the `angular2-client` folder run: 34 | * `yarn` or `npm install` to install all dependencies 35 | 36 | ### Setting up Django server 37 | In order to setup the Django server you need at least python 3.4. 38 | It is suggested to create a virtual environment to install all the dependencies: 39 | Install `virtualvenv` if you don't have it 40 | ```sh 41 | $ pip install virtualenv 42 | ``` 43 | create a new virtual environment 44 | ```sh 45 | $ virtualenv venv 46 | ``` 47 | activate the virtual environment that can be later deactivated with `deactivate` 48 | ```sh 49 | $ source venv/bin/activate 50 | ``` 51 | Then you can install all the required dependencies: 52 | ```sh 53 | $ pip install -r ./django-server/requirements.txt 54 | ``` 55 | 56 | ### Setting up MySQL 57 | The user registration, movie comments and ratings are stored in a MySQL database, if you don't have one already installed I suggest to follow the following quick tutorial (for Linux): https://www.digitalocean.com/community/tutorials/how-to-install-mysql-on-ubuntu-16-04 58 | You may also need to install these additional packages: 59 | ```sh 60 | $ sudo apt-get install python-dev mysql-server libmysqlclient-dev 61 | ``` 62 | 63 | ## Configuration 64 | 1. Create a `.env` file in the `/angular2-client` directory with the following line `SERVER_LOCATION=local`. This will set the appropriate env variable needed to address the api calls to `localhost:8000`. 65 | 2. Customize the `DATABASES` variable in `/django-server/server/settings.py` with the database name, host, user and password. 66 | 3. Create the database tables by running `python manage.py makemigrations movies` from the `/django-server` folder to create the migrations first, and then `python manage.py migrate` to apply them. 67 | 68 | ## Running the app 69 | * To start the server run `python manage.py runserver` from the `/django-server` folder. Server will be running on `http://localhost:8000/` 70 | * To start the client run `npm start` from the `/angular2-client` folder. Client will be running on `http://localhost:3000/` 71 | 72 | ## License 73 | MIT © [damnko](https://github.com/damnko) 74 | 75 | [licence-badge]: https://img.shields.io/npm/l/express.svg 76 | [build-badge]: https://travis-ci.org/damnko/angular2-feed-me.png?branch=master 77 | [build-link]: https://travis-ci.org/damnko/angular2-feed-me -------------------------------------------------------------------------------- /angular2-client/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /angular2-client/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "django-angular2-movies" 4 | } 5 | } -------------------------------------------------------------------------------- /angular2-client/config/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | exports.root = function(dir) { 6 | return path.resolve(__dirname, '..', dir); 7 | } 8 | -------------------------------------------------------------------------------- /angular2-client/config/karma-test.shim.js: -------------------------------------------------------------------------------- 1 | // source: https://angular.io/docs/ts/latest/guide/webpack.html#!#loaders 2 | 3 | Error.stackTraceLimit = Infinity; 4 | 5 | require('core-js/es6'); 6 | require('core-js/es7/reflect'); 7 | 8 | require('zone.js/dist/zone'); 9 | require('zone.js/dist/long-stack-trace-zone'); 10 | require('zone.js/dist/proxy'); 11 | require('zone.js/dist/sync-test'); 12 | require('zone.js/dist/jasmine-patch'); 13 | require('zone.js/dist/async-test'); 14 | require('zone.js/dist/fake-async-test'); 15 | 16 | var appContext = require.context('../src', true, /\.spec\.ts/); 17 | 18 | appContext.keys().forEach(appContext); 19 | 20 | var testing = require('@angular/core/testing'); 21 | var browser = require('@angular/platform-browser-dynamic/testing'); 22 | 23 | testing.TestBed.initTestEnvironment(browser.BrowserDynamicTestingModule, browser.platformBrowserDynamicTesting()); 24 | -------------------------------------------------------------------------------- /angular2-client/config/karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpackConfig = require('./webpack/webpack.test'); 4 | 5 | module.exports = function (config) { 6 | var _config = { 7 | basePath: '', 8 | frameworks: ['jasmine'], 9 | files: [ 10 | {pattern: './config/karma-test-shim.js', watched: false} 11 | ], 12 | preprocessors: { 13 | './config/karma-test-shim.js': ['coverage', 'webpack', 'sourcemap'] 14 | }, 15 | webpack: webpackConfig, 16 | webpackMiddleware: { 17 | stats: 'errors-only' 18 | }, 19 | webpackServer: { 20 | noInfo: true 21 | }, 22 | reporters: ['mocha'], 23 | port: 9876, 24 | colors: true, 25 | logLevel: config.LOG_INFO, 26 | autoWatch: false, 27 | browsers: ['Chrome'], 28 | singleRun: true, // if true, Karma captures browsers, runs the tests and exits 29 | failOnEmptyTestSuite: false 30 | }; 31 | 32 | config.set(_config); 33 | }; 34 | -------------------------------------------------------------------------------- /angular2-client/config/webpack/webpack.common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const AssetsPlugin = require('assets-webpack-plugin'); 4 | const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin; 5 | const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'); 6 | const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin'); 7 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 8 | const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin'); 9 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 10 | const ImageminPlugin = require('imagemin-webpack-plugin').default; 11 | const Dotenv = require('dotenv-webpack'); 12 | 13 | const helpers = require('../helpers'); 14 | 15 | const PROD = process.env.NODE_ENV === 'production'; 16 | 17 | const METADATA = { 18 | title: 'An Angular2 app to see & comment top movies', 19 | baseUrl: '/' 20 | } 21 | 22 | module.exports = { 23 | entry: { 24 | polyfills: './src/polyfills.ts', 25 | vendor: './src/vendor.ts' 26 | }, 27 | resolve: { 28 | extensions: ['.ts', '.js', '.json'], 29 | modules: [helpers.root('src'), helpers.root('node_modules')] 30 | }, 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.json$/, 35 | use: 'json-loader' 36 | }, 37 | { 38 | test: /\.css$/, 39 | use: ['to-string-loader', 'css-loader'], 40 | exclude: [helpers.root('src/styles')] 41 | }, 42 | { 43 | test: /\.scss$/, 44 | use: ['to-string-loader', 'css-loader', 'sass-loader'], 45 | exclude: [helpers.root('src/styles')] 46 | }, 47 | { 48 | test: /\.html$/, 49 | use: 'raw-loader', 50 | exclude: [helpers.root('src/index.html')] 51 | }, 52 | { 53 | test: /\.(png|jpe?g|gif|eot|ico)$/, 54 | use: 'file-loader' 55 | }, 56 | { 57 | test: /\.(eot|woff2?|svg|ttf)([\?]?.*)$/, 58 | use: 'file-loader' 59 | } 60 | ] 61 | }, 62 | plugins: [ 63 | new AssetsPlugin({ 64 | path: helpers.root('dist'), 65 | filename: 'webpack.assets.json', 66 | prettyPrint: true 67 | }), 68 | new CheckerPlugin(), 69 | new CommonsChunkPlugin({ 70 | name: ['polyfills', 'vendor', 'main'].reverse() 71 | }), 72 | new CopyWebpackPlugin([ 73 | { from: 'src/assets', to: 'assets' } 74 | ]), 75 | new ImageminPlugin({ 76 | // skipping svg because it sometimes resulted in corrupted img 77 | test: /\.(jpe?g|png|gif)$/i, 78 | disable: !PROD, 79 | mozjpeg: { 80 | quality: 65 81 | }, 82 | pngquant:{ 83 | quality: "10-20", 84 | speed: 4 85 | }, 86 | gifsicle: { 87 | optimizationLevel: 7, 88 | interlaced: false 89 | }, 90 | optipng: { 91 | optimizationLevel: 7, 92 | interlaced: false 93 | } 94 | }), 95 | new HtmlWebpackPlugin({ 96 | template: 'src/index.html', 97 | title: METADATA.title, 98 | chunksSortMode: 'dependency', 99 | metadata: METADATA, 100 | inject: 'head' 101 | }), 102 | new Dotenv() 103 | ] 104 | } 105 | -------------------------------------------------------------------------------- /angular2-client/config/webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpackMerge = require('webpack-merge'); 4 | const webpackMergeDll = webpackMerge.strategy({plugins: 'replace'}); 5 | const DllBundlesPlugin = require('webpack-dll-bundles-plugin').DllBundlesPlugin; 6 | const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); 7 | 8 | const commonConfig = require('./webpack.common'); 9 | const helpers = require('../helpers'); 10 | 11 | module.exports = webpackMerge(commonConfig, { 12 | devtool: 'cheap-module-source-map', 13 | entry: { 14 | main: './src/main.jit.ts' 15 | }, 16 | output: { 17 | path: helpers.root('dist'), 18 | filename: '[name].bundle.js', 19 | sourceMapFilename: '[file].map', 20 | chunkFilename: '[id].chunk.js', 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.ts$/, 26 | loaders: [ 27 | { 28 | loader: 'awesome-typescript-loader', 29 | options: { configFileName: 'tsconfig.json' } 30 | } , 31 | 'angular2-template-loader', 32 | 'angular-router-loader' 33 | ] 34 | }, 35 | { 36 | test: /\.css$/, 37 | use: ['style-loader', 'css-loader'], 38 | include: [helpers.root('src/styles')] 39 | }, 40 | { 41 | test: /\.scss$/, 42 | use: ['style-loader', 'css-loader', 'sass-loader'], 43 | include: [helpers.root('src/styles')] 44 | } 45 | ] 46 | }, 47 | plugins: [ 48 | 49 | ], 50 | devServer: { 51 | port: 3000, 52 | host: 'localhost', 53 | historyApiFallback: true, 54 | watchOptions: { 55 | aggregateTimeout: 300, 56 | poll: 1000 57 | }, 58 | proxy: { 59 | "/api": { 60 | target: "http://localhost:8000", 61 | pathRewrite: {"^/api" : ""} 62 | } 63 | }, 64 | stats: { 65 | cached: true, 66 | cachedAssets: true, 67 | chunks: true, 68 | chunkModules: false, 69 | colors: true, 70 | hash: false, 71 | reasons: true, 72 | timings: true, 73 | version: false 74 | } 75 | } 76 | }) 77 | -------------------------------------------------------------------------------- /angular2-client/config/webpack/webpack.prod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpackMerge = require('webpack-merge'); 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | const OptimizeJsPlugin = require('optimize-js-plugin'); 6 | const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin'); 7 | const NoEmitOnErrorsPlugin = require('webpack/lib/NoEmitOnErrorsPlugin'); 8 | const DefinePlugin = require('webpack/lib/DefinePlugin'); 9 | const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); 10 | const AotPlugin = require('@ngtools/webpack').AotPlugin; 11 | 12 | const commonConfig = require('./webpack.common'); 13 | const helpers = require('../helpers'); 14 | 15 | const ENV = process.env.NODE_ENV || 'production'; 16 | 17 | module.exports = webpackMerge(commonConfig, { 18 | entry: { 19 | main: './src/main.aot.ts' 20 | }, 21 | devtool: 'source-map', 22 | output: { 23 | path: helpers.root('dist'), 24 | filename: '[name].[chunkhash].bundle.js', 25 | sourceMapFilename: '[name].[chunkhash].bundle.map', 26 | chunkFilename: '[id].[chunkhash].chunk.js' 27 | }, 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.ts$/, 32 | loader: '@ngtools/webpack', 33 | }, 34 | { 35 | test: /\.css$/, 36 | loader: ExtractTextPlugin.extract({ 37 | fallback: 'style-loader', 38 | use: 'css-loader' 39 | }), 40 | include: [helpers.root('src/styles')] 41 | }, 42 | 43 | /* 44 | * Extract and compile SCSS files from .src/styles directory to external CSS file 45 | */ 46 | { 47 | test: /\.scss$/, 48 | loader: ExtractTextPlugin.extract({ 49 | fallback: 'style-loader', 50 | use: 'css-loader!sass-loader' 51 | }), 52 | include: [helpers.root('src/styles')] 53 | }, 54 | ], 55 | }, 56 | plugins: [ 57 | new NoEmitOnErrorsPlugin(), 58 | new OptimizeJsPlugin({ 59 | sourceMap: false 60 | }), 61 | new AotPlugin({ 62 | tsConfigPath: './tsconfig.json', 63 | entryModule: helpers.root('src/app/app.module#AppModule') 64 | }), 65 | new ExtractTextPlugin('[name].[contenthash].css'), 66 | new DefinePlugin({ 67 | 'process.env': { 68 | 'ENV': JSON.stringify(ENV), 69 | 'NODE_ENV': JSON.stringify(ENV) 70 | } 71 | }), 72 | // NOTE: To debug prod builds uncomment //debug lines and comment //prod lines 73 | new UglifyJsPlugin({ 74 | // beautify: true, //debug 75 | // mangle: false, //debug 76 | // dead_code: false, //debug 77 | // unused: false, //debug 78 | // deadCode: false, //debug 79 | // compress: { 80 | // screw_ie8: true, 81 | // keep_fnames: true, 82 | // drop_debugger: false, 83 | // dead_code: false, 84 | // unused: false 85 | // }, // debug 86 | // comments: true, //debug 87 | 88 | beautify: false, //prod 89 | output: { 90 | comments: false 91 | }, //prod 92 | mangle: { 93 | screw_ie8: true 94 | }, //prod 95 | compress: { 96 | screw_ie8: true, 97 | warnings: false, 98 | conditionals: true, 99 | unused: true, 100 | comparisons: true, 101 | sequences: true, 102 | dead_code: true, 103 | evaluate: true, 104 | if_return: true, 105 | join_vars: true, 106 | negate_iife: false // we need this for lazy v8 107 | }, 108 | }), 109 | new LoaderOptionsPlugin({ 110 | htmlLoader: { 111 | minimize: false // workaround for ng2 112 | } 113 | }) 114 | ] 115 | }); 116 | -------------------------------------------------------------------------------- /angular2-client/config/webpack/webpack.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin'); 4 | 5 | const helpers = require('../helpers'); 6 | 7 | module.exports = { 8 | devtool: 'inline-source-map', 9 | resolve: { 10 | extensions: ['.ts', '.js'] 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.ts$/, 16 | loaders: [ 17 | { 18 | loader: 'awesome-typescript-loader', 19 | options: { configFileName: 'tsconfig.json' } 20 | } , 'angular2-template-loader' 21 | ] 22 | }, 23 | { 24 | test: /\.html$/, 25 | use: 'raw-loader', 26 | exclude: [helpers.root('src/index.html')] 27 | }, 28 | { 29 | test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/, 30 | loader: 'null-loader' 31 | }, 32 | { 33 | test: /\.css$/, 34 | loader: ['to-string-loader', 'css-loader'], 35 | exclude: [helpers.root('src/index.html')] 36 | }, 37 | 38 | /** 39 | * Raw loader support for *.scss files 40 | * 41 | * See: https://github.com/webpack/raw-loader 42 | */ 43 | { 44 | test: /\.scss$/, 45 | loader: ['raw-loader', 'sass-loader'], 46 | exclude: [helpers.root('src/index.html')] 47 | }, 48 | ] 49 | }, 50 | 51 | plugins: [ 52 | new ContextReplacementPlugin( 53 | // The (\\|\/) piece accounts for path separators in *nix and Windows 54 | /angular(\\|\/)core(\\|\/)@angular/, 55 | helpers.root('src'), // location of your src 56 | {} // a map of your routes 57 | ) 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /angular2-client/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist", 4 | "rewrites": [ 5 | { 6 | "source": "**", 7 | "destination": "/index.html" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /angular2-client/karma.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./config/karma.conf.js'); 4 | -------------------------------------------------------------------------------- /angular2-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-movies", 3 | "version": "0.0.0", 4 | "description": "An Angular2 app to see & comment top movies", 5 | "main": "main.js", 6 | "scripts": { 7 | "clean": "rimraf build dist dll", 8 | "build:dev": "cross-env NODE_ENV=development webpack-dev-server --open --progress --profile --watch --content-base src/", 9 | "build:prod": "cross-env NODE_ENV=production webpack", 10 | "build:ngc": "ngc", 11 | "build": "npm run clean && npm run build:ngc && npm run build:prod", 12 | "build:server:prod": "npm run build && npm run server:prod", 13 | "development": "npm run build:dev", 14 | "lint": "tslint \"src/**/*.ts\"", 15 | "production": "npm run build:server:prod", 16 | "server:prod": "http-server dist -c-1 --cors", 17 | "start": "npm run development", 18 | "test": "npm run lint && karma start", 19 | "test:watch": "npm run test -- --auto-watch --no-single-run" 20 | }, 21 | "keywords": [ 22 | "angular2", 23 | "django" 24 | ], 25 | "author": "damnko", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "@angular/common": "^4.1.3", 29 | "@angular/compiler": "^4.1.3", 30 | "@angular/compiler-cli": "^4.1.3", 31 | "@angular/core": "^4.1.3", 32 | "@angular/forms": "^4.1.3", 33 | "@angular/http": "^4.1.3", 34 | "@angular/platform-browser": "^4.1.3", 35 | "@angular/platform-browser-dynamic": "^4.1.3", 36 | "@angular/platform-server": "^4.1.3", 37 | "@angular/router": "^4.1.3", 38 | "@ngtools/webpack": "^1.3.1", 39 | "@types/jasmine": "^2.5.47", 40 | "@types/lodash": "^4.14.66", 41 | "@types/node": "^7.0.13", 42 | "add-asset-html-webpack-plugin": "^2.0.1", 43 | "angular-router-loader": "^0.6.0", 44 | "angular2-template-loader": "^0.6.2", 45 | "assets-webpack-plugin": "^3.5.1", 46 | "awesome-typescript-loader": "^3.1.2", 47 | "codelyzer": "^3.0.0", 48 | "copy-webpack-plugin": "^4.0.1", 49 | "core-js": "^2.4.1", 50 | "cross-env": "^5.0.0", 51 | "css-loader": "^0.28.0", 52 | "dotenv": "^4.0.0", 53 | "dotenv-webpack": "^1.4.3", 54 | "extract-text-webpack-plugin": "^2.1.0", 55 | "file-loader": "^0.11.1", 56 | "html-webpack-plugin": "^2.28.0", 57 | "ie-shim": "^0.1.0", 58 | "image-webpack-loader": "^3.3.0", 59 | "imagemin-webpack-plugin": "^1.4.4", 60 | "jasmine-core": "^2.6.0", 61 | "json-loader": "^0.5.4", 62 | "karma": "^1.6.0", 63 | "karma-chrome-launcher": "^2.0.0", 64 | "karma-coverage": "^1.1.1", 65 | "karma-jasmine": "^1.1.0", 66 | "karma-mocha-reporter": "^2.2.3", 67 | "karma-sourcemap-loader": "^0.3.7", 68 | "karma-webpack": "^2.0.3", 69 | "node-sass": "^4.5.2", 70 | "optimize-js-plugin": "^0.0.4", 71 | "raw-loader": "^0.5.1", 72 | "rimraf": "^2.6.1", 73 | "rxjs": "^5.3.0", 74 | "sass-loader": "^6.0.3", 75 | "script-ext-html-webpack-plugin": "^1.7.1", 76 | "style-loader": "^0.17.0", 77 | "to-string-loader": "^1.1.5", 78 | "ts-lint": "^4.5.1", 79 | "ts-loader": "^2.0.3", 80 | "tslint": "^5.1.0", 81 | "typescript": "^2.2.2", 82 | "webpack": "^2.4.1", 83 | "webpack-dev-server": "^2.4.4", 84 | "webpack-dll-bundles-plugin": "^1.0.0-beta.5", 85 | "webpack-merge": "^4.1.0", 86 | "zone.js": "^0.8.10" 87 | }, 88 | "dependencies": { 89 | "@angular/animations": "^4.2.2", 90 | "@angular/flex-layout": "^2.0.0-beta.8", 91 | "@angular/material": "^2.0.0-beta.6", 92 | "angular-star-rating": "^3.0.0", 93 | "angular2-jwt": "^0.2.3", 94 | "font-awesome": "^4.7.0", 95 | "http-server": "^0.10.0", 96 | "lodash": "^4.17.4", 97 | "moment": "^2.18.1", 98 | "ng2-cookies": "^1.0.12", 99 | "ngx-pagination": "^3.0.0" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /angular2-client/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, AfterViewInit, Renderer } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app', 5 | template: ` 6 | 7 | 8 | 9 | ` 10 | }) 11 | 12 | export class AppComponent implements AfterViewInit { 13 | constructor( 14 | private renderer: Renderer 15 | ) {} 16 | 17 | ngAfterViewInit() { 18 | // hide preloader 19 | const preloader: HTMLElement = document.getElementById('preloader'); 20 | this.renderer.setElementClass(preloader, 'loaded', true); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /angular2-client/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, APP_INITIALIZER } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { HttpModule } from '@angular/http'; 4 | 5 | import { HelpersService } from './core/services'; 6 | import { StartupService } from './services/startup.service'; 7 | import { CoreModule } from './core/core.module'; 8 | import { SharedModule } from './shared/shared.module'; 9 | import { MoviesModule } from './movies/movies.module'; 10 | import { AppComponent } from './app.component'; 11 | import { AppRoutingModule } from './app.routing'; 12 | 13 | // global styles 14 | import '../styles/styles.scss'; 15 | 16 | export function startServiceFactory(ss: StartupService): () => Promise { 17 | return () => ss.load(); 18 | } 19 | 20 | @NgModule({ 21 | imports: [ 22 | BrowserModule, 23 | HttpModule, 24 | AppRoutingModule, 25 | MoviesModule, 26 | SharedModule, 27 | CoreModule 28 | ], 29 | declarations: [ AppComponent ], 30 | bootstrap: [ AppComponent ], 31 | providers: [ 32 | StartupService, 33 | // service to get csrf token cookie from server before app initialization 34 | // otherwise each call to the django server will return a '403 Forbidden' error 35 | { 36 | provide: APP_INITIALIZER, 37 | useFactory: startServiceFactory, 38 | deps: [StartupService], 39 | multi: true 40 | } 41 | ] 42 | }) 43 | export class AppModule { } 44 | -------------------------------------------------------------------------------- /angular2-client/src/app/app.routing.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { UserModule } from './user/user.module'; 5 | 6 | const routes: Routes = [ 7 | { path: 'user', loadChildren: './user/user.module#UserModule' }, 8 | ]; 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forRoot(routes)], 12 | exports: [RouterModule], 13 | }) 14 | export class AppRoutingModule { } 15 | -------------------------------------------------------------------------------- /angular2-client/src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | // had to install animations with " yarn add @angular/animations@latest" 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { AuthHttp, AuthConfig } from 'angular2-jwt'; 5 | import { Http, RequestOptions } from '@angular/http'; 6 | 7 | import { 8 | NonAuthGuard, 9 | AuthGuard, 10 | HelpersService, 11 | UserService, 12 | MoviesService 13 | } from './services'; 14 | import { SharedModule } from './../shared/shared.module'; 15 | 16 | // auth0/angular2-jwt custom configuration for csfr and token stored in cookies 17 | export function authHttpServiceFactory(http: Http, options: RequestOptions) { 18 | return new AuthHttp(new AuthConfig({ 19 | tokenName: 'token', 20 | // initially considering to store token in cookies with Set-Cookie header 21 | // but could not retrieve it when client and server where hosted on different domains 22 | // so, reverting to localStorage on final version 23 | // tokenGetter: (() => Cookie.get('token')), 24 | noTokenScheme: true, // otherwise it will put "Bearer " in front of the token 25 | globalHeaders: [{ 26 | // in order for this to work I had to get the csrf token with APP_INITIALIZER on app.module.ts 27 | 'X-CSRFToken': localStorage.getItem('csrftoken'), 28 | }], 29 | }), http, options); 30 | } 31 | 32 | @NgModule({ 33 | imports: [ 34 | SharedModule, 35 | BrowserAnimationsModule, 36 | ], 37 | providers: [ 38 | HelpersService, 39 | MoviesService, 40 | UserService, 41 | AuthGuard, 42 | NonAuthGuard, 43 | // Does not work with themoviedb api, so it gets handled individually on each get/post request 44 | // { provide: XSRFStrategy, useValue: new CookieXSRFStrategy('csrftoken', 'X-CSRFToken') } 45 | { 46 | provide: AuthHttp, 47 | useFactory: authHttpServiceFactory, 48 | deps: [Http, RequestOptions] 49 | } 50 | ] 51 | }) 52 | export class CoreModule { } 53 | -------------------------------------------------------------------------------- /angular2-client/src/app/core/services/auth-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, 4 | CanActivate, 5 | RouterStateSnapshot, 6 | Router 7 | } from '@angular/router'; 8 | import { UserService } from './user.service'; 9 | 10 | @Injectable() 11 | export class AuthGuard implements CanActivate { 12 | constructor( 13 | private us: UserService, 14 | private router: Router 15 | ) { } 16 | 17 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 18 | if (!this.us.isAuth()) { 19 | localStorage.setItem('error', 'You need to be logged in to access this page'); 20 | this.router.navigate(['/']); 21 | return false; 22 | } 23 | return true; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /angular2-client/src/app/core/services/helpers.service.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs/Rx'; 2 | import { RequestOptions, Headers, Http } from '@angular/http'; 3 | import { Injectable } from '@angular/core'; 4 | import { MdSnackBar } from '@angular/material'; 5 | 6 | import { config } from './../../../config'; 7 | import { UserService } from './user.service'; 8 | 9 | @Injectable() 10 | export class HelpersService { 11 | 12 | constructor( 13 | private snackBar: MdSnackBar, 14 | private http: Http 15 | ) { } 16 | 17 | showMessage(body: string): void { 18 | this.snackBar.open(body, 'OK', { 19 | duration: 3000, 20 | extraClasses: ['error'] 21 | }); 22 | } 23 | 24 | getCsrf(): Observable { 25 | const options = new RequestOptions({ withCredentials: true }); 26 | const csrfToken = localStorage.getItem('csrftoken'); 27 | if (!csrfToken) { 28 | return this.http.get(`${config.api}/movies/auth/csrf`, options) 29 | .first() 30 | .map(res => res.json()) 31 | .do(res => localStorage.setItem('csrftoken', res.data)); 32 | } 33 | return Observable.of(csrfToken); 34 | } 35 | 36 | createHeaders(): RequestOptions { 37 | const headers = new Headers({ 38 | 'Content-Type': 'application/json', 39 | 'X-CSRFToken': localStorage.getItem('csrftoken') 40 | }); 41 | const options = new RequestOptions({ headers, withCredentials: true }); 42 | return options; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /angular2-client/src/app/core/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth-guard.service'; 2 | export * from './helpers.service'; 3 | export * from './movies.service'; 4 | export * from './non-auth-guard.service'; 5 | export * from './user.service'; 6 | -------------------------------------------------------------------------------- /angular2-client/src/app/core/services/movies.service.ts: -------------------------------------------------------------------------------- 1 | import { Cookie } from 'ng2-cookies'; 2 | import { AuthHttp } from 'angular2-jwt'; 3 | import { Http, Headers, RequestOptions } from '@angular/http'; 4 | import { Injectable } from '@angular/core'; 5 | import { Observable } from 'rxjs/Rx'; 6 | import * as moment from 'moment'; 7 | 8 | import { HelpersService } from './helpers.service'; 9 | import { UserService } from './user.service'; 10 | import { config } from './../../../config'; 11 | 12 | @Injectable() 13 | export class MoviesService { 14 | 15 | constructor( 16 | private http: Http, 17 | private us: UserService, 18 | private hs: HelpersService, 19 | private auth: AuthHttp 20 | ) { } 21 | 22 | moviesFromDate(format?: string): Date | string { 23 | const daysAgo = 30; 24 | const date = moment().subtract(daysAgo, 'days'); 25 | 26 | if (format) { 27 | return date.format(format); 28 | } 29 | return date.toDate(); 30 | } 31 | 32 | getTopMovies(): any { 33 | const baseUrl = `${config.themoviedb.endpoint}/discover/movie?`; 34 | const params = [ 35 | `api_key=${config.themoviedb.apiKey}`, 36 | `include_adult=false`, 37 | `release_date.gte=${this.moviesFromDate('YYYY-MM-DD')}`, 38 | `sort_by=popularity.desc` 39 | ].join('&'); 40 | 41 | return this.http.get(`${baseUrl}${params}`) 42 | .map(res => res.json()) 43 | .flatMap(res => { 44 | const ids = res.results.map((movie: any) => movie.id); 45 | return this.getMoviesSummary(ids, res); 46 | }).map(({ tmdb, api }) => { 47 | // populate each field with custom data from backend api 48 | tmdb.results.forEach((movie: any) => { 49 | movie['custom_data'] = api.data.movies[movie.id]; 50 | }); 51 | 52 | return tmdb; 53 | }); 54 | } 55 | 56 | getMovieDetails(id: string): Observable { 57 | const url = `${config.themoviedb.endpoint}/movie/${id}?api_key=${config.themoviedb.apiKey}`; 58 | 59 | return this.http.get(url) 60 | .map(res => res.json()); 61 | } 62 | 63 | getMovieInternalDetails(id: string): Observable { 64 | return this.http.get(`${config.api}/movies/movie/${id}/`) 65 | .first() 66 | .map((res: any) => res.json()); 67 | } 68 | 69 | rateMovie(id: string, rating: number): Observable { 70 | const url = `${config.api}/movies/rate`; 71 | // check if user is logged in 72 | return this.us.user$ 73 | .first() 74 | .flatMap(user => { 75 | if (user) { 76 | return this.auth.post(url, { id, rating }, { withCredentials: true }); 77 | } 78 | return this.postRequest(url, { id, rating }); 79 | }); 80 | } 81 | 82 | getMovieRating(id: string): Observable { 83 | const url = `${config.api}/movies/movie/${id}/rating/`; 84 | return this.postRequest(url, {}); 85 | } 86 | 87 | removeRating(id: string): Observable { 88 | const params = [ 89 | `u=${this.us.getOrSetUsername()}`, 90 | `m_id=${id}` 91 | ].join('&'); 92 | 93 | const options = this.hs.createHeaders(); 94 | 95 | return this.http.delete(`${config.api}/movies/rate?${params}`, options) 96 | .first() 97 | .map(res => res.json()); 98 | } 99 | 100 | getComments(id: string, page: number): Observable { 101 | const params = [ 102 | `u=${this.us.getOrSetUsername()}`, 103 | `p=${page}` 104 | ].join('&'); 105 | 106 | return this.http.get(`${config.api}/movies/movie/${id}/comments/?${params}`) 107 | .map(res => res.json()); 108 | } 109 | 110 | postComment(id: string, body: string): Observable { 111 | const url = `${config.api}/movies/comment`; 112 | // check if user is logged in 113 | return this.us.user$ 114 | .first() 115 | .flatMap(user => { 116 | if (user) { 117 | return this.auth.post(url, { id, body }, { withCredentials: true }); 118 | } 119 | return this.postRequest(url, { id, body }); 120 | }); 121 | } 122 | 123 | removeComment(id: string): Observable { 124 | const params = [ 125 | `u=${this.us.getOrSetUsername()}`, 126 | `id=${id}` 127 | ].join('&'); 128 | 129 | return this.http.delete(`${config.api}/movies/comment?${params}`, this.hs.createHeaders()) 130 | .first() 131 | .map(res => res.json()); 132 | } 133 | 134 | private getMoviesSummary(movieIds: string[], tmdbRes: any): Observable<{tmdb: any, api: any}> { 135 | return Observable.combineLatest( 136 | Observable.of(tmdbRes), 137 | this.http.get(`${config.api}/movies/get-all?ids=${movieIds.join(',')}`) 138 | .map((api: any) => api.json()), 139 | (tmdb, api) => { 140 | return { tmdb, api }; 141 | }); 142 | } 143 | 144 | private postRequest(url: string, data: any): Observable { 145 | const username = this.us.getOrSetUsername(); 146 | 147 | const options = this.hs.createHeaders(); 148 | 149 | return this.http.post(url, Object.assign({ username }, data), options) 150 | .map(res => res.json()); 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /angular2-client/src/app/core/services/non-auth-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | Router, 4 | ActivatedRouteSnapshot, 5 | CanActivateChild, 6 | RouterStateSnapshot 7 | } from '@angular/router'; 8 | import { UserService } from './user.service'; 9 | 10 | @Injectable() 11 | export class NonAuthGuard implements CanActivateChild { 12 | constructor( 13 | private us: UserService, 14 | private router: Router 15 | ) { } 16 | 17 | canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 18 | if (this.us.isAuth()) { 19 | localStorage.setItem('error', 'You need to logout to access this page'); 20 | this.router.navigate(['/']); 21 | return false; 22 | } 23 | return true; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /angular2-client/src/app/core/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 2 | import { Http } from '@angular/http'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import { Subject } from 'rxjs/Subject'; 5 | import { Injectable } from '@angular/core'; 6 | import { JwtHelper, AuthHttp } from 'angular2-jwt'; 7 | 8 | import { HelpersService } from './helpers.service'; 9 | import { config } from './../../../config'; 10 | 11 | @Injectable() 12 | export class UserService { 13 | jwtHelper: JwtHelper = new JwtHelper(); 14 | user$ = new BehaviorSubject(this.getAuthDetails()); 15 | user: any = false; 16 | 17 | constructor( 18 | private http: Http, 19 | private hs: HelpersService, 20 | private auth: AuthHttp 21 | ) { 22 | this.setUserData(); 23 | } 24 | 25 | authPost(url: string, data: any): Observable { 26 | const options = this.hs.createHeaders(); 27 | 28 | return this.http.post(url, data, options) 29 | .map(res => res.json()); 30 | } 31 | 32 | getOrSetUsername(): string { 33 | let username = this.user.username || localStorage.getItem('username'); 34 | if (!username) { 35 | username = btoa(Math.random().toString()); 36 | localStorage.setItem('username', username); 37 | } 38 | return username; 39 | } 40 | 41 | usernameIsUnique(username: string): Observable { 42 | return this.http.get(`${config.api}/movies/auth/username-exists/?u=${username}`) 43 | .first() 44 | .map(res => res.json()) 45 | .map(res => !res.data.username_exists); 46 | } 47 | 48 | register(formData: any): Observable { 49 | return this.authPost(`${config.api}/movies/auth/register/`, formData) 50 | .do(res => this.setToken(res.data)); 51 | } 52 | 53 | login(formData: any): Observable { 54 | return this.authPost(`${config.api}/movies/auth/login/`, formData) 55 | .do(res => this.setToken(res.data)); 56 | } 57 | 58 | editProfile(formData: any): Observable { 59 | // need to set withCredentials to send csrf token for Django 60 | return this.auth.post(`${config.api}/movies/user/update/`, formData, { withCredentials: true }); 61 | } 62 | 63 | editPassword(formData: any): Observable { 64 | return this.auth.post( 65 | `${config.api}/movies/user/update-password/`, 66 | formData, 67 | { withCredentials: true } 68 | ); 69 | } 70 | 71 | logout(): void { 72 | localStorage.removeItem('token'); 73 | // Cookie.delete('token', '/'); 74 | this.setUserData(); 75 | } 76 | 77 | isAuth(): boolean { 78 | const cookieToken = this.getToken(); 79 | if (cookieToken) { 80 | return !this.jwtHelper.isTokenExpired(cookieToken); 81 | } else { 82 | return false; 83 | } 84 | } 85 | 86 | getAuthDetails(): any { 87 | const cookieToken = this.getToken(); 88 | if (cookieToken) { 89 | return this.jwtHelper.decodeToken(cookieToken); 90 | } else { 91 | return false; 92 | } 93 | } 94 | 95 | setUserData(): void { 96 | this.user$.next(this.getAuthDetails()); 97 | this.user = this.getAuthDetails(); 98 | } 99 | 100 | private setToken(token: any): void { 101 | localStorage.setItem('token', token); 102 | } 103 | 104 | private getToken(): string { 105 | return localStorage.getItem('token'); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /angular2-client/src/app/movies/components/comments/comments.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Comments

5 | 6 | No comments yet, be the first to drop a line! 7 | 8 | 14 | 15 | 16 | 17 |

18 | {{ comment.username }} 19 | {{ comment.date | date:'medium' }} 20 | 21 | 22 | 23 |

24 |

{{comment.body}}

25 |
26 |
27 | 30 |
31 |
32 |
34 |
35 | 36 | 38 | 39 |
40 |
41 | 44 |
45 |
46 | -------------------------------------------------------------------------------- /angular2-client/src/app/movies/components/comments/comments.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/variables.scss'; 2 | 3 | .fw { 4 | width: 100%; 5 | } 6 | .content { 7 | background-color: map-get($colors, light-); 8 | padding: 1em; 9 | } 10 | .comments { 11 | border-top: 1px solid darken(map-get($colors, light-), 10%); 12 | margin-top: 2em; 13 | 14 | .comment-item { 15 | border-bottom: 1px solid map-get($colors, text-on-dark); 16 | 17 | &:last-child { 18 | border-bottom: none; 19 | } 20 | } 21 | 22 | .comment-datetime { 23 | color: darken(map-get($colors, light-), 30%); 24 | font-size: .8em; 25 | 26 | i { 27 | margin-right: .1em; 28 | margin-left: .5em; 29 | } 30 | } 31 | 32 | .remove-comment { 33 | cursor: pointer; 34 | margin-left: 2em; 35 | color: map-get($colors, error); 36 | } 37 | 38 | .no-comments { 39 | color: map-get($colors, dark); 40 | } 41 | 42 | /deep/ .my-pagination { 43 | .ngx-pagination { 44 | li:not(.disabled) { 45 | cursor: pointer; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /angular2-client/src/app/movies/components/comments/comments.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { FormGroup, FormBuilder, Validators } from '@angular/forms'; 3 | import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 4 | import * as _ from 'lodash'; 5 | 6 | import { 7 | HelpersService, 8 | MoviesService, 9 | UserService 10 | } from './../../../core/services'; 11 | 12 | @Component({ 13 | selector: 'comments', 14 | templateUrl: './comments.component.html', 15 | styleUrls: ['./comments.component.scss'] 16 | }) 17 | 18 | export class CommentsComponent implements OnInit { 19 | @Input() movieId: string; 20 | @Input() width: number; 21 | commentsData: { 22 | comments: any[], 23 | currentPage: number, 24 | totalPages: number, 25 | itemsPerPage: number 26 | }; 27 | commentForm: FormGroup; 28 | postingComment: boolean = false; 29 | commentsPageNr$ = new BehaviorSubject(1); 30 | 31 | constructor( 32 | private ms: MoviesService, 33 | private fb: FormBuilder, 34 | private helpers: HelpersService, 35 | public us: UserService 36 | ) { 37 | this.commentForm = this.fb.group({ 38 | comment: ['', Validators.required] 39 | }); 40 | } 41 | 42 | ngOnInit() { 43 | this.commentsPageNr$ 44 | .flatMap(page => this.ms.getComments(this.movieId, page)) 45 | .map(res => res.data) 46 | .map(comments => { 47 | return { 48 | comments: comments.comments.reverse(), 49 | currentPage: comments.current_page, 50 | totalPages: comments.total_pages, 51 | itemsPerPage: comments.items_per_page 52 | }; 53 | }).subscribe(commentsObj => { 54 | this.commentsData = commentsObj; 55 | }); 56 | } 57 | 58 | changePage(ev: number): void { 59 | this.commentsPageNr$.next(ev); 60 | } 61 | 62 | postComent(): void { 63 | if (!this.commentForm.valid) { 64 | return; 65 | } 66 | 67 | const commentBody = this.commentForm.value['comment']; 68 | this.postingComment = true; 69 | this.ms.postComment(this.movieId, commentBody) 70 | .subscribe(res => { 71 | this.postingComment = false; 72 | this.commentForm.reset(); 73 | 74 | // reload comments from page 1 75 | this.commentsPageNr$.next(1); 76 | }); 77 | } 78 | 79 | removeComment(id: string): void { 80 | this.ms.removeComment(id) 81 | .subscribe( 82 | res => { 83 | // reload current comments page 84 | this.commentsPageNr$.next(this.commentsData.currentPage); 85 | }, 86 | err => this.helpers.showMessage('The comment could not be removed') 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /angular2-client/src/app/movies/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './comments/comments.component'; 2 | export * from './movie-details/movie-details.component'; 3 | export * from './movie-list/movie-list.component'; 4 | -------------------------------------------------------------------------------- /angular2-client/src/app/movies/components/movie-details/movie-details.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ (movie$ | async).original_title }}

4 |
5 |
6 |
7 |
8 | 9 |
10 |
11 | {{ (movie$ | async).overview }} 12 | 13 |
14 |
15 |
16 | 17 | Rating {{ movieInternalDetails?.rating.avg }}/5 18 | 19 | Based on {{ movieInternalDetails?.rating.count }} votes 20 |
21 | 22 |
23 | No votes yet 24 |
25 |
26 | 30 | 31 |
32 |
    33 |
  • 34 |

    Genres

    35 | {{ (movie$ | async).genres | myGenresToText }} 36 |
  • 37 |
  • 38 |

    Rel. date

    39 | {{ (movie$ | async).release_date | date }} 40 |
  • 41 |
  • 42 |

    Runtime

    43 | {{ (movie$ | async).runtime }} mins 44 |
  • 45 |
46 | See on IMDB 47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /angular2-client/src/app/movies/components/movie-details/movie-details.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/variables.scss'; 2 | 3 | .backdrop-cont { 4 | height: 400px; 5 | position: relative; 6 | 7 | .backdrop { 8 | height: 400px; 9 | position: absolute; 10 | left: 0; 11 | width: 100%; 12 | background-size: cover; 13 | background-repeat: no-repeat; 14 | background-position: center 15%; 15 | 16 | h1 { 17 | margin-top: 2em; 18 | color: map-get($colors, text-on-dark); 19 | text-align: center; 20 | text-shadow: 0px 0px 9px rgba(0, 0, 0, 0.95); 21 | font-size: 3em; 22 | } 23 | } 24 | } 25 | 26 | .movie-content { 27 | position: relative; 28 | margin-top: -2em; 29 | 30 | .left-content { 31 | text-align: right; 32 | } 33 | 34 | .main { 35 | padding: 1em; 36 | background-color: map-get($colors, text-on-dark); 37 | } 38 | 39 | 40 | 41 | .specs { 42 | text-align: center; 43 | background-color: darken(map-get($colors, dark) , 5%); 44 | color: map-get($colors, text-on-dark); 45 | 46 | /deep/ .star-rating .star { 47 | cursor: pointer !important; 48 | } 49 | 50 | .rating { 51 | background-color: lighten(map-get($colors, dark), 10%); 52 | border-bottom: 1px solid lighten(map-get($colors, dark), 25%); 53 | margin-bottom: 1.5em; 54 | 55 | &.current-rating { 56 | color: map-get($colors, light-tint); 57 | padding-bottom: .5em; 58 | 59 | .rating-avg { 60 | display: block; 61 | padding: .5em; 62 | padding-bottom: 0; 63 | color: map-get($colors, text-on-dark); 64 | font-size: 1.3em; 65 | } 66 | } 67 | 68 | &.no-votes { 69 | padding: 1em; 70 | text-transform: uppercase; 71 | font-weight: bold; 72 | } 73 | } 74 | 75 | ul { 76 | list-style: none; 77 | padding: 1em; 78 | margin: 0; 79 | 80 | li { 81 | text-align: left; 82 | margin-bottom: .5em; 83 | 84 | h3 { 85 | margin: 0; 86 | } 87 | } 88 | } 89 | 90 | a { 91 | display: inline-block; 92 | padding: .25em; 93 | border: 1px solid rgba(map-get($colors, text-on-dark), .47); 94 | border-radius: 5px; 95 | color: map-get($colors, text-on-dark); 96 | text-decoration: none; 97 | margin-bottom: 1em; 98 | 99 | &:hover { 100 | background-color: map-get($colors, text-on-dark); 101 | color: map-get($colors, dark); 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /angular2-client/src/app/movies/components/movie-details/movie-details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import { IStarRatingOnClickEvent } from 'angular-star-rating/src/star-rating-struct'; 5 | 6 | import { 7 | HelpersService, 8 | MoviesService, 9 | UserService 10 | } from './../../../core/services'; 11 | 12 | @Component({ 13 | templateUrl: './movie-details.component.html', 14 | styleUrls: ['./movie-details.component.scss'] 15 | }) 16 | 17 | export class MovieDetailsComponent implements OnInit { 18 | movie$: Observable; 19 | movieInternalDetails: any; 20 | movieId: string; 21 | previousUserRating: number = 0; 22 | 23 | constructor( 24 | private route: ActivatedRoute, 25 | public us: UserService, 26 | private ms: MoviesService, 27 | private helpers: HelpersService 28 | ) {} 29 | 30 | ngOnInit() { 31 | this.movie$ = this.route.data.map((res: any) => res.movie); 32 | this.route.params.first().subscribe(par => { 33 | const movieId = par['id']; 34 | this.getInternalDetails(movieId); 35 | this.movieId = movieId; 36 | }); 37 | 38 | // check if user has already rated the movie 39 | this.ms.getMovieRating(this.movieId).first().subscribe(res => { 40 | this.previousUserRating = res.data.rating || 0; 41 | }); 42 | } 43 | 44 | rateMovie(ev: IStarRatingOnClickEvent): void { 45 | this.ms.rateMovie(this.movieId, ev.rating) 46 | .subscribe( 47 | res => { 48 | this.previousUserRating = ev.rating; 49 | this.getInternalDetails(this.movieId); 50 | }, 51 | err => this.helpers.showMessage('There was an error while rating the movie') 52 | ); 53 | } 54 | 55 | removeRating(): void { 56 | this.ms.removeRating(this.movieId) 57 | .subscribe( 58 | res => { 59 | this.previousUserRating = 0; 60 | this.getInternalDetails(this.movieId); 61 | }, 62 | err => this.helpers.showMessage('There was an error while removing the rating') 63 | ); 64 | } 65 | 66 | private getInternalDetails(id: string): void { 67 | this.ms.getMovieInternalDetails(id) 68 | .map(res => res.data) 69 | .subscribe(res => this.movieInternalDetails = res); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /angular2-client/src/app/movies/components/movie-list/movie-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Top 20 Movies in Theaters

4 |

since {{ moviesSinceDate | date }}

5 |
6 |
7 |
8 |
9 | Loading movies 10 |
11 |
12 |
13 | 19 | 20 |
21 | 22 |
23 | 24 |

{{ movie.title }}

25 |

{{ movie.overview }}

26 |
27 |
28 |
29 | {{ movie.custom_data?.comment_count || 0 }} 30 |
31 |
32 |
33 | {{ movie.vote_average }} 34 |
35 |
36 |
37 | {{ movie.custom_data?.avg_rating || 0 }} 38 |
39 |
40 |
41 | 42 | 43 | 44 |
45 |
46 | -------------------------------------------------------------------------------- /angular2-client/src/app/movies/components/movie-list/movie-list.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/variables.scss'; 2 | 3 | .title { 4 | text-align: center; 5 | color: map-get($colors, text-on-dark); 6 | 7 | h1 { 8 | margin-bottom: 0; 9 | } 10 | h3 { 11 | margin-top: 0; 12 | } 13 | } 14 | 15 | .movie-card { 16 | font-family: 'Lato', sans-serif; 17 | margin-bottom: 1em; 18 | text-align: center; 19 | 20 | .img-wrap { 21 | cursor: pointer; 22 | // border "hack" just to make the shadow appear over the image 23 | border: 2px solid transparent; 24 | -webkit-box-shadow: inset 0px 0px 0px 4px rgba(#fff, 1); 25 | -moz-box-shadow: inset 0px 0px 0px 4px rgba(#fff, 1); 26 | box-shadow: inset 0px 0px 0px 4px rgba(#fff, 1); 27 | } 28 | 29 | .specs { 30 | >div { 31 | text-align: center; 32 | color: map-get($colors, complementary); 33 | 34 | &:nth-child(2) { 35 | $border-style: 1px solid #e4e4e4; 36 | border-left: $border-style; 37 | border-right: $border-style; 38 | } 39 | } 40 | } 41 | 42 | button.mat-raised-button { 43 | border: 2px solid map-get($colors, complementary); 44 | border-radius: 5px; 45 | color: map-get($colors, complementary); 46 | font-weight: bold; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /angular2-client/src/app/movies/components/movie-list/movie-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs/Observable'; 3 | import { Router } from '@angular/router'; 4 | 5 | import { MoviesService } from './../../../core/services'; 6 | 7 | @Component({ 8 | selector: 'movie-list', 9 | templateUrl: './movie-list.component.html', 10 | styleUrls: ['./movie-list.component.scss'] 11 | }) 12 | 13 | export class MovieListComponent implements OnInit { 14 | public topMovies$: Observable; 15 | public moviesSinceDate: Date | any; 16 | public spinnerStyles: any; 17 | 18 | constructor( 19 | private movies: MoviesService, 20 | private router: Router 21 | ) { } 22 | 23 | ngOnInit() { 24 | this.topMovies$ = this.movies.getTopMovies(); 25 | this.moviesSinceDate = this.movies.moviesFromDate(); 26 | 27 | // custom styles to fit loader to card container 28 | this.spinnerStyles = { 29 | margin: '-24px -24px 16px -24px' 30 | }; 31 | } 32 | 33 | getMovieDetails(movie: any): void { 34 | movie.detailsLoading = true; 35 | const title = encodeURIComponent(movie.original_title.toLowerCase().replace(/ /g, '-')); 36 | this.router.navigate([`/details/${movie.id}/${title}`]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /angular2-client/src/app/movies/movies.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { MovieDetailResolver } from './services/movie-detail-resolver.service'; 4 | import { SharedModule } from './../shared/shared.module'; 5 | import { MoviesRoutingModule } from './movies.routing'; 6 | import { 7 | CommentsComponent, 8 | MovieDetailsComponent, 9 | MovieListComponent 10 | } from './components'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | SharedModule, 15 | MoviesRoutingModule 16 | ], 17 | declarations: [ 18 | MovieListComponent, 19 | MovieDetailsComponent, 20 | CommentsComponent 21 | ], 22 | providers: [ 23 | MovieDetailResolver 24 | ] 25 | }) 26 | export class MoviesModule { } 27 | -------------------------------------------------------------------------------- /angular2-client/src/app/movies/movies.routing.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { MovieDetailResolver } from './services/movie-detail-resolver.service'; 5 | import { 6 | MovieDetailsComponent, 7 | MovieListComponent 8 | } from './components'; 9 | 10 | const routes: Routes = [ 11 | { path: '', component: MovieListComponent }, 12 | { 13 | path: 'details/:id/:moviename', 14 | component: MovieDetailsComponent, 15 | resolve: { movie: MovieDetailResolver } 16 | } 17 | ]; 18 | 19 | @NgModule({ 20 | imports: [RouterModule.forChild(routes)], 21 | exports: [RouterModule], 22 | }) 23 | export class MoviesRoutingModule { } 24 | -------------------------------------------------------------------------------- /angular2-client/src/app/movies/services/movie-detail-resolver.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { MoviesService } from './../../core/services'; 6 | 7 | @Injectable() 8 | export class MovieDetailResolver implements Resolve { 9 | 10 | constructor( 11 | private movies: MoviesService, 12 | private router: Router 13 | ) { } 14 | 15 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 16 | const id = route.params['id']; 17 | 18 | return this.movies.getMovieDetails(id).first() 19 | .catch((err: any) => { 20 | const errCode = err.json().status_code; 21 | let message = ''; 22 | switch (errCode) { 23 | case 7: 24 | message = 'There was an error with The Movie Database API'; 25 | break; 26 | case 34: 27 | message = `The movie with id ${id} could not be found`; 28 | break; 29 | default: 30 | message = `There was an error while searching for movie with id ${id}`; 31 | break; 32 | } 33 | localStorage.setItem('error', message); 34 | this.router.navigate(['/']); 35 | return Observable.of(null); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /angular2-client/src/app/services/startup.service.ts: -------------------------------------------------------------------------------- 1 | import { Http, RequestOptions } from '@angular/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { config } from './../../config'; 6 | 7 | @Injectable() 8 | export class StartupService { 9 | 10 | constructor( 11 | private http: Http 12 | ) { } 13 | 14 | load(): Promise { 15 | return this.getCsrf() 16 | .toPromise() 17 | .catch(() => Promise.resolve()); 18 | } 19 | 20 | getCsrf(): Observable { 21 | const options = new RequestOptions({ withCredentials: true }); 22 | const csrfToken = localStorage.getItem('csrftoken'); 23 | if (!csrfToken) { 24 | return this.http.get(`${config.api}/movies/auth/csrf`, options) 25 | .first() 26 | .map(res => res.json()) 27 | .do(res => localStorage.setItem('csrftoken', res.data)); 28 | } 29 | return Observable.of(csrfToken); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /angular2-client/src/app/shared/components/errorbar/errorbar.component.scss: -------------------------------------------------------------------------------- 1 | .error-bar { 2 | text-align: center; 3 | margin-top: 1em; 4 | } 5 | -------------------------------------------------------------------------------- /angular2-client/src/app/shared/components/errorbar/errorbar.component.ts: -------------------------------------------------------------------------------- 1 | import { Router, NavigationEnd } from '@angular/router'; 2 | import { Component, OnInit, OnDestroy } from '@angular/core'; 3 | import { Subscription } from 'rxjs/Subscription'; 4 | 5 | @Component({ 6 | selector: 'error-bar', 7 | styleUrls: ['./errorbar.component.scss'], 8 | template: ` 9 |
10 |
11 | {{ error }} 12 |
13 |
14 | ` 15 | }) 16 | 17 | export class ErrorBarComponent implements OnInit, OnDestroy { 18 | error: string; 19 | routerSub: Subscription; 20 | 21 | constructor( 22 | private router: Router 23 | ) { } 24 | 25 | ngOnInit() { 26 | this.routerSub = this.router.events.subscribe(res => { 27 | // check, show and remove error after every navigation event ends 28 | if (res instanceof NavigationEnd) { 29 | this.error = localStorage.getItem('error'); 30 | localStorage.removeItem('error'); 31 | } 32 | }); 33 | } 34 | 35 | ngOnDestroy() { 36 | this.routerSub.unsubscribe(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /angular2-client/src/app/shared/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errorbar/errorbar.component'; 2 | export * from './loading-spinner/loading-spinner.component'; 3 | export * from './toolbar/toolbar.component'; 4 | -------------------------------------------------------------------------------- /angular2-client/src/app/shared/components/loading-spinner/loading-spinner.component.scss: -------------------------------------------------------------------------------- 1 | $loadingText: #fff; 2 | 3 | .loading-wrap { 4 | position: absolute; 5 | width: 100%; 6 | height: 100%; 7 | z-index: 1; 8 | 9 | &.backdrop { 10 | background-color: rgba(0, 0, 0, 0.70); 11 | } 12 | 13 | .top { 14 | position: relative; 15 | top: 30% 16 | } 17 | 18 | .loading-text { 19 | color: $loadingText; 20 | } 21 | } 22 | 23 | // source: http://tobiasahlin.com/spinkit/ 24 | .sk-fading-circle { 25 | margin: 0 auto; 26 | width: 40px; 27 | height: 40px; 28 | position: relative; 29 | // top: 30%; 30 | } 31 | 32 | .sk-fading-circle .sk-circle { 33 | width: 100%; 34 | height: 100%; 35 | position: absolute; 36 | left: 0; 37 | top: 0; 38 | } 39 | 40 | .sk-fading-circle .sk-circle:before { 41 | content: ''; 42 | display: block; 43 | margin: 0 auto; 44 | width: 15%; 45 | height: 15%; 46 | background-color: $loadingText; 47 | border-radius: 100%; 48 | -webkit-animation: sk-circleFadeDelay 1.2s infinite ease-in-out both; 49 | animation: sk-circleFadeDelay 1.2s infinite ease-in-out both; 50 | } 51 | .sk-fading-circle .sk-circle2 { 52 | -webkit-transform: rotate(30deg); 53 | -ms-transform: rotate(30deg); 54 | transform: rotate(30deg); 55 | } 56 | .sk-fading-circle .sk-circle3 { 57 | -webkit-transform: rotate(60deg); 58 | -ms-transform: rotate(60deg); 59 | transform: rotate(60deg); 60 | } 61 | .sk-fading-circle .sk-circle4 { 62 | -webkit-transform: rotate(90deg); 63 | -ms-transform: rotate(90deg); 64 | transform: rotate(90deg); 65 | } 66 | .sk-fading-circle .sk-circle5 { 67 | -webkit-transform: rotate(120deg); 68 | -ms-transform: rotate(120deg); 69 | transform: rotate(120deg); 70 | } 71 | .sk-fading-circle .sk-circle6 { 72 | -webkit-transform: rotate(150deg); 73 | -ms-transform: rotate(150deg); 74 | transform: rotate(150deg); 75 | } 76 | .sk-fading-circle .sk-circle7 { 77 | -webkit-transform: rotate(180deg); 78 | -ms-transform: rotate(180deg); 79 | transform: rotate(180deg); 80 | } 81 | .sk-fading-circle .sk-circle8 { 82 | -webkit-transform: rotate(210deg); 83 | -ms-transform: rotate(210deg); 84 | transform: rotate(210deg); 85 | } 86 | .sk-fading-circle .sk-circle9 { 87 | -webkit-transform: rotate(240deg); 88 | -ms-transform: rotate(240deg); 89 | transform: rotate(240deg); 90 | } 91 | .sk-fading-circle .sk-circle10 { 92 | -webkit-transform: rotate(270deg); 93 | -ms-transform: rotate(270deg); 94 | transform: rotate(270deg); 95 | } 96 | .sk-fading-circle .sk-circle11 { 97 | -webkit-transform: rotate(300deg); 98 | -ms-transform: rotate(300deg); 99 | transform: rotate(300deg); 100 | } 101 | .sk-fading-circle .sk-circle12 { 102 | -webkit-transform: rotate(330deg); 103 | -ms-transform: rotate(330deg); 104 | transform: rotate(330deg); 105 | } 106 | .sk-fading-circle .sk-circle2:before { 107 | -webkit-animation-delay: -1.1s; 108 | animation-delay: -1.1s; 109 | } 110 | .sk-fading-circle .sk-circle3:before { 111 | -webkit-animation-delay: -1s; 112 | animation-delay: -1s; 113 | } 114 | .sk-fading-circle .sk-circle4:before { 115 | -webkit-animation-delay: -0.9s; 116 | animation-delay: -0.9s; 117 | } 118 | .sk-fading-circle .sk-circle5:before { 119 | -webkit-animation-delay: -0.8s; 120 | animation-delay: -0.8s; 121 | } 122 | .sk-fading-circle .sk-circle6:before { 123 | -webkit-animation-delay: -0.7s; 124 | animation-delay: -0.7s; 125 | } 126 | .sk-fading-circle .sk-circle7:before { 127 | -webkit-animation-delay: -0.6s; 128 | animation-delay: -0.6s; 129 | } 130 | .sk-fading-circle .sk-circle8:before { 131 | -webkit-animation-delay: -0.5s; 132 | animation-delay: -0.5s; 133 | } 134 | .sk-fading-circle .sk-circle9:before { 135 | -webkit-animation-delay: -0.4s; 136 | animation-delay: -0.4s; 137 | } 138 | .sk-fading-circle .sk-circle10:before { 139 | -webkit-animation-delay: -0.3s; 140 | animation-delay: -0.3s; 141 | } 142 | .sk-fading-circle .sk-circle11:before { 143 | -webkit-animation-delay: -0.2s; 144 | animation-delay: -0.2s; 145 | } 146 | .sk-fading-circle .sk-circle12:before { 147 | -webkit-animation-delay: -0.1s; 148 | animation-delay: -0.1s; 149 | } 150 | 151 | @-webkit-keyframes sk-circleFadeDelay { 152 | 0%, 39%, 100% { opacity: 0; } 153 | 40% { opacity: 1; } 154 | } 155 | 156 | @keyframes sk-circleFadeDelay { 157 | 0%, 39%, 100% { opacity: 0; } 158 | 40% { opacity: 1; } 159 | } 160 | -------------------------------------------------------------------------------- /angular2-client/src/app/shared/components/loading-spinner/loading-spinner.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'loading-spinner', 5 | styleUrls: ['./loading-spinner.component.scss'], 6 | template: ` 7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 |
25 |
26 | `, 27 | }) 28 | 29 | export class LoadingSpinnerComponent { 30 | @Input() customStyles: any; 31 | @Input() topMargin: boolean = true; 32 | @Input() backdrop: boolean = true; 33 | } 34 | -------------------------------------------------------------------------------- /angular2-client/src/app/shared/components/toolbar/toolbar.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | Angular2 Django | Movies 6 | 7 | 8 | 9 | 10 | Hello 11 | 12 | 16 | 20 | 21 | 22 | 23 | Register 24 | Login 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 | -------------------------------------------------------------------------------- /angular2-client/src/app/shared/components/toolbar/toolbar.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/variables.scss'; 2 | 3 | .menu-space-filler { 4 | flex: 1 1 auto; 5 | } 6 | 7 | .mat-toolbar { 8 | background-color: map-get($colors, dark-); 9 | 10 | a { 11 | text-decoration: none; 12 | color: map-get($colors, text-on-dark); 13 | cursor: pointer; 14 | 15 | &.auth-link { 16 | border: 2px solid map-get($colors, light-tint); 17 | border-radius: 5px; 18 | padding: .5em; 19 | margin-right: 1em; 20 | font-size: .8em; 21 | 22 | &:hover { 23 | background-color: map-get($colors, light-tint); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /angular2-client/src/app/shared/components/toolbar/toolbar.component.ts: -------------------------------------------------------------------------------- 1 | import { Router, NavigationEnd } from '@angular/router'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { UserService } from './../../../core/services'; 4 | 5 | @Component({ 6 | selector: 'toolbar', 7 | styleUrls: ['./toolbar.component.scss'], 8 | templateUrl: './toolbar.component.html' 9 | }) 10 | 11 | export class ToolbarComponent implements OnInit { 12 | 13 | constructor( 14 | public us: UserService, 15 | private router: Router 16 | ) { } 17 | 18 | ngOnInit() { 19 | // after each navigation event ends, check if auth token is not expired 20 | this.router.events.subscribe(ev => { 21 | if (ev instanceof NavigationEnd && !this.us.isAuth()) { 22 | this.us.logout(); 23 | } 24 | }); 25 | } 26 | 27 | logout(): void { 28 | this.us.logout(); 29 | this.router.navigate(['/']); 30 | } 31 | 32 | goToEditProfile(): void { 33 | this.router.navigate(['/user/edit']); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /angular2-client/src/app/shared/pipes/errors-to-list.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import * as _ from 'lodash'; 3 | 4 | @Pipe({ 5 | name: 'myErrorsToList' 6 | }) 7 | 8 | export class ErrorsToListPipe implements PipeTransform { 9 | transform(value: {[key: string]: string | boolean}): any[] { 10 | return _.toArray(value); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /angular2-client/src/app/shared/pipes/genres-to-text.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import * as _ from 'lodash'; 3 | 4 | @Pipe({ 5 | name: 'myGenresToText' 6 | }) 7 | 8 | export class GenresToTextPipe implements PipeTransform { 9 | transform(genres: any[]): string { 10 | return _.map(genres, 'name').join(', '); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /angular2-client/src/app/shared/pipes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors-to-list.pipe'; 2 | export * from './genres-to-text.pipe'; 3 | -------------------------------------------------------------------------------- /angular2-client/src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { RouterModule } from '@angular/router'; 2 | import { NgModule } from '@angular/core'; 3 | import { FlexLayoutModule } from '@angular/flex-layout'; 4 | import { CommonModule } from '@angular/common'; 5 | import { StarRatingModule } from 'angular-star-rating'; 6 | import { ReactiveFormsModule, FormsModule } from '@angular/forms'; 7 | import { NgxPaginationModule } from 'ngx-pagination'; 8 | import { 9 | MdToolbarModule, 10 | MdSnackBarModule, 11 | MdCardModule, 12 | MdListModule, 13 | MdInputModule, 14 | MdButtonModule, 15 | MdMenuModule, 16 | MdProgressSpinnerModule 17 | } from '@angular/material'; 18 | 19 | import { 20 | ErrorsToListPipe, 21 | GenresToTextPipe 22 | } from './pipes'; 23 | import { 24 | ErrorBarComponent, 25 | LoadingSpinnerComponent, 26 | ToolbarComponent 27 | } from './components'; 28 | 29 | const ANGULAR_MATERIAL_COMPONENTS = [ 30 | MdCardModule, 31 | MdToolbarModule, 32 | MdSnackBarModule, 33 | MdListModule, 34 | MdInputModule, 35 | MdButtonModule, 36 | MdMenuModule, 37 | MdProgressSpinnerModule 38 | ]; 39 | 40 | const COMPONENTS = [ 41 | ErrorBarComponent, 42 | LoadingSpinnerComponent, 43 | ToolbarComponent 44 | ]; 45 | 46 | const PIPES = [ 47 | ErrorsToListPipe, 48 | GenresToTextPipe 49 | ]; 50 | 51 | @NgModule({ 52 | imports: [ 53 | RouterModule, 54 | CommonModule, 55 | ...ANGULAR_MATERIAL_COMPONENTS, 56 | FlexLayoutModule, 57 | ], 58 | exports: [ 59 | RouterModule, 60 | CommonModule, 61 | ...ANGULAR_MATERIAL_COMPONENTS, 62 | FlexLayoutModule, 63 | StarRatingModule, 64 | FormsModule, 65 | ReactiveFormsModule, 66 | LoadingSpinnerComponent, 67 | NgxPaginationModule, 68 | ...COMPONENTS, 69 | ...PIPES 70 | ], 71 | declarations: [ 72 | ...COMPONENTS, 73 | ...PIPES 74 | ], 75 | }) 76 | export class SharedModule { } 77 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './password-form/password-form.component'; 2 | export * from './input-hint.component'; 3 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/components/input-hint.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | Input, 4 | ElementRef, 5 | Renderer 6 | } from '@angular/core'; 7 | 8 | @Directive({ selector: '[inputHint]' }) 9 | export class HintDirective { 10 | mainClass = { 11 | valid: 'valid', 12 | invalid: 'invalid' 13 | }; 14 | 15 | constructor( 16 | private el: ElementRef, 17 | private renderer: Renderer 18 | ) { } 19 | 20 | @Input() set inputHint(isValid: boolean | {[key: string]: boolean}) { 21 | // get current text 22 | const currentText = this.el.nativeElement.innerText; 23 | 24 | // clear current text and remove eventual valid clas 25 | this.renderer.setElementProperty(this.el.nativeElement, 'innerText', ''); 26 | this.renderer.setElementClass(this.el.nativeElement, this.mainClass.valid, false); 27 | 28 | // create icon and set styles 29 | const icon = this.renderer.createElement(this.el.nativeElement, 'i'); 30 | this.renderer.setElementClass(icon, 'fa', true); 31 | let resultClass = []; 32 | if (isValid) { 33 | resultClass = ['fa-check', this.mainClass.valid]; 34 | } else { 35 | resultClass = ['fa-times', this.mainClass.invalid]; 36 | } 37 | this.renderer.setElementClass(icon, resultClass[0], true); 38 | this.renderer.setElementClass(this.el.nativeElement, resultClass[1], true); 39 | 40 | // append previous text 41 | this.renderer.createText(this.el.nativeElement, currentText); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/components/password-form/password-form.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | Password must be equal 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 | Password must: 20 |
    21 |
  • 22 | Be at least 8 chars long 23 |
  • 24 |
  • 25 | Have at least one special char 26 |
  • 27 |
  • 28 | Have at least one capital letter 29 |
  • 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/components/password-form/password-form.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/variables.scss'; 2 | 3 | .password-hints { 4 | text-align: left; 5 | padding-left: 1em; 6 | 7 | ul { 8 | list-style: none; 9 | padding-left: 0; 10 | margin-top: 0; 11 | text-align: left; 12 | color: map-get($colors, light-tint); 13 | 14 | li { 15 | margin-bottom: .25em; 16 | &.valid { 17 | color: map-get($colors, success); 18 | } 19 | i { 20 | margin-right: .3em; 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/components/password-form/password-form.component.ts: -------------------------------------------------------------------------------- 1 | import { FormGroup, FormBuilder, Validators, FormControl } from '@angular/forms'; 2 | import { Component, Input, OnInit } from '@angular/core'; 3 | 4 | import { 5 | emailValidator, 6 | passwordMatchValidator, 7 | passwordValidator 8 | } from '../../helpers/form-validators'; 9 | 10 | @Component({ 11 | selector: 'password-form', 12 | styleUrls: ['./password-form.component.scss'], 13 | templateUrl: './password-form.component.html' 14 | }) 15 | 16 | export class PasswordFormComponent implements OnInit { 17 | @Input() sourceForm: FormGroup; 18 | @Input() isEdit = false; 19 | passwords: FormGroup; 20 | 21 | constructor( 22 | private fb: FormBuilder 23 | ) { } 24 | 25 | ngOnInit() { 26 | this.passwords = this.fb.group({ 27 | password: ['', [Validators.required, passwordValidator()]], 28 | passwordConfirm: ['', Validators.required], 29 | }, { validator: passwordMatchValidator() }); 30 | 31 | this.sourceForm.setControl('passwords', this.passwords); 32 | 33 | // add oldPassword control on edit form 34 | if (this.isEdit) { 35 | const control: FormControl = new FormControl('', Validators.required); 36 | this.passwords.addControl('oldPassword', control); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/helpers/form-validators.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl, ValidatorFn } from '@angular/forms'; 2 | 3 | export function emailValidator(): ValidatorFn { 4 | return (control: AbstractControl): {[key: string]: any} => { 5 | const email = control.value; 6 | const emailRe = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 7 | const valid = emailRe.test(email); 8 | return valid ? null : { emailInvalid: true }; 9 | }; 10 | } 11 | 12 | export function passwordMatchValidator(): ValidatorFn { 13 | return (control: AbstractControl): {[key: string]: any} => { 14 | const password = control.get('password').value; 15 | const passwordConfirm = control.get('passwordConfirm').value; 16 | const valid = (password === passwordConfirm); 17 | 18 | return valid ? null : { passwordMismatch: true }; 19 | }; 20 | } 21 | 22 | export function passwordValidator(): ValidatorFn { 23 | return (control: AbstractControl): {[key: string]: any} => { 24 | const password = control.value; 25 | 26 | const errors: any = {}; 27 | 28 | if (password.length < 7) { 29 | errors['tooShort'] = true; 30 | } 31 | if (!/[A-Z]+/.test(password)) { 32 | errors['noCapitalLetter'] = true; 33 | } 34 | if (/^[a-zA-Z0-9]+$/.test(password) || password.length === 0) { 35 | errors['noSpecialChars'] = true; 36 | } 37 | 38 | const valid = Object.keys(errors).length === 0; 39 | return valid ? null : errors; 40 | }; 41 | } 42 | 43 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/pages/auth-home/auth-home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Register 4 |
5 |
6 | Login 7 |
8 |
9 | 10 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/pages/auth-home/auth-home.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/variables.scss'; 2 | 3 | .wrap { 4 | margin-top: 5em; 5 | 6 | a { 7 | display: block; 8 | text-align: center; 9 | color: map-get($colors, light-tint); 10 | font-size: 1.7em; 11 | font-weight: bold; 12 | text-decoration: none; 13 | padding: .5em; 14 | border-bottom: 1px solid map-get($colors, dark); 15 | 16 | &.active { 17 | border-bottom: 10px solid map-get($colors, text-on-dark); 18 | color: map-get($colors, text-on-dark); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/pages/auth-home/auth-home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | templateUrl: './auth-home.component.html', 5 | styleUrls: ['./auth-home.component.scss'] 6 | }) 7 | 8 | export class AuthHomeComponent { 9 | } 10 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/pages/edit/edit.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Update user details

4 |
5 |
6 |
7 |
8 |
9 | There was an error during form submission 10 |
11 |
12 | User profile was updated succesfully 13 |
14 |
15 | 16 | 17 | 18 | 19 | 20 | Email is not valid 21 | 22 |
23 |
24 | 28 |
29 |
30 |
31 |
32 | There was an error during form submission 33 |
34 |
35 | The old password you provided is wrong 36 |
37 |
38 | Password was updated succesfully 39 |
40 |
41 | 42 |
43 |
44 | 48 |
49 |
50 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/pages/edit/edit.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/variables.scss'; 2 | 3 | h1 { 4 | text-align: center; 5 | color: map-get($colors, text-on-dark); 6 | } 7 | 8 | .cont { 9 | form:first-child { 10 | padding-bottom: 1em; 11 | border-bottom: 2px dotted map-get($colors, dark); 12 | margin-bottom: 1em; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/pages/edit/edit.component.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs/Observable'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 4 | import { Component } from '@angular/core'; 5 | 6 | import { UserService } from './../../../core/services'; 7 | import { AsyncFormValidatorsService } from './../../services/async-form-validators.service'; 8 | import { 9 | emailValidator, 10 | passwordMatchValidator, 11 | passwordValidator 12 | } from '../../helpers/form-validators'; 13 | 14 | @Component({ 15 | styleUrls: ['./edit.component.scss'], 16 | templateUrl: './edit.component.html' 17 | }) 18 | 19 | export class EditComponent { 20 | updateProfileForm: FormGroup; 21 | passwordForm: FormGroup; 22 | success = { 23 | profile: false, 24 | password: false 25 | }; 26 | 27 | constructor( 28 | private fb: FormBuilder, 29 | private us: UserService 30 | ) { 31 | this.setupForm(); 32 | } 33 | 34 | setupForm(): void { 35 | this.updateProfileForm = this.fb.group({ 36 | username: [ 37 | { value: '', disabled: true }, 38 | Validators.required 39 | ], 40 | email: ['', [Validators.required, emailValidator()]] 41 | }); 42 | 43 | this.us.user$.first() 44 | .subscribe(user => { 45 | this.updateProfileForm.patchValue({ 46 | username: user.username, 47 | email: user.email 48 | }); 49 | }); 50 | 51 | // create empty formGroup that will be populated by password-form component 52 | this.passwordForm = this.fb.group({}); 53 | } 54 | 55 | updateProfile(): void { 56 | this.success.profile = false; 57 | this.updateProfileForm.markAsPending(); 58 | const formData = { 59 | username: this.updateProfileForm.value.username, 60 | email: this.updateProfileForm.value.email 61 | }; 62 | 63 | this.us.editProfile(formData) 64 | .first() 65 | .subscribe( 66 | res => { 67 | this.success.profile = true; 68 | this.us.setUserData(); 69 | // by using setErrors I can revert the markAsPending state 70 | this.updateProfileForm.setErrors({}); 71 | }, 72 | err => { 73 | this.updateProfileForm.setErrors({ 74 | formError: true 75 | }); 76 | } 77 | ); 78 | } 79 | 80 | updatePassword(): void { 81 | this.success.password = false; 82 | this.passwordForm.markAsPending(); 83 | const formData = { 84 | oldPassword: this.passwordForm.value.passwords.oldPassword, 85 | password: this.passwordForm.value.passwords.password 86 | }; 87 | 88 | this.us.editPassword(formData) 89 | .first() 90 | .catch((err: any) => { 91 | let formError; 92 | if (err.status === 401) { 93 | formError = { wrongPassword: true }; 94 | } else { 95 | formError = { formError: true }; 96 | } 97 | this.passwordForm.setErrors(formError); 98 | 99 | return Observable.throw(new Error(err)); 100 | }) 101 | .subscribe( 102 | res => { 103 | this.success.password = true; 104 | this.us.setUserData(); 105 | // by using setErrors I can revert the markAsPending state 106 | this.passwordForm.setErrors({}); 107 | } 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth-home/auth-home.component'; 2 | export * from './edit/edit.component'; 3 | export * from './login/login.component'; 4 | export * from './register/register.component'; 5 | export * from './user-home/user-home.component'; 6 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/pages/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
    6 |
  • 7 | {{ error }} 8 |
  • 9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | 23 |
24 |
25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/pages/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Router } from '@angular/router'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Component } from '@angular/core'; 4 | import { Observable } from 'rxjs/Observable'; 5 | 6 | import { UserService } from './../../../core/services'; 7 | 8 | @Component({ 9 | templateUrl: './login.component.html' 10 | }) 11 | 12 | export class LoginComponent { 13 | loginForm: FormGroup; 14 | 15 | constructor( 16 | private fb: FormBuilder, 17 | private us: UserService, 18 | private router: Router 19 | ) { 20 | this.loginForm = this.fb.group({ 21 | username: ['', Validators.required], 22 | password: ['', Validators.required] 23 | }); 24 | } 25 | 26 | submitForm(): void { 27 | // TODO: 28 | // https://stackoverflow.com/questions/44631754/how-to-revert-markaspending-in-angular-2-form 29 | this.loginForm.markAsPending(); 30 | const formData = { 31 | username: this.loginForm.value.username, 32 | password: this.loginForm.value.password, 33 | }; 34 | this.us.login(formData) 35 | .first() 36 | .catch((err: any) => { 37 | let formError; 38 | if (err.status === 401) { 39 | formError = { wrongPassword: 'Username or password is wrong' }; 40 | } else { 41 | formError = { formError: 'There was an error during login' }; 42 | } 43 | this.loginForm.setErrors(formError); 44 | 45 | return Observable.throw(new Error(err)); 46 | }).subscribe( 47 | res => { 48 | this.us.setUserData(); 49 | this.router.navigate(['/']); 50 | } 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/pages/register/register.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | There was an error during form submission 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | This username is already taken 13 | 14 | 15 | 16 | Email is not valid 17 | 18 | 19 |
20 |
21 | 25 |
26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/pages/register/register.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/variables.scss'; 2 | 3 | .input-icon { 4 | position: absolute; 5 | right: 0; 6 | 7 | &.loading { 8 | color: map-get($colors, light-tint); 9 | } 10 | &.valid { 11 | color: map-get($colors, success); 12 | } 13 | &.invalid { 14 | color: map-get($colors, error); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/pages/register/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Router } from '@angular/router'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Component } from '@angular/core'; 4 | 5 | import { UserService } from './../../../core/services'; 6 | import { AsyncFormValidatorsService } from './../../services/async-form-validators.service'; 7 | import { 8 | emailValidator, 9 | passwordMatchValidator, 10 | passwordValidator 11 | } from '../../helpers/form-validators'; 12 | 13 | @Component({ 14 | styleUrls: ['./register.component.scss'], 15 | templateUrl: './register.component.html' 16 | }) 17 | 18 | export class RegisterComponent { 19 | formError: boolean = false; 20 | formLoading: boolean = false; 21 | registerForm: FormGroup; 22 | 23 | constructor( 24 | private fb: FormBuilder, 25 | private asyncValidators: AsyncFormValidatorsService, 26 | private us: UserService, 27 | private router: Router 28 | ) { 29 | this.setupForm(); 30 | } 31 | 32 | setupForm(): void { 33 | this.registerForm = this.fb.group({ 34 | username: [ 35 | '', 36 | Validators.required, 37 | this.asyncValidators.usernameUnique() 38 | ], 39 | email: ['', [Validators.required, emailValidator()]] 40 | }); 41 | } 42 | 43 | submitForm(): void { 44 | this.formLoading = true; 45 | const formData = { 46 | username: this.registerForm.value.username, 47 | email: this.registerForm.value.email, 48 | password: this.registerForm.value.passwords.password 49 | }; 50 | 51 | this.us.register(formData) 52 | .first() 53 | .finally(() => this.formLoading = false) 54 | .subscribe( 55 | res => { 56 | this.us.setUserData(); 57 | this.router.navigate(['/']); 58 | }, 59 | err => { 60 | this.formError = true; 61 | } 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/pages/user-home/user-home.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/variables.scss'; 2 | 3 | /deep/ .cont { 4 | text-align: center; 5 | background-color: map-get($colors, text-on-dark); 6 | padding: 1.5em; 7 | } 8 | /deep/ button[type="submit"] { 9 | width: 100%; 10 | background-color: map-get($colors, vivid); 11 | border: 2px solid map-get($colors, text-on-dark); 12 | color: map-get($colors, dark); 13 | font-weight: bold; 14 | text-transform: uppercase; 15 | } 16 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/pages/user-home/user-home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | styleUrls: ['./user-home.component.scss'], 5 | template: ` 6 |
7 |
8 | 9 |
10 |
11 | ` 12 | }) 13 | 14 | export class UserHomeComponent { } 15 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/services/async-form-validators.service.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs/Observable'; 2 | import { AbstractControl, ValidatorFn } from '@angular/forms'; 3 | import { Injectable } from '@angular/core'; 4 | 5 | import { UserService } from './../../core/services'; 6 | 7 | @Injectable() 8 | export class AsyncFormValidatorsService { 9 | timeout: any; 10 | debounceTime: number = 1000; 11 | 12 | constructor( 13 | private us: UserService 14 | ) { } 15 | 16 | usernameUnique(): ValidatorFn { 17 | return (c: AbstractControl): Observable<{[key: string]: any}> => { 18 | const username = c.value; 19 | // hack to achieve debounce with async form validator 20 | clearTimeout(this.timeout); 21 | return Observable.create((observer: any) => { 22 | this.timeout = setTimeout(() => { 23 | this.us.usernameIsUnique(username) 24 | .map(isUnique => isUnique ? null : { isNotUnique: true }) 25 | .subscribe( 26 | res => { observer.next(res); observer.complete(); } 27 | ); 28 | }, this.debounceTime); 29 | }); 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { AsyncFormValidatorsService } from './services/async-form-validators.service'; 4 | import { SharedModule } from './../shared/shared.module'; 5 | import { UserRoutingModule } from './user.routing'; 6 | import { 7 | EditComponent, 8 | AuthHomeComponent, 9 | LoginComponent, 10 | UserHomeComponent, 11 | RegisterComponent, 12 | } from './pages'; 13 | import { 14 | PasswordFormComponent, 15 | HintDirective 16 | } from './components'; 17 | 18 | @NgModule({ 19 | imports: [ 20 | SharedModule, 21 | UserRoutingModule 22 | ], 23 | declarations: [ 24 | LoginComponent, 25 | RegisterComponent, 26 | UserHomeComponent, 27 | HintDirective, 28 | AuthHomeComponent, 29 | PasswordFormComponent, 30 | EditComponent 31 | ], 32 | providers: [ 33 | AsyncFormValidatorsService 34 | ] 35 | }) 36 | export class UserModule { } 37 | -------------------------------------------------------------------------------- /angular2-client/src/app/user/user.routing.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { NonAuthGuard } from './../core/services/non-auth-guard.service'; 5 | import { AuthGuard } from './../core/services/auth-guard.service'; 6 | import { 7 | AuthHomeComponent, 8 | EditComponent, 9 | LoginComponent, 10 | RegisterComponent, 11 | UserHomeComponent 12 | } from './pages'; 13 | 14 | const routes: Routes = [ 15 | { path: '', component: UserHomeComponent, children: [ 16 | { path: '', component: AuthHomeComponent, canActivateChild: [NonAuthGuard], children: [ 17 | { path: '', redirectTo: 'register' }, 18 | { path: 'login', component: LoginComponent }, 19 | { path: 'register', component: RegisterComponent } 20 | ]}, 21 | { path: 'edit', component: EditComponent, canActivate: [AuthGuard]} 22 | ]} 23 | ]; 24 | 25 | @NgModule({ 26 | imports: [RouterModule.forChild(routes)], 27 | exports: [RouterModule], 28 | }) 29 | export class UserRoutingModule { } 30 | -------------------------------------------------------------------------------- /angular2-client/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damnko/angular2-django-movies/12bb03f2235e271cbdf33025d52e6feedba4b71f/angular2-client/src/assets/favicon.ico -------------------------------------------------------------------------------- /angular2-client/src/assets/images/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damnko/angular2-django-movies/12bb03f2235e271cbdf33025d52e6feedba4b71f/angular2-client/src/assets/images/preview.gif -------------------------------------------------------------------------------- /angular2-client/src/assets/images/star-rating.icons.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | star-empty 7 | 9 | 10 | 11 | 12 | star-half 13 | 15 | 16 | 17 | 18 | star-filled 19 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /angular2-client/src/config.ts: -------------------------------------------------------------------------------- 1 | const isDev = process.env.NODE_ENV === 'development'; 2 | const isLocal = process.env.SERVER_LOCATION === 'local'; // set via .env file 3 | 4 | const apiEndpoints = { 5 | dev: '/api', 6 | prodLocal: 'http://localhost:8000', 7 | prodOnline: 'https://glacial-shore-18891.herokuapp.com' 8 | }; 9 | 10 | const prodEndpoint = isLocal ? apiEndpoints.prodLocal : apiEndpoints.prodOnline; 11 | 12 | export const config = { 13 | themoviedb: { 14 | apiKey: '737d47b7285bab76358e9cbe46b76b35', 15 | endpoint: 'https://api.themoviedb.org/3' 16 | }, 17 | api: isDev ? apiEndpoints.dev : prodEndpoint 18 | }; 19 | -------------------------------------------------------------------------------- /angular2-client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /angular2-client/src/main.aot.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Angular bootstraping 3 | */ 4 | import { platformBrowser } from '@angular/platform-browser'; 5 | import { enableProdMode } from '@angular/core'; 6 | /* 7 | * App Module 8 | * our top level module that holds all of our components 9 | */ 10 | import { AppModuleNgFactory } from '../build/src/app/app.module.ngfactory'; 11 | 12 | if (process.env.NODE_ENV === 'production') { 13 | enableProdMode(); 14 | } 15 | 16 | /* 17 | * Bootstrap our Angular app with a top level NgModule 18 | */ 19 | export function main(): Promise { 20 | return platformBrowser() 21 | .bootstrapModuleFactory(AppModuleNgFactory) 22 | .catch((err) => console.error(err)); 23 | } 24 | 25 | if (document.readyState === 'complete') { 26 | main(); 27 | } else { 28 | document.addEventListener('DOMContentLoaded', main); 29 | } 30 | -------------------------------------------------------------------------------- /angular2-client/src/main.jit.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Angular bootstraping 3 | */ 4 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 5 | import { enableProdMode } from '@angular/core'; 6 | /* 7 | * App Module 8 | * our top level module that holds all of our components 9 | */ 10 | import { AppModule } from './app/app.module'; 11 | 12 | if (process.env.NODE_ENV === 'production') { 13 | enableProdMode(); 14 | } 15 | 16 | /* 17 | * Bootstrap our Angular app with a top level NgModule 18 | */ 19 | export function main(): Promise { 20 | return platformBrowserDynamic() 21 | .bootstrapModule(AppModule) 22 | .catch((err) => console.error(err)); 23 | } 24 | 25 | if (document.readyState === 'complete') { 26 | main(); 27 | } else { 28 | document.addEventListener('DOMContentLoaded', main); 29 | } 30 | -------------------------------------------------------------------------------- /angular2-client/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'ie-shim'; // Internet Explorer 9 support 2 | 3 | // import 'core-js/es6'; 4 | // Added parts of es6 which are necessary for your project or your browser support requirements. 5 | import 'core-js/es6/symbol'; 6 | import 'core-js/es6/object'; 7 | import 'core-js/es6/function'; 8 | import 'core-js/es6/parse-int'; 9 | import 'core-js/es6/parse-float'; 10 | import 'core-js/es6/number'; 11 | import 'core-js/es6/math'; 12 | import 'core-js/es6/string'; 13 | import 'core-js/es6/date'; 14 | import 'core-js/es6/array'; 15 | import 'core-js/es6/regexp'; 16 | import 'core-js/es6/map'; 17 | import 'core-js/es6/set'; 18 | import 'core-js/es6/weak-map'; 19 | import 'core-js/es6/weak-set'; 20 | import 'core-js/es6/typed'; 21 | import 'core-js/es6/reflect'; 22 | // see issue https://github.com/AngularClass/angular2-webpack-starter/issues/709 23 | // import 'core-js/es6/promise'; 24 | 25 | import 'core-js/es7/reflect'; 26 | import 'zone.js/dist/zone'; 27 | 28 | if (process.env.ENV === 'production') { 29 | // Production 30 | } else { 31 | // Development and test 32 | Error['stackTraceLimit'] = Infinity; 33 | require('zone.js/dist/long-stack-trace-zone'); 34 | } 35 | -------------------------------------------------------------------------------- /angular2-client/src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Lato'); 2 | @import '~@angular/material/prebuilt-themes/deeppurple-amber.css'; 3 | @import '~font-awesome/css/font-awesome.css'; 4 | 5 | @import './variables.scss'; 6 | 7 | body { 8 | background-color: map-get($colors, dark); 9 | margin: 0; 10 | font-family: 'Lato', sans-serif; 11 | } 12 | 13 | .fw { 14 | width: 100%; 15 | } 16 | 17 | .block { 18 | display: block; 19 | } 20 | .text-center { 21 | text-align: center; 22 | } 23 | 24 | snack-bar-container.error { 25 | background-color: map-get($colors, error); 26 | } 27 | 28 | .alert { 29 | padding: .5em; 30 | border-radius: .5em; 31 | color: map-get($colors, text-on-dark); 32 | margin-bottom: .7em; 33 | 34 | &.error { 35 | background-color: map-get($colors, error); 36 | } 37 | &.success { 38 | background-color: map-get($colors, success); 39 | } 40 | 41 | ul { 42 | list-style: none; 43 | padding: 0; 44 | margin: 0; 45 | } 46 | } 47 | 48 | // preloader 49 | #preloader { 50 | position: fixed; 51 | top: 0; 52 | left: 0; 53 | width: 100%; 54 | height: 100%; 55 | z-index: 99999999; 56 | background: map-get($colors, dark); 57 | 58 | &.loaded { 59 | visibility: hidden; 60 | opacity: 0; 61 | transition: visibility 0s .2s, opacity .2s linear; 62 | } 63 | 64 | // spinner source: https://github.com/tobiasahlin/SpinKit 65 | .spinner { 66 | // margin: 100px auto 0; 67 | width: 70px; 68 | text-align: center; 69 | width: 70px; 70 | text-align: center; 71 | left: 50%; 72 | top: 50%; 73 | position: relative; 74 | } 75 | 76 | .spinner > div { 77 | width: 18px; 78 | height: 18px; 79 | background-color: map-get($colors, vivid); 80 | 81 | border-radius: 100%; 82 | display: inline-block; 83 | -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; 84 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 85 | } 86 | 87 | .spinner .bounce1 { 88 | -webkit-animation-delay: -0.32s; 89 | animation-delay: -0.32s; 90 | } 91 | 92 | .spinner .bounce2 { 93 | -webkit-animation-delay: -0.16s; 94 | animation-delay: -0.16s; 95 | } 96 | 97 | @-webkit-keyframes sk-bouncedelay { 98 | 0%, 80%, 100% { -webkit-transform: scale(0) } 99 | 40% { -webkit-transform: scale(1.0) } 100 | } 101 | 102 | @keyframes sk-bouncedelay { 103 | 0%, 80%, 100% { 104 | -webkit-transform: scale(0); 105 | transform: scale(0); 106 | } 40% { 107 | -webkit-transform: scale(1.0); 108 | transform: scale(1.0); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /angular2-client/src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // colors 2 | $colors: ( 3 | dark: #193048, 4 | dark-:#2c445d, 5 | text-on-dark: #fff, 6 | light-tint: #7c8896, 7 | error: #ef6464, 8 | success: #63a95f, 9 | light-: #f1f1f1, 10 | complementary: #9c6346, 11 | vivid: #ded344 12 | ); 13 | -------------------------------------------------------------------------------- /angular2-client/src/vendor.ts: -------------------------------------------------------------------------------- 1 | // Angular 2 | import '@angular/platform-browser'; 3 | import '@angular/platform-browser-dynamic'; 4 | import '@angular/core'; 5 | import '@angular/common'; 6 | import '@angular/http'; 7 | import '@angular/router'; 8 | 9 | // RxJS 10 | import 'rxjs'; 11 | 12 | // Other vendors for example jQuery, Lodash or Bootstrap 13 | // You can import js, ts, css, sass, ... 14 | -------------------------------------------------------------------------------- /angular2-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "lib": ["dom", "es6"], 11 | "sourceMap": true, 12 | "pretty": true, 13 | "allowUnreachableCode": false, 14 | "allowUnusedLabels": false, 15 | "noImplicitAny": true, 16 | "noImplicitReturns": true, 17 | "noImplicitUseStrict": false, 18 | "noFallthroughCasesInSwitch": true, 19 | "moduleResolution": "node", 20 | "outDir": "build/tmp", 21 | "typeRoots": [ 22 | "./node_modules/@types" 23 | ], 24 | "types": [ 25 | "node", 26 | "jasmine" 27 | ] 28 | }, 29 | "exclude": [ 30 | "build", 31 | "dist", 32 | "node_modules", 33 | "src/main.aot.ts", 34 | "tmp" 35 | ], 36 | "angularCompilerOptions": { 37 | "debug": true, 38 | "genDir": "build", 39 | "skipMetadataEmit": true 40 | }, 41 | "compileOnSave": false, 42 | "buildOnSave": false 43 | } 44 | -------------------------------------------------------------------------------- /angular2-client/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended" 4 | ], 5 | "rulesDirectory": [ 6 | "node_modules/codelyzer" 7 | ], 8 | "rules": { 9 | // Custom 10 | "trailing-comma": [false, {"multiline": "always", "singleline": "never"}], 11 | "interface-name": [false, "always-prefix"], 12 | // Angular 2 13 | "component-class-suffix": true, 14 | // "component-selector": [true, "element", "my", "kebab-case"], 15 | "directive-class-suffix": true, 16 | // "directive-selector": [true, "attribute", "my", "camelCase"], 17 | "import-destructuring-spacing": true, 18 | "invoke-injectable": true, 19 | "no-access-missing-member": true, 20 | "member-access": [ 21 | false 22 | ], 23 | "member-ordering": [ 24 | false 25 | ], 26 | "no-attribute-parameter-decorator": true, 27 | "arrow-parens": false, 28 | "no-forward-ref": true, 29 | "no-input-rename": true, 30 | "one-variable-per-declaration": [ 31 | false 32 | ], 33 | "no-output-rename": true, 34 | "no-var-requires": false, 35 | "pipe-naming": [true, "camelCase", "my"], 36 | "templates-use-public": true, 37 | "use-host-property-decorator": true, 38 | "use-input-property-decorator": true, 39 | "use-life-cycle-interface": true, 40 | "use-output-property-decorator": true, 41 | "use-pipe-transform-interface": true, 42 | // General 43 | "no-console": [true, 44 | "time", 45 | "timeEnd", 46 | "trace" 47 | ], 48 | "max-line-length": [ 49 | true, 50 | 100 51 | ], 52 | "no-string-literal": false, 53 | "no-use-before-declare": true, 54 | "object-literal-sort-keys": false, 55 | "ordered-imports": false, 56 | "quotemark": [ 57 | true, 58 | "single", 59 | "avoid-escape" 60 | ], 61 | "variable-name": [ 62 | true, 63 | "allow-leading-underscore", 64 | "allow-pascal-case", 65 | "ban-keywords", 66 | "check-format" 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /angular2-client/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const devConfig = require('./config/webpack/webpack.dev'); 4 | const prodConfig = require('./config/webpack/webpack.prod'); 5 | 6 | const ENV = process.env.NODE_ENV; 7 | 8 | switch (ENV) { 9 | case 'production': 10 | module.exports = prodConfig; 11 | break 12 | default: 13 | module.exports = devConfig; 14 | } 15 | -------------------------------------------------------------------------------- /django-server/Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn server.wsgi --log-file - -------------------------------------------------------------------------------- /django-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 | -------------------------------------------------------------------------------- /django-server/movies/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damnko/angular2-django-movies/12bb03f2235e271cbdf33025d52e6feedba4b71f/django-server/movies/__init__.py -------------------------------------------------------------------------------- /django-server/movies/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /django-server/movies/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MoviesConfig(AppConfig): 5 | name = 'movies' 6 | -------------------------------------------------------------------------------- /django-server/movies/middlewares/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damnko/angular2-django-movies/12bb03f2235e271cbdf33025d52e6feedba4b71f/django-server/movies/middlewares/__init__.py -------------------------------------------------------------------------------- /django-server/movies/middlewares/jwt_authentication.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import PermissionDenied 2 | import jwt 3 | from django.conf import settings 4 | from movies import utils 5 | 6 | # since I am using the middleware on a per-view basis I need to define the process_request | process_response 7 | # methods and not the __init__ | __call__ ones 8 | # http://agiliq.com/blog/2015/07/understanding-django-middlewares/ 9 | # https://docs.djangoproject.com/en/1.11/ref/utils/#django.utils.decorators.decorator_from_middleware 10 | 11 | class JwtAuthentication(object): 12 | def process_request(self, request): 13 | token = utils.get_token(request) 14 | if token: 15 | try: 16 | payload = jwt.decode(token, settings.JWT_SECRET) 17 | except jwt.ExpiredSignatureError: 18 | raise PermissionDenied 19 | except Exception as e: 20 | raise PermissionDenied 21 | # permission granted 22 | return None 23 | else: 24 | raise PermissionDenied 25 | 26 | 27 | 28 | # "standard" middleware 29 | # https://docs.djangoproject.com/en/1.11/topics/http/middleware/#writing-your-own-middleware 30 | # and then add on the end of settings.py > MIDDLEWARE as movies.middlewares... 31 | # middleware will be applied to each view 32 | 33 | # other interesting link, middlewares with arguments or to decorate methods 34 | # https://docs.djangoproject.com/en/1.11/ref/utils/#module-django.utils.decorators -------------------------------------------------------------------------------- /django-server/movies/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damnko/angular2-django-movies/12bb03f2235e271cbdf33025d52e6feedba4b71f/django-server/movies/migrations/__init__.py -------------------------------------------------------------------------------- /django-server/movies/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.core.validators import MinLengthValidator 3 | 4 | # Create your models here. 5 | class Movie(models.Model): 6 | source_id = models.CharField(max_length=100, primary_key=True) 7 | title = models.CharField(max_length=200) 8 | 9 | def __str__(self): 10 | return 'ID: ' + self.source_id 11 | 12 | def save(self, *args, **kwargs): 13 | # movie ID has to be unique (should probably make it a primary key) 14 | if Movie.objects.filter(source_id = self.source_id).exists(): 15 | raise ValueError('The movie with ID %s is already present' % self.source_id) 16 | else: 17 | # save 18 | super(Movie, self).save(*args, **kwargs) 19 | 20 | 21 | class Rating(models.Model): 22 | movie = models.ForeignKey(Movie, on_delete=models.CASCADE) 23 | rating = models.PositiveIntegerField() 24 | username = models.CharField(max_length=100, validators=[MinLengthValidator(1)]) 25 | 26 | def __str__(self): 27 | return 'ID: %s | Vote: %s' % (self.movie, self.rating) 28 | 29 | def save(self, *args, **kwargs): 30 | self.full_clean() 31 | super(Rating, self).save(*args, **kwargs) 32 | 33 | 34 | class Comment(models.Model): 35 | movie = models.ForeignKey(Movie, on_delete=models.CASCADE) 36 | username = models.CharField(max_length=100, validators=[MinLengthValidator(1)]) 37 | body = models.TextField() 38 | date = models.DateTimeField(auto_now=True) 39 | 40 | def __str__(self): 41 | return self.body 42 | 43 | def save(self, *args, **kwargs): 44 | self.full_clean() 45 | super(Comment, self).save(*args, **kwargs) 46 | -------------------------------------------------------------------------------- /django-server/movies/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /django-server/movies/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from . import views 3 | 4 | movies_routes = [ 5 | url(r'^get-all$', views.movies_summary, name='movies summary'), 6 | url(r'^movie/new$', views.new_movie, name='new movie'), 7 | url(r'^movie/(?P[a-zA-Z0-9]+)/$', views.movie_details, name='movie details'), 8 | url(r'^movie/(?P[a-zA-Z0-9]+)/rating/$', views.getRating, name='get movie rating'), 9 | url(r'^movie/(?P[0-9]+)/comments/$', views.get_comments, name='get movie comments'), 10 | ] 11 | 12 | rate_routes = [ 13 | url(r'^rate$', views.rate, name='rate'), 14 | ] 15 | 16 | comment_routes = [ 17 | url(r'^comment$', views.comment, name='comment'), 18 | url(r'^comment/(?P[0-9]+)/$', views.update_comment, name='update comment'), 19 | ] 20 | 21 | auth_routes = [ 22 | url(r'^auth/csrf$', views.send_csrf, name='send csrf token'), 23 | url(r'^auth/login/$', views.login, name='login'), 24 | url(r'^auth/register/$', views.register, name='register'), 25 | url(r'^auth/username-exists/$', views.username_exists, name='check unique username'), 26 | ] 27 | 28 | user_data_routes = [ 29 | url(r'^user/get-data/$', views.get_user_data, name='get user data'), 30 | url(r'^user/update/$', views.update_data, name='update user data'), 31 | url(r'^user/update-password/$', views.update_password, name='update user password'), 32 | url(r'^user/delete/$', views.delete_account, name='delete user account') 33 | ] 34 | 35 | urlpatterns = movies_routes + rate_routes + auth_routes + user_data_routes + comment_routes 36 | 37 | -------------------------------------------------------------------------------- /django-server/movies/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from django.conf import settings 3 | import jwt 4 | 5 | 6 | def create_login_token(data): 7 | expiration = datetime.utcnow() + timedelta(days=30) 8 | data['exp'] = expiration 9 | token = jwt.encode(data, settings.JWT_SECRET, algorithm='HS256') 10 | return { 11 | 'token': token, 12 | 'exp': expiration 13 | } 14 | 15 | def get_token(request): 16 | token = request.META['HTTP_AUTHORIZATION'] 17 | return token 18 | 19 | def get_token_data(request): 20 | token = get_token(request) 21 | token = jwt.decode(token, settings.JWT_SECRET) 22 | return token -------------------------------------------------------------------------------- /django-server/movies/validators.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | import re 3 | 4 | def validate_password(password): 5 | print('validating passowrd', password) 6 | if len(password) < 8: 7 | print('pass is short') 8 | raise ValidationError('Password should be longer than 8 chars') 9 | # return TypeError('Password should be longer than 8 chars') 10 | elif re.search('[A-Z]', password) is None: 11 | print('pass is not short') 12 | raise ValidationError('Password should have at least one capital letter') 13 | elif re.search('^[a-z0-9A-Z]+$', password) is not None: 14 | print('pass has no special chars') 15 | raise ValidationError('Password should contain at least one special char') 16 | 17 | def validate_email(email): 18 | if re.search(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$', email) is None: 19 | raise ValidationError('Email is not valid') -------------------------------------------------------------------------------- /django-server/movies/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .comments import * 2 | from .movies import * 3 | from .ratings import * 4 | from .auth import * 5 | from .user_data import * -------------------------------------------------------------------------------- /django-server/movies/views/auth.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | from django.contrib.auth.models import User 3 | from django.contrib.auth import authenticate 4 | from django.middleware.csrf import get_token 5 | from django.core.exceptions import ValidationError 6 | import json 7 | 8 | from movies.utils import create_login_token 9 | from movies.validators import validate_password, validate_email 10 | 11 | def send_csrf(request): 12 | # just by doing this it will send csrf token back as Set-Cookie header 13 | csrf_token = get_token(request) 14 | return JsonResponse({ 15 | 'status': 'success', 16 | 'data': csrf_token 17 | }) 18 | 19 | def username_exists(request): 20 | username = request.GET.get('u', '') 21 | 22 | try: 23 | u = User.objects.get(username=username) 24 | except User.DoesNotExist: 25 | return JsonResponse({ 26 | 'status': 'success', 27 | 'data': { 28 | 'username_exists': False 29 | } 30 | }) 31 | return JsonResponse({ 32 | 'status': 'success', 33 | 'data': { 34 | 'username_exists': True 35 | } 36 | }) 37 | 38 | def register(request): 39 | if request.method != 'POST': 40 | pass 41 | 42 | post_data = json.loads(request.body) 43 | username = post_data['username'] 44 | email = post_data['email'] 45 | password = post_data['password'] 46 | 47 | try: 48 | validate_password(password) 49 | validate_email(email) 50 | except ValidationError as e: 51 | return JsonResponse({ 52 | 'status': 'fail', 53 | 'data': { 54 | 'message': str(e) 55 | } 56 | }, status=500) 57 | 58 | # register user 59 | try: 60 | u = User.objects.create_user(username=username, password=password, email=email) 61 | u.save() 62 | except: 63 | return JsonResponse({ 64 | 'status': 'fail', 65 | 'data': { 66 | 'message': 'There was an error during registration' 67 | } 68 | }, status=500) 69 | 70 | # login user 71 | return login(request, True, {'username': username, 'email': email}) 72 | 73 | def login(request, redirect_after_registration=False, registration_data=None): 74 | if redirect_after_registration: 75 | token = create_login_token(registration_data) 76 | else: 77 | # check credentials 78 | post_data = json.loads(request.body) 79 | username = post_data['username'] 80 | password = post_data['password'] 81 | 82 | u = authenticate(username=username, password=password) 83 | # if authenticated, create and return token 84 | if u is not None: 85 | token = create_login_token({'username': u.username, 'email': u.email}) 86 | else: 87 | return JsonResponse({ 88 | 'status': 'fail' 89 | }, status=401) 90 | 91 | print('token is', token['token']) 92 | 93 | res = JsonResponse({ 94 | 'status': 'success', 95 | 'data': str(token['token'], 'utf-8') 96 | }) 97 | res.set_cookie('token', value=token['token'], expires=token['exp']) 98 | return res 99 | -------------------------------------------------------------------------------- /django-server/movies/views/comments.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | import math 3 | import json 4 | 5 | from ..models import Comment, Movie 6 | from movies.utils import get_token_data 7 | 8 | # @csrf_exempt # temporary decorator to remove csrf, just to test with postman 9 | def comment(request): 10 | if request.method == 'POST': 11 | post_data = json.loads(request.body) 12 | movie_id = post_data['id'] 13 | body = post_data['body'] 14 | 15 | try: 16 | username = post_data['username'] 17 | except KeyError: 18 | token = get_token_data(request) 19 | username = token['username'] 20 | 21 | # get movie object 22 | m, created = Movie.objects.get_or_create(source_id = movie_id, defaults={'title': ''}) 23 | # comment 24 | c = Comment(movie = m, username = username, body = body) 25 | try: 26 | c.save() 27 | except: 28 | return JsonResponse({ 29 | 'status': 'fail', 30 | 'data': { 31 | 'message': 'Error while saving comment' 32 | } 33 | }, status=500) 34 | 35 | return JsonResponse({ 36 | 'status': 'success', 37 | 'data': { 38 | 'id': c.id 39 | } 40 | }) 41 | elif request.method == 'DELETE': 42 | id = request.GET.get('id', '') 43 | username = request.GET.get('u', '') 44 | 45 | try: 46 | c = Comment.objects.get(id=id, username=username) 47 | except Comment.DoesNotExist: 48 | return JsonResponse({ 49 | 'status': 'fail', 50 | 'data': { 51 | 'message': 'This comment does not exist' 52 | } 53 | }, status=500) 54 | 55 | try: 56 | c.delete() 57 | except: 58 | return JsonResponse({ 59 | 'status': 'fail', 60 | 'data': { 61 | 'message': 'Error while deleting comment' 62 | } 63 | }, status=500) 64 | 65 | return JsonResponse({ 66 | 'status': 'success' 67 | }) 68 | 69 | def get_comments(request, movie_id): 70 | if request.method != 'GET': 71 | pass 72 | 73 | items_per_page = 7 74 | page = int(request.GET.get('p', 1)) 75 | 76 | c = Comment.objects.filter(movie_id=movie_id).order_by('-date') 77 | total_pages = math.ceil(c.count() / items_per_page) 78 | 79 | page = page-1 if page <=total_pages or total_pages==0 else total_pages-1 80 | limits = { 81 | 'from': items_per_page * page, 82 | 'to': (items_per_page * page) + items_per_page 83 | } 84 | 85 | comments = c[limits['from']: limits['to']].values() 86 | 87 | return JsonResponse({ 88 | 'status': 'success', 89 | 'data': { 90 | 'comments': list(comments), 91 | 'total_pages': total_pages, 92 | 'current_page': page+1, 93 | 'items_per_page': items_per_page 94 | } 95 | }) 96 | 97 | # not currently used 98 | def update_comment(request, id): 99 | if request.method != 'POST': 100 | pass 101 | 102 | username = request.POST.get('username', '') 103 | body = request.POST.get('body', '') 104 | 105 | try: 106 | c = Comment.objects.get(id=id, username=username) 107 | except Comment.DoesNotExist: 108 | return JsonResponse({ 109 | 'status': 'fail', 110 | 'data': { 111 | 'message': 'This comment does not exist' 112 | } 113 | }, status=500) 114 | 115 | c.body = body 116 | try: 117 | c.save() 118 | except: 119 | return JsonResponse({ 120 | 'status': 'fail', 121 | 'data': { 122 | 'message': 'Error while updating comment' 123 | } 124 | }) 125 | 126 | return JsonResponse({ 127 | 'status': 'success' 128 | }) -------------------------------------------------------------------------------- /django-server/movies/views/movies.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | from django.db.models import Avg, Count, Func 3 | from ..models import Movie, Rating, Comment 4 | 5 | 6 | def new_movie(request): 7 | if request.method != 'POST': 8 | pass 9 | 10 | # get movie id and title 11 | id = request.POST.get('id', '') 12 | title = request.POST.get('title', '') 13 | 14 | # save new movie 15 | m = Movie(source_id = id, title = title) 16 | try: 17 | m.save() 18 | except Exception as e: 19 | return JsonResponse({ 20 | 'status': 'fail', 21 | 'data': { 22 | 'message': str(e) if type(e) == ValueError else 'Error while saving movie' 23 | } 24 | }, status=500) 25 | 26 | return JsonResponse({ 27 | 'status': 'success', 28 | 'data': { 29 | 'title': m.title 30 | } 31 | }) 32 | 33 | 34 | def movie_details(request, movie_id): 35 | if request.method != 'GET': 36 | pass 37 | 38 | # get movie 39 | try: 40 | m = Movie.objects.get(source_id=movie_id) 41 | except Movie.DoesNotExist: 42 | return JsonResponse({ 43 | 'status': 'success', 44 | 'data': { 45 | 'rating': { 46 | 'avg': None, 47 | 'comments': None 48 | } 49 | } 50 | }) 51 | 52 | # get rating 53 | r = Rating.objects.filter(movie=m)\ 54 | .values('rating')\ 55 | .aggregate( 56 | avg_rating=Avg('rating'), 57 | rating_count=Count('rating') 58 | ) 59 | avg_rating = r['avg_rating'] 60 | rating_count = r['rating_count'] 61 | 62 | # get comments 63 | c = Comment.objects.filter(movie=m).values('body', 'username') 64 | 65 | return JsonResponse({ 66 | 'status': 'success', 67 | 'data': { 68 | 'rating': { 69 | 'avg': '{:.1f}'.format(avg_rating) if avg_rating is not None else None, 70 | 'count': rating_count 71 | }, 72 | 'comments': list(c) 73 | } 74 | }) 75 | 76 | class Round(Func): 77 | function = 'ROUND' 78 | template='%(function)s(%(expressions)s, 1)' 79 | 80 | def movies_summary(request): 81 | if request.method != 'GET': 82 | pass 83 | 84 | # get all requested movie ids 85 | movie_ids = request.GET.get('ids', '').split(',') 86 | 87 | m = Movie.objects.filter(source_id__in=movie_ids).annotate( 88 | avg_rating=Round(Avg('rating__rating')), # avg on rating column of rating table 89 | comment_count=Count('comment', distinct=True) 90 | ).values() 91 | 92 | movies = {} 93 | for movie in list(m): 94 | movies[movie.get('source_id')] = movie 95 | 96 | return JsonResponse({ 97 | 'status': 'success', 98 | 'data': { 99 | 'movies': movies 100 | } 101 | }) 102 | -------------------------------------------------------------------------------- /django-server/movies/views/ratings.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | import json 3 | 4 | from movies.utils import get_token_data 5 | from ..models import Rating, Movie 6 | 7 | 8 | def rate(request): 9 | 10 | # if POST, save or update rating 11 | if request.method == 'POST': 12 | body = json.loads(request.body) 13 | movie_id = body['id'] 14 | rating = int(body['rating']) 15 | 16 | try: 17 | username = body['username'] 18 | except KeyError: 19 | token = get_token_data(request) 20 | username = token['username'] 21 | 22 | # get the movie object with id movie_id, or create it 23 | m, created = Movie.objects.get_or_create(source_id=movie_id, defaults={'title': ''}) 24 | # save or update rating 25 | try: 26 | r, created = Rating.objects.update_or_create(username=username, movie=m, defaults={'rating': rating}) 27 | except Exception as e: 28 | print(e) 29 | return JsonResponse({ 30 | 'status': 'fail', 31 | 'data': { 32 | 'message': 'Error while saving rating' 33 | } 34 | }, status=500) 35 | 36 | return JsonResponse({ 37 | 'status': 'success', 38 | 'data': { 39 | 'title': m.title, 40 | 'rating': r.rating, 41 | 'is_new': created 42 | } 43 | }) 44 | elif request.method == 'DELETE': 45 | username = request.GET.get('u', '') 46 | movie_id = request.GET.get('m_id', '') 47 | 48 | # find movie object 49 | m = Movie.objects.filter(source_id=movie_id).first() 50 | r = Rating.objects.filter(movie=m, username=username) 51 | 52 | # delete rating 53 | try: 54 | r.delete() 55 | except: 56 | return JsonResponse({ 57 | 'status': 'fail', 58 | 'data': { 59 | 'message': 'Error while deleting rating' 60 | } 61 | }, status=500) 62 | 63 | return JsonResponse({ 64 | 'status': 'success' 65 | }) 66 | 67 | def getRating(request, movie_id): 68 | if request.method != 'POST': 69 | pass 70 | 71 | body = json.loads(request.body) 72 | username = body['username'] 73 | 74 | # get rating 75 | r = Rating.objects.filter(movie_id = movie_id, username = username).first() 76 | 77 | return JsonResponse({ 78 | 'result': 'success', 79 | 'data': { 80 | 'rating': r.rating if r else None 81 | } 82 | }) -------------------------------------------------------------------------------- /django-server/movies/views/user_data.py: -------------------------------------------------------------------------------- 1 | from django.utils.decorators import decorator_from_middleware 2 | from movies.middlewares.jwt_authentication import JwtAuthentication 3 | from django.contrib.auth.models import User 4 | from django.http import JsonResponse 5 | from django.contrib.auth import authenticate 6 | from django.core.exceptions import ValidationError 7 | import json 8 | 9 | from movies.utils import get_token_data, create_login_token 10 | from movies.validators import validate_email, validate_password 11 | 12 | @decorator_from_middleware(JwtAuthentication) 13 | def get_user_data(request): 14 | token = get_token_data(request) 15 | username = token['username'] 16 | 17 | try: 18 | u = User.objects.get(username=username).values('username', 'email') 19 | except User.DoesNotExist: 20 | return JsonResponse({ 21 | 'status': 'fail', 22 | 'data': { 23 | 'message': 'The username does not exist' 24 | } 25 | }, status=500) 26 | 27 | return JsonResponse({ 28 | 'status': 'success', 29 | 'data': u 30 | }) 31 | 32 | @decorator_from_middleware(JwtAuthentication) 33 | def update_data(request): 34 | # only the email can be updated here 35 | token = get_token_data(request) 36 | username = token['username'] 37 | 38 | post_data = json.loads(request.body) 39 | new_email = post_data['email'] 40 | 41 | try: 42 | validate_email(new_email) 43 | except ValidationError as e: 44 | return JsonResponse({ 45 | 'status': 'fail', 46 | 'data': { 47 | 'message': str(e) 48 | } 49 | }, status=500) 50 | 51 | # get user object 52 | u = User.objects.get(username=username) 53 | u.email = new_email 54 | try: 55 | u.save() 56 | except: 57 | return JsonResponse({ 58 | 'status': 'fail', 59 | 'data': { 60 | 'message': 'There was an error while updating user data' 61 | } 62 | }, status=500) 63 | 64 | token = create_login_token({'username': u.username, 'email': u.email}) 65 | res = JsonResponse({ 66 | 'status': 'success' 67 | }) 68 | res.set_cookie('token', value=token['token'], expires=token['exp']) 69 | return res 70 | 71 | def update_password(request): 72 | token = get_token_data(request) 73 | username = token['username'] 74 | 75 | post_data = json.loads(request.body) 76 | new_password = post_data['password'] 77 | old_password = post_data['oldPassword'] 78 | 79 | try: 80 | validate_password(new_password) 81 | except ValidationError as e: 82 | return JsonResponse({ 83 | 'status': 'fail', 84 | 'data': { 85 | 'message': str(e) 86 | } 87 | }, status=500) 88 | 89 | # check old password and get user object 90 | u = authenticate(username=username, password=old_password) 91 | if u is not None: 92 | u.set_password(new_password) 93 | try: 94 | u.save() 95 | except: 96 | return JsonResponse({ 97 | 'status': 'fail', 98 | 'data': { 99 | 'message': 'There was an error while updating the password' 100 | } 101 | }, status=500) 102 | 103 | return JsonResponse({ 104 | 'status': 'success' 105 | }) 106 | else: 107 | return JsonResponse({ 108 | 'status': 'fail' 109 | }, status=401) 110 | 111 | def delete_account(request): 112 | if request.method != 'DELETE': 113 | pass 114 | 115 | token = get_token_data(request) 116 | username = token['username'] 117 | 118 | u = User.objects.get(username=username) 119 | try: 120 | u.delete() 121 | except: 122 | return JsonResponse({ 123 | 'status': 'fail', 124 | 'data': { 125 | 'message': 'There was an error while deleting user account' 126 | } 127 | }, status=500) 128 | 129 | # need to delete jwt cookie on client side 130 | return JsonResponse({ 131 | 'status': 'success' 132 | }) -------------------------------------------------------------------------------- /django-server/requirements.txt: -------------------------------------------------------------------------------- 1 | awsebcli==3.10.2 2 | blessed==1.14.2 3 | botocore==1.5.73 4 | cement==2.8.2 5 | certifi==2017.4.17 6 | chardet==3.0.4 7 | colorama==0.3.7 8 | Django==1.11.2 9 | django-cors-headers==2.1.0 10 | docker-py==1.7.2 11 | dockerpty==0.4.1 12 | docopt==0.6.2 13 | docutils==0.13.1 14 | gunicorn==19.7.1 15 | idna==2.5 16 | jmespath==0.9.3 17 | mysqlclient==1.3.10 18 | pathspec==0.5.0 19 | PyJWT==1.5.0 20 | python-dateutil==2.6.0 21 | pytz==2017.2 22 | PyYAML==3.12 23 | requests==2.9.1 24 | semantic-version==2.5.0 25 | six==1.10.0 26 | tabulate==0.7.5 27 | termcolor==1.1.0 28 | urllib3==1.21.1 29 | wcwidth==0.1.7 30 | websocket-client==0.43.0 31 | whitenoise==3.3.0 32 | -------------------------------------------------------------------------------- /django-server/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damnko/angular2-django-movies/12bb03f2235e271cbdf33025d52e6feedba4b71f/django-server/server/__init__.py -------------------------------------------------------------------------------- /django-server/server/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for server project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.2. 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 | import base64 15 | import sys 16 | from urllib import parse 17 | 18 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 19 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 20 | 21 | 22 | # Quick-start development settings - unsuitable for production 23 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 24 | 25 | # SECURITY WARNING: keep the secret key used in production secret! 26 | SECRET_KEY = 'os5^q5qs3%ch()09f7l$sy+p6^mue@+*r(l*hv_5z-87jngttq' 27 | 28 | # SECURITY WARNING: don't run with debug turned on in production! 29 | DEBUG = False if 'DATABASE_URL' in os.environ else True 30 | 31 | ALLOWED_HOSTS = [ 32 | 'glacial-shore-18891.herokuapp.com', 33 | 'localhost' 34 | ] 35 | 36 | 37 | # Application definition 38 | 39 | INSTALLED_APPS = [ 40 | 'corsheaders', 41 | 'movies.apps.MoviesConfig', 42 | 'django.contrib.admin', 43 | 'django.contrib.auth', 44 | 'django.contrib.contenttypes', 45 | 'django.contrib.sessions', 46 | 'django.contrib.messages', 47 | 'django.contrib.staticfiles', 48 | ] 49 | 50 | MIDDLEWARE = [ 51 | 'corsheaders.middleware.CorsMiddleware', 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 | ROOT_URLCONF = 'server.urls' 62 | 63 | TEMPLATES = [ 64 | { 65 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 66 | 'DIRS': [], 67 | 'APP_DIRS': True, 68 | 'OPTIONS': { 69 | 'context_processors': [ 70 | 'django.template.context_processors.debug', 71 | 'django.template.context_processors.request', 72 | 'django.contrib.auth.context_processors.auth', 73 | 'django.contrib.messages.context_processors.messages', 74 | ], 75 | }, 76 | }, 77 | ] 78 | 79 | WSGI_APPLICATION = 'server.wsgi.application' 80 | 81 | 82 | # Database 83 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 84 | 85 | DATABASES = { 86 | 'default': { 87 | 'ENGINE': 'django.db.backends.mysql', 88 | 'NAME': 'django_angular2', 89 | 'USER': 'root', 90 | 'PASSWORD': 'password', 91 | 'HOST': '127.0.0.1' 92 | } 93 | } 94 | 95 | 96 | # Password validation 97 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 98 | 99 | AUTH_PASSWORD_VALIDATORS = [ 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 105 | }, 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 108 | }, 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 111 | } 112 | ] 113 | 114 | 115 | # Internationalization 116 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 117 | 118 | LANGUAGE_CODE = 'en-us' 119 | 120 | TIME_ZONE = 'Europe/Rome' 121 | 122 | USE_I18N = True 123 | 124 | USE_L10N = True 125 | 126 | USE_TZ = True 127 | 128 | 129 | # Static files (CSS, JavaScript, Images) 130 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 131 | 132 | STATIC_URL = '/static/' 133 | 134 | CORS_ORIGIN_ALLOW_ALL = True 135 | CORS_ALLOW_CREDENTIALS = True 136 | CSRF_COOKIE_SECURE = False 137 | CSRF_TRUSTED_ORIGINS = ['django-angular2-movies.firebaseapp.com'] 138 | 139 | # custom settings 140 | JWT_SECRET = base64.b64encode(b'ScaredCherriesEatSurelySimpleVulcansParticipateIntensely') 141 | 142 | # heroku database settings 143 | # Register database schemes in URLs. 144 | parse.uses_netloc.append('mysql') 145 | 146 | try: 147 | 148 | # Check to make sure DATABASES is set in settings.py file. 149 | # If not default to {} 150 | 151 | if 'DATABASES' not in locals(): 152 | DATABASES = {} 153 | 154 | if 'DATABASE_URL' in os.environ: 155 | url = parse.urlparse(os.environ['DATABASE_URL']) 156 | 157 | # Ensure default database exists. 158 | DATABASES['default'] = DATABASES.get('default', {}) 159 | 160 | # Update with environment configuration. 161 | DATABASES['default'].update({ 162 | 'NAME': url.path[1:], 163 | 'USER': url.username, 164 | 'PASSWORD': url.password, 165 | 'HOST': url.hostname, 166 | 'PORT': url.port, 167 | }) 168 | 169 | 170 | if url.scheme == 'mysql': 171 | DATABASES['default']['ENGINE'] = 'django.db.backends.mysql' 172 | except Exception: 173 | print('Unexpected error:', sys.exc_info()) -------------------------------------------------------------------------------- /django-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, include 17 | from django.contrib import admin 18 | 19 | urlpatterns = [ 20 | url(r'^admin/', admin.site.urls), 21 | url(r'^movies/', include('movies.urls')) 22 | ] 23 | -------------------------------------------------------------------------------- /django-server/server/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for gettingstarted project. 3 | It exposes the WSGI callable as a module-level variable named ``application``. 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ 6 | """ 7 | 8 | import os 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") 10 | 11 | from django.core.wsgi import get_wsgi_application 12 | from whitenoise.django import DjangoWhiteNoise 13 | 14 | application = get_wsgi_application() 15 | application = DjangoWhiteNoise(application) --------------------------------------------------------------------------------