├── .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 |
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 |
31 |
32 |
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 |
0; else noVotes" class="rating current-rating">
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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)
--------------------------------------------------------------------------------
Comments
5 | 6 | No comments yet, be the first to drop a line! 7 | 8 |18 | {{ comment.username }} 19 | {{ comment.date | date:'medium' }} 20 | 21 | 22 | 23 |
24 |{{comment.body}}
25 |