├── client ├── .dockerignore ├── .editorconfig ├── .gitignore ├── Dockerfile ├── README.md ├── angular.json ├── e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.e2e.json ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── components │ │ │ ├── driver-dashboard │ │ │ │ ├── driver-dashboard.component.css │ │ │ │ ├── driver-dashboard.component.html │ │ │ │ ├── driver-dashboard.component.spec.ts │ │ │ │ └── driver-dashboard.component.ts │ │ │ ├── driver-detail │ │ │ │ ├── driver-detail.component.css │ │ │ │ ├── driver-detail.component.html │ │ │ │ ├── driver-detail.component.spec.ts │ │ │ │ └── driver-detail.component.ts │ │ │ ├── driver │ │ │ │ ├── driver.component.css │ │ │ │ ├── driver.component.html │ │ │ │ ├── driver.component.spec.ts │ │ │ │ └── driver.component.ts │ │ │ ├── landing │ │ │ │ ├── landing.component.css │ │ │ │ ├── landing.component.html │ │ │ │ ├── landing.component.spec.ts │ │ │ │ └── landing.component.ts │ │ │ ├── log-in │ │ │ │ ├── log-in.component.css │ │ │ │ ├── log-in.component.html │ │ │ │ ├── log-in.component.spec.ts │ │ │ │ └── log-in.component.ts │ │ │ ├── rider-dashboard │ │ │ │ ├── rider-dashboard.component.css │ │ │ │ ├── rider-dashboard.component.html │ │ │ │ ├── rider-dashboard.component.spec.ts │ │ │ │ └── rider-dashboard.component.ts │ │ │ ├── rider-detail │ │ │ │ ├── rider-detail.component.css │ │ │ │ ├── rider-detail.component.html │ │ │ │ ├── rider-detail.component.spec.ts │ │ │ │ └── rider-detail.component.ts │ │ │ ├── rider-request │ │ │ │ ├── rider-request.component.css │ │ │ │ ├── rider-request.component.html │ │ │ │ ├── rider-request.component.spec.ts │ │ │ │ └── rider-request.component.ts │ │ │ ├── rider │ │ │ │ ├── rider.component.css │ │ │ │ ├── rider.component.html │ │ │ │ ├── rider.component.spec.ts │ │ │ │ └── rider.component.ts │ │ │ ├── sign-up │ │ │ │ ├── sign-up.component.css │ │ │ │ ├── sign-up.component.html │ │ │ │ ├── sign-up.component.spec.ts │ │ │ │ └── sign-up.component.ts │ │ │ └── trip-card │ │ │ │ ├── trip-card.component.css │ │ │ │ ├── trip-card.component.html │ │ │ │ ├── trip-card.component.spec.ts │ │ │ │ └── trip-card.component.ts │ │ ├── services │ │ │ ├── auth.service.spec.ts │ │ │ ├── auth.service.ts │ │ │ ├── google-maps.service.spec.ts │ │ │ ├── google-maps.service.ts │ │ │ ├── is-driver.service.spec.ts │ │ │ ├── is-driver.service.ts │ │ │ ├── is-rider.service.spec.ts │ │ │ ├── is-rider.service.ts │ │ │ ├── trip-detail.resolver.spec.ts │ │ │ ├── trip-detail.resolver.ts │ │ │ ├── trip-list.resolver.spec.ts │ │ │ ├── trip-list.resolver.ts │ │ │ ├── trip.service.spec.ts │ │ │ └── trip.service.ts │ │ └── testing │ │ │ └── factories.ts │ ├── assets │ │ └── .gitkeep │ ├── browserslist │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── karma.conf.js │ ├── main.ts │ ├── polyfills.ts │ ├── styles.css │ ├── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json ├── tsconfig.json └── tslint.json ├── docker-compose.yml ├── nginx ├── Dockerfile ├── dev.conf ├── include.forwarded └── include.websocket ├── pytest.ini └── server ├── .dockerignore ├── Dockerfile ├── media └── test.txt ├── requirements.txt ├── start.sh └── taxi ├── manage.py ├── taxi ├── __init__.py ├── asgi.py ├── routing.py ├── settings.py ├── urls.py └── wsgi.py └── trips ├── __init__.py ├── admin.py ├── apps.py ├── consumers.py ├── migrations ├── 0001_initial.py ├── 0002_trip.py ├── 0003_trip_driver_rider.py ├── 0004_user_photo.py └── __init__.py ├── models.py ├── serializers.py ├── tests ├── __init__.py ├── test_http.py └── test_websockets.py ├── urls.py └── views.py /client/.dockerignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dockerignore 3 | node_modules 4 | README.md 5 | .gitignore 6 | -------------------------------------------------------------------------------- /client/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events.json 15 | speed-measure-plugin.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | # base image 2 | FROM node:11.10.1-alpine 3 | 4 | # install chrome 5 | RUN echo @edge http://nl.alpinelinux.org/alpine/edge/community >> /etc/apk/repositories \ 6 | && echo @edge http://nl.alpinelinux.org/alpine/edge/main >> /etc/apk/repositories \ 7 | && apk add --no-cache \ 8 | chromium@edge \ 9 | harfbuzz@edge \ 10 | nss@edge \ 11 | && rm -rf /var/cache/* \ 12 | && mkdir /var/cache/apk 13 | 14 | # set working directory 15 | RUN mkdir -p /usr/src/app 16 | WORKDIR /usr/src/app 17 | 18 | # add `/usr/src/app/node_modules/.bin` to $PATH 19 | ENV PATH /usr/src/app/node_modules/.bin:$PATH 20 | 21 | # install app dependencies 22 | COPY package.json /usr/src/app/package.json 23 | COPY package-lock.json /usr/src/app/package-lock.json 24 | RUN npm install 25 | 26 | # copy the client directory into the container 27 | COPY . /usr/src/app 28 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Client 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.3.2. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /client/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "client": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/client", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": [ 22 | "src/favicon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "node_modules/bootswatch/dist/lumen/bootstrap.min.css", 27 | "src/styles.css" 28 | ], 29 | "scripts": [ 30 | "node_modules/jquery/dist/jquery.min.js", 31 | "node_modules/popper.js/dist/umd/popper.min.js", 32 | "node_modules/bootstrap/dist/js/bootstrap.min.js" 33 | ], 34 | "es5BrowserSupport": true 35 | }, 36 | "configurations": { 37 | "production": { 38 | "fileReplacements": [ 39 | { 40 | "replace": "src/environments/environment.ts", 41 | "with": "src/environments/environment.prod.ts" 42 | } 43 | ], 44 | "optimization": true, 45 | "outputHashing": "all", 46 | "sourceMap": false, 47 | "extractCss": true, 48 | "namedChunks": false, 49 | "aot": true, 50 | "extractLicenses": true, 51 | "vendorChunk": false, 52 | "buildOptimizer": true, 53 | "budgets": [ 54 | { 55 | "type": "initial", 56 | "maximumWarning": "2mb", 57 | "maximumError": "5mb" 58 | } 59 | ] 60 | } 61 | } 62 | }, 63 | "serve": { 64 | "builder": "@angular-devkit/build-angular:dev-server", 65 | "options": { 66 | "browserTarget": "client:build" 67 | }, 68 | "configurations": { 69 | "production": { 70 | "browserTarget": "client:build:production" 71 | } 72 | } 73 | }, 74 | "extract-i18n": { 75 | "builder": "@angular-devkit/build-angular:extract-i18n", 76 | "options": { 77 | "browserTarget": "client:build" 78 | } 79 | }, 80 | "test": { 81 | "builder": "@angular-devkit/build-angular:karma", 82 | "options": { 83 | "main": "src/test.ts", 84 | "polyfills": "src/polyfills.ts", 85 | "tsConfig": "src/tsconfig.spec.json", 86 | "karmaConfig": "src/karma.conf.js", 87 | "styles": [ 88 | "src/styles.css" 89 | ], 90 | "scripts": [], 91 | "assets": [ 92 | "src/favicon.ico", 93 | "src/assets" 94 | ] 95 | } 96 | }, 97 | "lint": { 98 | "builder": "@angular-devkit/build-angular:tslint", 99 | "options": { 100 | "tsConfig": [ 101 | "src/tsconfig.app.json", 102 | "src/tsconfig.spec.json" 103 | ], 104 | "exclude": [ 105 | "**/node_modules/**" 106 | ] 107 | } 108 | } 109 | } 110 | }, 111 | "client-e2e": { 112 | "root": "e2e/", 113 | "projectType": "application", 114 | "prefix": "", 115 | "architect": { 116 | "e2e": { 117 | "builder": "@angular-devkit/build-angular:protractor", 118 | "options": { 119 | "protractorConfig": "e2e/protractor.conf.js", 120 | "devServerTarget": "client:serve" 121 | }, 122 | "configurations": { 123 | "production": { 124 | "devServerTarget": "client:serve:production" 125 | } 126 | } 127 | }, 128 | "lint": { 129 | "builder": "@angular-devkit/build-angular:tslint", 130 | "options": { 131 | "tsConfig": "e2e/tsconfig.e2e.json", 132 | "exclude": [ 133 | "**/node_modules/**" 134 | ] 135 | } 136 | } 137 | } 138 | } 139 | }, 140 | "defaultProject": "client" 141 | } 142 | -------------------------------------------------------------------------------- /client/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /client/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('Welcome to client!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@agm/core": "^1.0.0-beta.5", 15 | "@angular/animations": "^7.2.8", 16 | "@angular/common": "~7.2.0", 17 | "@angular/compiler": "~7.2.0", 18 | "@angular/core": "~7.2.0", 19 | "@angular/forms": "~7.2.0", 20 | "@angular/platform-browser": "~7.2.0", 21 | "@angular/platform-browser-dynamic": "~7.2.0", 22 | "@angular/router": "~7.2.0", 23 | "@types/faker": "^4.1.5", 24 | "bootstrap": "^4.3.1", 25 | "bootswatch": "^4.3.1", 26 | "core-js": "^2.5.4", 27 | "faker": "^4.1.0", 28 | "jquery": "^3.3.1", 29 | "ng6-toastr-notifications": "^1.0.4", 30 | "popper.js": "^1.14.7", 31 | "rxjs": "~6.3.3", 32 | "tslib": "^1.9.0", 33 | "zone.js": "~0.8.26" 34 | }, 35 | "devDependencies": { 36 | "@angular-devkit/build-angular": "~0.13.0", 37 | "@angular/cli": "~7.3.2", 38 | "@angular/compiler-cli": "~7.2.0", 39 | "@angular/language-service": "~7.2.0", 40 | "@types/node": "~8.9.4", 41 | "@types/jasmine": "~2.8.8", 42 | "@types/jasminewd2": "~2.0.3", 43 | "codelyzer": "~4.5.0", 44 | "jasmine-core": "~2.99.1", 45 | "jasmine-spec-reporter": "~4.2.1", 46 | "karma": "~3.1.1", 47 | "karma-chrome-launcher": "~2.2.0", 48 | "karma-coverage-istanbul-reporter": "~2.0.1", 49 | "karma-jasmine": "~1.1.2", 50 | "karma-jasmine-html-reporter": "^0.2.2", 51 | "protractor": "~5.4.0", 52 | "ts-node": "~7.0.0", 53 | "tslint": "~5.11.0", 54 | "typescript": "~3.2.2" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /client/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/taxi-app/8f4e25bac1373ac2312d1b20398a48c21f6130cb/client/src/app/app.component.css -------------------------------------------------------------------------------- /client/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /client/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | 4 | import { AppComponent } from '../app/app.component'; 5 | 6 | describe('AppComponent', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | imports: [ 10 | RouterTestingModule.withRoutes([]) 11 | ], 12 | declarations: [ 13 | AppComponent 14 | ], 15 | }).compileComponents(); 16 | }); 17 | 18 | it('should create the app', () => { 19 | const fixture = TestBed.createComponent(AppComponent); 20 | const app = fixture.debugElement.componentInstance; 21 | expect(app).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'client'; 10 | } 11 | -------------------------------------------------------------------------------- /client/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { NgModule } from '@angular/core'; 4 | import { RouterModule } from '@angular/router'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { HttpClientModule } from '@angular/common/http'; 7 | 8 | import { AgmCoreModule } from '@agm/core'; 9 | import { ToastrModule } from 'ng6-toastr-notifications'; 10 | 11 | import { environment } from '../environments/environment'; 12 | 13 | import { AuthService } from './services/auth.service'; 14 | import { IsRider } from './services/is-rider.service'; 15 | import { TripService } from './services/trip.service'; 16 | import { TripListResolver } from './services/trip-list.resolver'; 17 | import { TripDetailResolver } from './services/trip-detail.resolver'; 18 | import { IsDriver } from './services/is-driver.service'; 19 | import { GoogleMapsService } from './services/google-maps.service'; 20 | 21 | import { AppComponent } from './app.component'; 22 | import { SignUpComponent } from './components/sign-up/sign-up.component'; 23 | import { LogInComponent } from './components/log-in/log-in.component'; 24 | import { LandingComponent } from './components/landing/landing.component'; 25 | import { RiderComponent } from './components/rider/rider.component'; 26 | import { RiderDashboardComponent } from './components/rider-dashboard/rider-dashboard.component'; 27 | import { RiderRequestComponent } from './components/rider-request/rider-request.component'; 28 | import { RiderDetailComponent } from './components/rider-detail/rider-detail.component'; 29 | import { TripCardComponent } from './components/trip-card/trip-card.component'; 30 | import { DriverComponent } from './components/driver/driver.component'; 31 | import { DriverDashboardComponent } from './components/driver-dashboard/driver-dashboard.component'; 32 | import { DriverDetailComponent } from './components/driver-detail/driver-detail.component'; 33 | 34 | @NgModule({ 35 | declarations: [ 36 | AppComponent, 37 | SignUpComponent, 38 | LogInComponent, 39 | LandingComponent, 40 | RiderComponent, 41 | RiderDashboardComponent, 42 | RiderRequestComponent, 43 | RiderDetailComponent, 44 | TripCardComponent, 45 | DriverComponent, 46 | DriverDashboardComponent, 47 | DriverDetailComponent 48 | ], 49 | imports: [ 50 | HttpClientModule, 51 | BrowserModule, 52 | BrowserAnimationsModule, 53 | FormsModule, 54 | RouterModule.forRoot([ 55 | { path: 'sign-up', component: SignUpComponent }, 56 | { path: 'log-in', component: LogInComponent }, 57 | { 58 | path: 'rider', 59 | component: RiderComponent, 60 | canActivate: [ IsRider ], 61 | children: [ 62 | { 63 | path: 'request', 64 | component: RiderRequestComponent 65 | }, 66 | { 67 | path: ':id', 68 | component: RiderDetailComponent, 69 | resolve: { trip: TripDetailResolver } 70 | }, 71 | { 72 | path: '', 73 | component: RiderDashboardComponent, 74 | resolve: { trips: TripListResolver } 75 | } 76 | ] 77 | }, 78 | { 79 | path: 'driver', 80 | component: DriverComponent, 81 | canActivate: [ IsDriver ], 82 | children: [ 83 | { 84 | path: '', 85 | component: DriverDashboardComponent, 86 | resolve: { trips: TripListResolver } 87 | }, 88 | { 89 | path: ':id', 90 | component: DriverDetailComponent, 91 | resolve: { trip: TripDetailResolver } 92 | } 93 | ] 94 | }, 95 | { path: '', component: LandingComponent } 96 | ], { useHash: true }), 97 | AgmCoreModule.forRoot({ 98 | apiKey: environment.GOOGLE_API_KEY 99 | }), 100 | ToastrModule.forRoot() 101 | ], 102 | providers: [ 103 | AuthService, 104 | GoogleMapsService, 105 | IsDriver, 106 | IsRider, 107 | TripService, 108 | TripListResolver, 109 | TripDetailResolver 110 | ], 111 | bootstrap: [ AppComponent ] 112 | }) 113 | 114 | export class AppModule { } 115 | -------------------------------------------------------------------------------- /client/src/app/components/driver-dashboard/driver-dashboard.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/taxi-app/8f4e25bac1373ac2312d1b20398a48c21f6130cb/client/src/app/components/driver-dashboard/driver-dashboard.component.css -------------------------------------------------------------------------------- /client/src/app/components/driver-dashboard/driver-dashboard.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 8 | 13 | 18 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /client/src/app/components/driver-dashboard/driver-dashboard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, async } from '@angular/core/testing'; 2 | import { ActivatedRoute, Data } from '@angular/router'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | 5 | import { Observable, of } from 'rxjs'; 6 | import { ToastrModule } from 'ng6-toastr-notifications'; 7 | 8 | import { TripService } from '../../services/trip.service'; 9 | import { TripFactory } from '../../testing/factories'; 10 | import { DriverDashboardComponent } from './driver-dashboard.component'; 11 | import { TripCardComponent } from '../../components/trip-card/trip-card.component'; 12 | 13 | describe('DriverDashboardComponent', () => { 14 | let component: DriverDashboardComponent; 15 | let fixture: ComponentFixture; 16 | const trip1 = TripFactory.create({driver: null}); 17 | const trip2 = TripFactory.create({status: 'COMPLETED'}); 18 | const trip3 = TripFactory.create({status: 'IN_PROGRESS'}); 19 | 20 | class MockActivatedRoute { 21 | data: Observable = of({ 22 | trips: [trip1, trip2, trip3] 23 | }); 24 | } 25 | 26 | class MockTripService { 27 | messages: Observable = of(); 28 | connect(): void {} 29 | } 30 | 31 | beforeEach(() => { 32 | TestBed.configureTestingModule({ 33 | imports: [ 34 | RouterTestingModule.withRoutes([]), 35 | ToastrModule.forRoot() 36 | ], 37 | declarations: [ 38 | DriverDashboardComponent, 39 | TripCardComponent 40 | ], 41 | providers: [ 42 | { provide: ActivatedRoute, useClass: MockActivatedRoute }, 43 | { provide: TripService, useClass: MockTripService } 44 | ] 45 | }); 46 | fixture = TestBed.createComponent(DriverDashboardComponent); 47 | component = fixture.componentInstance; 48 | }); 49 | 50 | it('should get current trips', async(() => { 51 | fixture.whenStable().then(() => { 52 | fixture.detectChanges(); 53 | expect(component.currentTrips).toEqual([trip3]); 54 | }); 55 | component.ngOnInit(); 56 | })); 57 | 58 | it('should get requested trips', async(() => { 59 | fixture.whenStable().then(() => { 60 | fixture.detectChanges(); 61 | expect(component.requestedTrips).toEqual([trip1]); 62 | }); 63 | component.ngOnInit(); 64 | })); 65 | 66 | it('should get completed trips', async(() => { 67 | fixture.whenStable().then(() => { 68 | fixture.detectChanges(); 69 | expect(component.completedTrips).toEqual([trip2]); 70 | }); 71 | component.ngOnInit(); 72 | })); 73 | }); 74 | -------------------------------------------------------------------------------- /client/src/app/components/driver-dashboard/driver-dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { Subscription } from 'rxjs'; 4 | import { ToastrManager } from 'ng6-toastr-notifications'; 5 | import { Trip, TripService } from '../../services/trip.service'; 6 | 7 | @Component({ 8 | selector: 'app-driver-dashboard', 9 | templateUrl: './driver-dashboard.component.html', 10 | styleUrls: ['./driver-dashboard.component.css'] 11 | }) 12 | export class DriverDashboardComponent implements OnInit, OnDestroy { 13 | messages: Subscription; 14 | trips: Trip[]; 15 | 16 | constructor( 17 | private route: ActivatedRoute, 18 | private tripService: TripService, 19 | private toastr: ToastrManager 20 | ) {} 21 | 22 | get currentTrips(): Trip[] { 23 | return this.trips.filter(trip => { 24 | return trip.driver !== null && trip.status !== 'COMPLETED'; 25 | }); 26 | } 27 | 28 | get requestedTrips(): Trip[] { 29 | return this.trips.filter(trip => { 30 | return trip.status === 'REQUESTED'; 31 | }); 32 | } 33 | 34 | get completedTrips(): Trip[] { 35 | return this.trips.filter(trip => { 36 | return trip.status === 'COMPLETED'; 37 | }); 38 | } 39 | 40 | ngOnInit(): void { 41 | this.route.data.subscribe((data: {trips: Trip[]}) => this.trips = data.trips); 42 | this.tripService.connect(); 43 | this.messages = this.tripService.messages.subscribe((message: any) => { 44 | const trip: Trip = Trip.create(message.data); 45 | this.updateTrips(trip); 46 | this.updateToast(trip); 47 | }); 48 | } 49 | 50 | updateTrips(trip: Trip): void { 51 | this.trips = this.trips.filter(thisTrip => thisTrip.id !== trip.id); 52 | this.trips.push(trip); 53 | } 54 | 55 | updateToast(trip: Trip): void { 56 | if (trip.driver === null) { 57 | this.toastr.infoToastr(`Rider ${trip.rider.username} has requested a trip.`); 58 | } 59 | } 60 | 61 | ngOnDestroy(): void { 62 | this.messages.unsubscribe(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /client/src/app/components/driver-detail/driver-detail.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/taxi-app/8f4e25bac1373ac2312d1b20398a48c21f6130cb/client/src/app/components/driver-detail/driver-detail.component.css -------------------------------------------------------------------------------- /client/src/app/components/driver-detail/driver-detail.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 11 |
12 |

Trip

13 |
14 |
    15 |
  • 16 | 22 |
    23 |
    {{ trip.rider.username }}
    24 | {{ trip.pick_up_address }} to {{ trip.drop_off_address }}
    25 | {{ trip.status }} 26 |
    27 |
  • 28 |
29 |
30 | 54 |
55 |
56 |
57 | -------------------------------------------------------------------------------- /client/src/app/components/driver-detail/driver-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { ComponentFixture, TestBed, async } from '@angular/core/testing'; 3 | import { ActivatedRoute, Data } from '@angular/router'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | 6 | import { Observable, of } from 'rxjs'; 7 | 8 | import { TripService } from '../../services/trip.service'; 9 | import { TripFactory } from '../../testing/factories'; 10 | import { DriverDetailComponent } from './driver-detail.component'; 11 | 12 | describe('DriverDetailComponent', () => { 13 | let component: DriverDetailComponent; 14 | let fixture: ComponentFixture; 15 | let tripService: TripService; 16 | const trip = TripFactory.create(); 17 | 18 | class MockActivatedRoute { 19 | data: Observable = of({ 20 | trip 21 | }); 22 | } 23 | 24 | beforeEach(() => { 25 | TestBed.configureTestingModule({ 26 | imports: [ 27 | HttpClientTestingModule, 28 | RouterTestingModule.withRoutes([]) 29 | ], 30 | declarations: [ DriverDetailComponent ], 31 | providers: [ 32 | { provide: ActivatedRoute, useClass: MockActivatedRoute } 33 | ] 34 | }); 35 | fixture = TestBed.createComponent(DriverDetailComponent); 36 | component = fixture.componentInstance; 37 | tripService = TestBed.get(TripService); 38 | }); 39 | 40 | it('should update data on initialization', async(() => { 41 | fixture.whenStable().then(() => { 42 | fixture.detectChanges(); 43 | expect(component.trip).toEqual(trip); 44 | }); 45 | component.ngOnInit(); 46 | })); 47 | 48 | it('should update trip status', () => { 49 | const spyUpdateTrip = spyOn(tripService, 'updateTrip'); 50 | component.trip = TripFactory.create(); 51 | component.updateTripStatus('STARTED'); 52 | expect(spyUpdateTrip).toHaveBeenCalledWith(component.trip); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /client/src/app/components/driver-detail/driver-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | 4 | import { User } from '../../services/auth.service'; 5 | import { Trip, TripService } from '../../services/trip.service'; 6 | 7 | @Component({ 8 | selector: 'app-driver-detail', 9 | templateUrl: './driver-detail.component.html', 10 | styleUrls: ['./driver-detail.component.css'] 11 | }) 12 | export class DriverDetailComponent implements OnInit { 13 | trip: Trip; 14 | 15 | constructor( 16 | private route: ActivatedRoute, 17 | private tripService: TripService 18 | ) {} 19 | 20 | ngOnInit(): void { 21 | this.route.data.subscribe((data: {trip: Trip}) => this.trip = data.trip); 22 | } 23 | 24 | updateTripStatus(status: string): void { 25 | this.trip.driver = User.getUser(); 26 | this.trip.status = status; 27 | this.tripService.updateTrip(this.trip); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/src/app/components/driver/driver.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/taxi-app/8f4e25bac1373ac2312d1b20398a48c21f6130cb/client/src/app/components/driver/driver.component.css -------------------------------------------------------------------------------- /client/src/app/components/driver/driver.component.html: -------------------------------------------------------------------------------- 1 | 4 |
5 | 6 |
7 | -------------------------------------------------------------------------------- /client/src/app/components/driver/driver.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | 4 | import { DriverComponent } from './driver.component'; 5 | 6 | describe('DriverComponent', () => { 7 | let component: DriverComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [ 13 | RouterTestingModule.withRoutes([]) 14 | ], 15 | declarations: [ 16 | DriverComponent 17 | ] 18 | }) 19 | .compileComponents(); 20 | }); 21 | 22 | beforeEach(() => { 23 | fixture = TestBed.createComponent(DriverComponent); 24 | component = fixture.componentInstance; 25 | fixture.detectChanges(); 26 | }); 27 | 28 | it('should create', () => { 29 | expect(component).toBeTruthy(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /client/src/app/components/driver/driver.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-driver', 5 | templateUrl: './driver.component.html', 6 | styleUrls: ['./driver.component.css'] 7 | }) 8 | export class DriverComponent {} 9 | -------------------------------------------------------------------------------- /client/src/app/components/landing/landing.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/taxi-app/8f4e25bac1373ac2312d1b20398a48c21f6130cb/client/src/app/components/landing/landing.component.css -------------------------------------------------------------------------------- /client/src/app/components/landing/landing.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Taxi

3 |
4 | 5 | Dashboard 6 | 7 | 8 | 9 | Log in 10 | Sign up 11 | 12 |
13 | -------------------------------------------------------------------------------- /client/src/app/components/landing/landing.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpClientTestingModule, HttpTestingController, TestRequest 3 | } from '@angular/common/http/testing'; 4 | import { TestBed, ComponentFixture } from '@angular/core/testing'; 5 | import { RouterTestingModule } from '@angular/router/testing'; 6 | import { By } from '@angular/platform-browser'; 7 | import { DebugElement } from '@angular/core'; 8 | import { FormsModule } from '@angular/forms'; 9 | 10 | import { AuthService } from '../../services/auth.service'; 11 | import { UserFactory } from '../../testing/factories'; 12 | import { LandingComponent } from './landing.component'; 13 | 14 | describe('LandingComponent', () => { 15 | let logOutButton: DebugElement; 16 | let component: LandingComponent; 17 | let fixture: ComponentFixture; 18 | let httpMock: HttpTestingController; 19 | 20 | beforeEach(() => { 21 | TestBed.configureTestingModule({ 22 | imports: [ 23 | FormsModule, 24 | HttpClientTestingModule, 25 | RouterTestingModule.withRoutes([]) 26 | ], 27 | declarations: [ LandingComponent ], 28 | providers: [ AuthService ] 29 | }); 30 | fixture = TestBed.createComponent(LandingComponent); 31 | component = fixture.componentInstance; 32 | httpMock = TestBed.get(HttpTestingController); 33 | localStorage.setItem('taxi.user', JSON.stringify( 34 | UserFactory.create() 35 | )); 36 | fixture.detectChanges(); 37 | logOutButton = fixture.debugElement.query(By.css('button.btn.btn-primary')); 38 | }); 39 | 40 | it('should allow a user to log out of an account', () => { 41 | logOutButton.triggerEventHandler('click', null); 42 | const request: TestRequest = httpMock.expectOne('/api/log_out/'); 43 | request.flush({}); 44 | expect(localStorage.getItem('taxi.user')).toBeNull(); 45 | }); 46 | 47 | it('should indicate whether a user is logged in', () => { 48 | localStorage.clear(); 49 | expect(component.getUser()).toBeFalsy(); 50 | localStorage.setItem('taxi.user', JSON.stringify( 51 | UserFactory.create() 52 | )); 53 | expect(component.getUser()).toBeTruthy(); 54 | }); 55 | 56 | it('should return true if the user is a rider', () => { 57 | localStorage.clear(); 58 | localStorage.setItem('taxi.user', JSON.stringify( 59 | UserFactory.create({group: 'rider'}) 60 | )); 61 | expect(component.isRider()).toBeTruthy(); 62 | }); 63 | 64 | afterEach(() => { 65 | httpMock.verify(); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /client/src/app/components/landing/landing.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { AuthService, User } from '../../services/auth.service'; 4 | 5 | @Component({ 6 | selector: 'app-landing', 7 | templateUrl: './landing.component.html', 8 | styleUrls: ['./landing.component.css'] 9 | }) 10 | export class LandingComponent { 11 | constructor(private authService: AuthService) {} 12 | 13 | getUser(): User { 14 | return User.getUser(); 15 | } 16 | 17 | isRider(): boolean { 18 | return User.isRider(); 19 | } 20 | 21 | logOut(): void { 22 | this.authService.logOut().subscribe(() => {}, (error) => { 23 | console.error(error); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/src/app/components/log-in/log-in.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/taxi-app/8f4e25bac1373ac2312d1b20398a48c21f6130cb/client/src/app/components/log-in/log-in.component.css -------------------------------------------------------------------------------- /client/src/app/components/log-in/log-in.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
Log in
5 |
6 |
7 |
8 | 9 | 17 |
18 |
19 | 20 | 28 |
29 | 35 |
36 |
37 |
38 |

39 | Don't have an account? 40 | Sign up! 41 |

42 |
43 |
44 | -------------------------------------------------------------------------------- /client/src/app/components/log-in/log-in.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpClientTestingModule, HttpTestingController 3 | } from '@angular/common/http/testing'; 4 | import { TestBed, ComponentFixture } from '@angular/core/testing'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { Router } from '@angular/router'; 7 | import { RouterTestingModule } from '@angular/router/testing'; 8 | 9 | import { AuthService } from '../../services/auth.service'; 10 | import { UserFactory } from '../../testing/factories'; 11 | import { LogInComponent } from '../log-in/log-in.component'; 12 | 13 | describe('LogInComponent', () => { 14 | let component: LogInComponent; 15 | let fixture: ComponentFixture; 16 | let router: Router; 17 | let httpMock: HttpTestingController; 18 | 19 | beforeEach(() => { 20 | TestBed.configureTestingModule({ 21 | imports: [ 22 | FormsModule, 23 | HttpClientTestingModule, 24 | RouterTestingModule.withRoutes([]) 25 | ], 26 | declarations: [ LogInComponent ], 27 | providers: [ AuthService ] 28 | }); 29 | fixture = TestBed.createComponent(LogInComponent); 30 | component = fixture.componentInstance; 31 | router = TestBed.get(Router); 32 | httpMock = TestBed.get(HttpTestingController); 33 | }); 34 | 35 | it('should allow a user to log into an existing account', () => { 36 | const spy = spyOn(router, 'navigateByUrl'); 37 | const user = UserFactory.create(); 38 | component.user = {username: user.username, password: 'pAssw0rd!'}; 39 | component.onSubmit(); 40 | const request = httpMock.expectOne('/api/log_in/'); 41 | request.flush(user); 42 | expect(localStorage.getItem('taxi.user')).toEqual(JSON.stringify(user)); 43 | expect(spy).toHaveBeenCalledWith(''); 44 | }); 45 | 46 | afterEach(() => { 47 | httpMock.verify(); 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /client/src/app/components/log-in/log-in.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | import { AuthService } from '../../services/auth.service'; 5 | 6 | class UserData { 7 | constructor( 8 | public username?: string, 9 | public password?: string 10 | ) {} 11 | } 12 | 13 | @Component({ 14 | selector: 'app-log-in', 15 | templateUrl: './log-in.component.html', 16 | styleUrls: ['./log-in.component.css'] 17 | }) 18 | export class LogInComponent { 19 | user: UserData = new UserData(); 20 | constructor( 21 | private router: Router, 22 | private authService: AuthService 23 | ) {} 24 | onSubmit(): void { 25 | this.authService.logIn( 26 | this.user.username, this.user.password 27 | ).subscribe(user => { 28 | this.router.navigateByUrl(''); 29 | }, (error) => { 30 | console.error(error); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/src/app/components/rider-dashboard/rider-dashboard.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/taxi-app/8f4e25bac1373ac2312d1b20398a48c21f6130cb/client/src/app/components/rider-dashboard/rider-dashboard.component.css -------------------------------------------------------------------------------- /client/src/app/components/rider-dashboard/rider-dashboard.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 8 | 13 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /client/src/app/components/rider-dashboard/rider-dashboard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, async } from '@angular/core/testing'; 2 | import { ActivatedRoute, Data } from '@angular/router'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | 5 | import { Observable, of } from 'rxjs'; 6 | import { ToastrModule } from 'ng6-toastr-notifications'; 7 | 8 | import { TripService } from '../../services/trip.service'; 9 | import { TripFactory } from '../../testing/factories'; 10 | import { RiderDashboardComponent } from './rider-dashboard.component'; 11 | import { TripCardComponent } from '../../components/trip-card/trip-card.component'; 12 | 13 | describe('RiderDashboardComponent', () => { 14 | let component: RiderDashboardComponent; 15 | let fixture: ComponentFixture; 16 | const trip1 = TripFactory.create({driver: null}); 17 | const trip2 = TripFactory.create({status: 'COMPLETED'}); 18 | const trip3 = TripFactory.create(); 19 | 20 | class MockActivatedRoute { 21 | data: Observable = of({ 22 | trips: [trip1, trip2, trip3] 23 | }); 24 | } 25 | 26 | class MockTripService { 27 | messages: Observable = of(); 28 | connect(): void {} 29 | } 30 | 31 | beforeEach(() => { 32 | TestBed.configureTestingModule({ 33 | imports: [ 34 | RouterTestingModule.withRoutes([]), 35 | ToastrModule.forRoot() 36 | ], 37 | declarations: [ 38 | RiderDashboardComponent, 39 | TripCardComponent 40 | ], 41 | providers: [ 42 | { provide: ActivatedRoute, useClass: MockActivatedRoute }, 43 | { provide: TripService, useClass: MockTripService } 44 | ] 45 | }); 46 | fixture = TestBed.createComponent(RiderDashboardComponent); 47 | component = fixture.componentInstance; 48 | }); 49 | 50 | it('should get current trips', async(() => { 51 | fixture.whenStable().then(() => { 52 | fixture.detectChanges(); 53 | expect(component.currentTrips).toEqual([trip3]); 54 | }); 55 | component.ngOnInit(); 56 | })); 57 | 58 | it('should get completed trips', async(() => { 59 | fixture.whenStable().then(() => { 60 | fixture.detectChanges(); 61 | expect(component.completedTrips).toEqual([trip2]); 62 | }); 63 | component.ngOnInit(); 64 | })); 65 | }); 66 | -------------------------------------------------------------------------------- /client/src/app/components/rider-dashboard/rider-dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | 4 | import { Subscription } from 'rxjs'; 5 | import { ToastrManager } from 'ng6-toastr-notifications'; 6 | 7 | import { Trip, TripService } from '../../services/trip.service'; 8 | 9 | @Component({ 10 | selector: 'app-rider-dashboard', 11 | templateUrl: './rider-dashboard.component.html', 12 | styleUrls: ['./rider-dashboard.component.css'] 13 | }) 14 | export class RiderDashboardComponent implements OnInit, OnDestroy { 15 | messages: Subscription; 16 | trips: Trip[]; 17 | 18 | constructor( 19 | private route: ActivatedRoute, 20 | private tripService: TripService, 21 | private toastr: ToastrManager 22 | ) {} 23 | 24 | get currentTrips(): Trip[] { 25 | return this.trips.filter(trip => { 26 | return trip.driver !== null && trip.status !== 'COMPLETED'; 27 | }); 28 | } 29 | 30 | get completedTrips(): Trip[] { 31 | return this.trips.filter(trip => { 32 | return trip.status === 'COMPLETED'; 33 | }); 34 | } 35 | 36 | ngOnInit(): void { 37 | this.route.data.subscribe((data: {trips: Trip[]}) => this.trips = data.trips); 38 | this.tripService.connect(); 39 | this.messages = this.tripService.messages.subscribe((message: any) => { 40 | const trip: Trip = Trip.create(message.data); 41 | this.updateTrips(trip); 42 | this.updateToast(trip); 43 | }); 44 | } 45 | 46 | updateTrips(trip: Trip): void { 47 | this.trips = this.trips.filter(thisTrip => thisTrip.id !== trip.id); 48 | this.trips.push(trip); 49 | } 50 | 51 | updateToast(trip: Trip): void { 52 | if (trip.status === 'STARTED') { 53 | this.toastr.infoToastr(`Driver ${trip.driver.username} is coming to pick you up.`); 54 | } else if (trip.status === 'IN_PROGRESS') { 55 | this.toastr.infoToastr(`Driver ${trip.driver.username} is headed to your destination.`); 56 | } else if (trip.status === 'COMPLETED') { 57 | this.toastr.infoToastr(`Driver ${trip.driver.username} has dropped you off.`); 58 | } 59 | } 60 | 61 | ngOnDestroy(): void { 62 | this.messages.unsubscribe(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /client/src/app/components/rider-detail/rider-detail.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/taxi-app/8f4e25bac1373ac2312d1b20398a48c21f6130cb/client/src/app/components/rider-detail/rider-detail.component.css -------------------------------------------------------------------------------- /client/src/app/components/rider-detail/rider-detail.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 11 |
12 |

Trip

13 |
14 |
    15 |
  • 16 | 22 |
    23 |
    {{ trip.driver.username }}
    24 | {{ trip.pick_up_address }} to {{ trip.drop_off_address }}
    25 | {{ trip.status }} 26 |
    27 |
  • 28 |
29 |
30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /client/src/app/components/rider-detail/rider-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, async } from '@angular/core/testing'; 2 | import { ActivatedRoute, Data } from '@angular/router'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | 5 | import { Observable, of } from 'rxjs'; 6 | 7 | import { TripFactory } from '../../testing/factories'; 8 | import { RiderDetailComponent } from './rider-detail.component'; 9 | 10 | describe('RiderDetailComponent', () => { 11 | let component: RiderDetailComponent; 12 | let fixture: ComponentFixture; 13 | const trip = TripFactory.create(); 14 | 15 | class MockActivatedRoute { 16 | data: Observable = of({ 17 | trip 18 | }); 19 | } 20 | 21 | beforeEach(() => { 22 | TestBed.configureTestingModule({ 23 | imports: [ 24 | RouterTestingModule.withRoutes([]) 25 | ], 26 | declarations: [ RiderDetailComponent ], 27 | providers: [ 28 | { provide: ActivatedRoute, useClass: MockActivatedRoute } 29 | ] 30 | }); 31 | fixture = TestBed.createComponent(RiderDetailComponent); 32 | component = fixture.componentInstance; 33 | }); 34 | 35 | it('should update data on initialization', async(() => { 36 | fixture.whenStable().then(() => { 37 | fixture.detectChanges(); 38 | expect(component.trip).toEqual(trip); 39 | }); 40 | component.ngOnInit(); 41 | })); 42 | }); 43 | -------------------------------------------------------------------------------- /client/src/app/components/rider-detail/rider-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | 4 | import { Trip } from '../../services/trip.service'; 5 | 6 | @Component({ 7 | selector: 'app-rider-detail', 8 | templateUrl: './rider-detail.component.html', 9 | styleUrls: ['./rider-detail.component.css'] 10 | }) 11 | export class RiderDetailComponent implements OnInit { 12 | trip: Trip; 13 | 14 | constructor(private route: ActivatedRoute) {} 15 | 16 | ngOnInit(): void { 17 | this.route.data.subscribe( 18 | (data: {trip: Trip}) => this.trip = data.trip 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/src/app/components/rider-request/rider-request.component.css: -------------------------------------------------------------------------------- 1 | agm-map { 2 | height: 300px; 3 | } 4 | -------------------------------------------------------------------------------- /client/src/app/components/rider-request/rider-request.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 11 |
12 |
Request Trip
13 |
14 |
15 |
16 | 17 | 26 |
27 |
28 | 29 | 38 |
39 | 45 | 46 | 52 | 53 | 54 | 60 |
61 |
62 |
63 |
64 |
65 | -------------------------------------------------------------------------------- /client/src/app/components/rider-request/rider-request.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { Router } from '@angular/router'; 5 | import { RouterTestingModule } from '@angular/router/testing'; 6 | 7 | import { AgmCoreModule } from '@agm/core'; 8 | 9 | import { GoogleMapsService } from '../../services/google-maps.service'; 10 | import { TripService } from '../../services/trip.service'; 11 | import { TripFactory } from '../../testing/factories'; 12 | import { RiderRequestComponent } from './rider-request.component'; 13 | 14 | describe('RiderRequestComponent', () => { 15 | let component: RiderRequestComponent; 16 | let fixture: ComponentFixture; 17 | let tripService: TripService; 18 | let router: Router; 19 | 20 | class MockGoogleMapsService {} 21 | 22 | beforeEach(() => { 23 | TestBed.configureTestingModule({ 24 | imports: [ 25 | FormsModule, 26 | HttpClientTestingModule, 27 | RouterTestingModule.withRoutes([]), 28 | AgmCoreModule.forRoot({}) 29 | ], 30 | declarations: [ RiderRequestComponent ], 31 | providers: [ 32 | { provide: GoogleMapsService, useClass: MockGoogleMapsService } 33 | ] 34 | }); 35 | fixture = TestBed.createComponent(RiderRequestComponent); 36 | component = fixture.componentInstance; 37 | tripService = TestBed.get(TripService); 38 | router = TestBed.get(Router); 39 | }); 40 | 41 | it('should create', () => { 42 | expect(component).toBeTruthy(); 43 | }); 44 | 45 | it('should handle form submit', () => { 46 | const spyCreateTrip = spyOn(tripService, 'createTrip'); 47 | const spyNavigateByUrl = spyOn(router, 'navigateByUrl'); 48 | component.trip = TripFactory.create(); 49 | component.onSubmit(); 50 | expect(spyCreateTrip).toHaveBeenCalledWith(component.trip); 51 | expect(spyNavigateByUrl).toHaveBeenCalledWith('/rider'); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /client/src/app/components/rider-request/rider-request.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | import { User } from '../../services/auth.service'; 5 | import { GoogleMapsService } from '../../services/google-maps.service'; 6 | import { Trip, TripService } from '../../services/trip.service'; 7 | 8 | class Marker { 9 | constructor( 10 | public lat: number, 11 | public lng: number, 12 | public label?: string 13 | ) {} 14 | } 15 | 16 | @Component({ 17 | selector: 'app-rider-request', 18 | templateUrl: './rider-request.component.html', 19 | styleUrls: ['./rider-request.component.css'] 20 | }) 21 | export class RiderRequestComponent implements OnInit { 22 | trip: Trip = new Trip(); 23 | lat = 0; 24 | lng = 0; 25 | zoom = 13; 26 | markers: Marker[]; 27 | 28 | constructor( 29 | private googleMapsService: GoogleMapsService, 30 | private router: Router, 31 | private tripService: TripService 32 | ) {} 33 | 34 | ngOnInit(): void { 35 | if (navigator.geolocation) { 36 | navigator.geolocation.getCurrentPosition((position: Position) => { 37 | this.lat = position.coords.latitude; 38 | this.lng = position.coords.longitude; 39 | this.markers = [ 40 | new Marker(this.lat, this.lng) 41 | ]; 42 | }); 43 | } 44 | } 45 | 46 | onSubmit(): void { 47 | this.trip.rider = User.getUser(); 48 | this.tripService.createTrip(this.trip); 49 | this.router.navigateByUrl('/rider'); 50 | } 51 | 52 | onUpdate(): void { 53 | if ( 54 | !!this.trip.pick_up_address && 55 | !!this.trip.drop_off_address 56 | ) { 57 | this.googleMapsService.directions( 58 | this.trip.pick_up_address, 59 | this.trip.drop_off_address 60 | ).subscribe((data: any) => { 61 | const route: any = data.routes[0]; 62 | const leg: any = route.legs[0]; 63 | this.lat = leg.start_location.lat(); 64 | this.lng = leg.start_location.lng(); 65 | this.markers = [ 66 | { 67 | lat: leg.start_location.lat(), 68 | lng: leg.start_location.lng(), 69 | label: 'A' 70 | }, 71 | { 72 | lat: leg.end_location.lat(), 73 | lng: leg.end_location.lng(), 74 | label: 'B' 75 | } 76 | ]; 77 | }); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/src/app/components/rider/rider.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/taxi-app/8f4e25bac1373ac2312d1b20398a48c21f6130cb/client/src/app/components/rider/rider.component.css -------------------------------------------------------------------------------- /client/src/app/components/rider/rider.component.html: -------------------------------------------------------------------------------- 1 | 19 |
20 | 21 |
22 | -------------------------------------------------------------------------------- /client/src/app/components/rider/rider.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | 4 | import { RiderComponent } from './rider.component'; 5 | 6 | describe('RiderComponent', () => { 7 | let component: RiderComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [ 13 | RouterTestingModule.withRoutes([]) 14 | ], 15 | declarations: [ 16 | RiderComponent 17 | ] 18 | }) 19 | .compileComponents(); 20 | }); 21 | 22 | beforeEach(() => { 23 | fixture = TestBed.createComponent(RiderComponent); 24 | component = fixture.componentInstance; 25 | fixture.detectChanges(); 26 | }); 27 | 28 | it('should create', () => { 29 | expect(component).toBeTruthy(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /client/src/app/components/rider/rider.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-rider', 5 | templateUrl: './rider.component.html', 6 | styleUrls: ['./rider.component.css'] 7 | }) 8 | export class RiderComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /client/src/app/components/sign-up/sign-up.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/taxi-app/8f4e25bac1373ac2312d1b20398a48c21f6130cb/client/src/app/components/sign-up/sign-up.component.css -------------------------------------------------------------------------------- /client/src/app/components/sign-up/sign-up.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
Sign up
5 |
6 |
7 |
8 | 9 | 17 |
18 |
19 | 20 | 28 |
29 |
30 | 31 | 39 |
40 |
41 | 42 | 49 |
50 |
51 | 52 | 61 |
62 |
63 | 64 | 70 |
71 | 77 |
78 |
79 |
80 |

81 | Already have an account? 82 | Log in! 83 |

84 |
85 |
86 | -------------------------------------------------------------------------------- /client/src/app/components/sign-up/sign-up.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpClientTestingModule, HttpTestingController 3 | } from '@angular/common/http/testing'; 4 | import { TestBed, ComponentFixture } from '@angular/core/testing'; 5 | import { Router } from '@angular/router'; 6 | import { RouterTestingModule } from '@angular/router/testing'; 7 | import { FormsModule } from '@angular/forms'; 8 | 9 | import { AuthService } from '../../services/auth.service'; 10 | import { UserFactory } from '../../testing/factories'; 11 | import { SignUpComponent } from './sign-up.component'; 12 | 13 | describe('SignUpComponent', () => { 14 | let component: SignUpComponent; 15 | let fixture: ComponentFixture; 16 | let router: Router; 17 | let httpMock: HttpTestingController; 18 | 19 | beforeEach(() => { 20 | TestBed.configureTestingModule({ 21 | imports: [ 22 | FormsModule, 23 | HttpClientTestingModule, 24 | RouterTestingModule.withRoutes([]) 25 | ], 26 | declarations: [ SignUpComponent ], 27 | providers: [ AuthService ] 28 | }); 29 | fixture = TestBed.createComponent(SignUpComponent); 30 | component = fixture.componentInstance; 31 | router = TestBed.get(Router); 32 | httpMock = TestBed.get(HttpTestingController); 33 | }); 34 | 35 | it('should allow a user to sign up for an account', () => { 36 | const spy = spyOn(router, 'navigateByUrl'); 37 | const user = UserFactory.create(); 38 | const photo = new File(['photo'], user.photo, {type: 'image/jpeg'}); 39 | component.user = { 40 | username: user.username, 41 | firstName: user.first_name, 42 | lastName: user.last_name, 43 | password: 'pAssw0rd!', 44 | group: user.group, 45 | photo 46 | }; 47 | component.onSubmit(); 48 | const request = httpMock.expectOne('/api/sign_up/'); 49 | request.flush(user); 50 | expect(spy).toHaveBeenCalledWith('/log-in'); 51 | }); 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /client/src/app/components/sign-up/sign-up.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | import { AuthService } from '../../services/auth.service'; 5 | 6 | class UserData { 7 | constructor( 8 | public username?: string, 9 | public firstName?: string, 10 | public lastName?: string, 11 | public password?: string, 12 | public group?: string, 13 | public photo?: any 14 | ) {} 15 | } 16 | 17 | @Component({ 18 | selector: 'app-sign-up', 19 | templateUrl: './sign-up.component.html', 20 | styleUrls: ['./sign-up.component.css'] 21 | }) 22 | export class SignUpComponent { 23 | user: UserData = new UserData(); 24 | constructor( 25 | private router: Router, 26 | private authService: AuthService 27 | ) {} 28 | onChange(event): void { 29 | if (event.target.files && event.target.files.length > 0) { 30 | this.user.photo = event.target.files[0]; 31 | } 32 | } 33 | onSubmit(): void { 34 | this.authService.signUp( 35 | this.user.username, 36 | this.user.firstName, 37 | this.user.lastName, 38 | this.user.password, 39 | this.user.group, 40 | this.user.photo 41 | ).subscribe(() => { 42 | this.router.navigateByUrl('/log-in'); 43 | }, (error) => { 44 | console.error(error); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/src/app/components/trip-card/trip-card.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/taxi-app/8f4e25bac1373ac2312d1b20398a48c21f6130cb/client/src/app/components/trip-card/trip-card.component.css -------------------------------------------------------------------------------- /client/src/app/components/trip-card/trip-card.component.html: -------------------------------------------------------------------------------- 1 |

{{ title }}

2 |
3 | 4 |
5 |
    6 |
  • 7 | 8 |
    9 |
    {{ trip.otherUser.first_name }}
    10 | {{ trip.pick_up_address }} to {{ trip.drop_off_address }}
    11 | {{ trip.status }} 12 |
    13 |
  • 14 |
15 |
16 |
17 | 18 |
19 | No trips. 20 |
21 |
22 | -------------------------------------------------------------------------------- /client/src/app/components/trip-card/trip-card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | 4 | import { TripCardComponent } from './trip-card.component'; 5 | 6 | describe('TripCardComponent', () => { 7 | let component: TripCardComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [ 13 | RouterTestingModule.withRoutes([]) 14 | ], 15 | declarations: [ TripCardComponent ] 16 | }) 17 | .compileComponents(); 18 | })); 19 | 20 | beforeEach(() => { 21 | fixture = TestBed.createComponent(TripCardComponent); 22 | component = fixture.componentInstance; 23 | fixture.detectChanges(); 24 | }); 25 | 26 | it('should create', () => { 27 | expect(component).toBeTruthy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /client/src/app/components/trip-card/trip-card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { Trip } from '../../services/trip.service'; 3 | 4 | @Component({ 5 | selector: 'app-trip-card', 6 | templateUrl: './trip-card.component.html', 7 | styleUrls: ['./trip-card.component.css'] 8 | }) 9 | export class TripCardComponent { 10 | @Input() title: string; 11 | @Input() trips: Trip[]; 12 | constructor() {} 13 | } 14 | -------------------------------------------------------------------------------- /client/src/app/services/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpClientTestingModule, HttpTestingController 3 | } from '@angular/common/http/testing'; 4 | import { TestBed } from '@angular/core/testing'; 5 | 6 | import { AuthService, User } from './auth.service'; 7 | import { UserFactory } from '../testing/factories'; 8 | 9 | describe('Authentication using a service', () => { 10 | let authService: AuthService; 11 | let httpMock: HttpTestingController; 12 | 13 | beforeEach(() => { 14 | TestBed.configureTestingModule({ 15 | imports: [ HttpClientTestingModule ], 16 | providers: [ AuthService ] 17 | }); 18 | authService = TestBed.get(AuthService); 19 | httpMock = TestBed.get(HttpTestingController); 20 | }); 21 | 22 | it('should allow a user to sign up for a new account', () => { 23 | // Set up the data. 24 | const userData = UserFactory.create(); 25 | const photo = new File(['photo'], userData.photo, {type: 'image/jpeg'}); 26 | // Execute the function under test. 27 | authService.signUp( 28 | userData.username, 29 | userData.first_name, 30 | userData.last_name, 31 | 'pAssw0rd!', 32 | userData.group, 33 | photo 34 | ).subscribe(user => { 35 | expect(user).toBe(userData); 36 | }); 37 | const request = httpMock.expectOne('/api/sign_up/'); 38 | request.flush(userData); 39 | }); 40 | 41 | it('should allow a user to log in to an existing account', () => { 42 | // Set up the data. 43 | const userData = UserFactory.create(); 44 | // A successful login should write data to local storage. 45 | localStorage.clear(); 46 | // Execute the function under test. 47 | authService.logIn( 48 | userData.username, 'pAssw0rd!' 49 | ).subscribe(user => { 50 | expect(user).toBe(userData); 51 | }); 52 | const request = httpMock.expectOne('/api/log_in/'); 53 | request.flush(userData); 54 | // Confirm that the expected data was written to local storage. 55 | expect(localStorage.getItem('taxi.user')).toBe(JSON.stringify(userData)); 56 | }); 57 | 58 | it('should allow a user to log out', () => { 59 | // Set up the data. 60 | const userData = {}; 61 | // A successful logout should delete local storage data. 62 | localStorage.setItem('taxi.user', JSON.stringify({})); 63 | // Execute the function under test. 64 | authService.logOut().subscribe(user => { 65 | expect(user).toEqual(userData); 66 | }); 67 | const request = httpMock.expectOne('/api/log_out/'); 68 | request.flush(userData); 69 | // Confirm that the local storage data was deleted. 70 | expect(localStorage.getItem('taxi.user')).toBeNull(); 71 | }); 72 | 73 | it('should determine whether a user is logged in', () => { 74 | localStorage.clear(); 75 | expect(User.getUser()).toBeFalsy(); 76 | localStorage.setItem('taxi.user', JSON.stringify( 77 | UserFactory.create() 78 | )); 79 | expect(User.getUser()).toBeTruthy(); 80 | }); 81 | 82 | afterEach(() => { 83 | httpMock.verify(); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /client/src/app/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | import { Observable } from 'rxjs'; 5 | import { finalize, tap } from 'rxjs/operators'; 6 | 7 | export class User { 8 | constructor( 9 | public id?: number, 10 | public username?: string, 11 | public first_name?: string, 12 | public last_name?: string, 13 | public group?: string, 14 | public photo?: any 15 | ) {} 16 | 17 | static create(data: any): User { 18 | return new User( 19 | data.id, 20 | data.username, 21 | data.first_name, 22 | data.last_name, 23 | data.group, 24 | data.photo 25 | ); 26 | } 27 | 28 | static getUser(): User { 29 | const userData = localStorage.getItem('taxi.user'); 30 | if (userData) { 31 | return User.create(JSON.parse(userData)); 32 | } 33 | return null; 34 | } 35 | 36 | static isRider(): boolean { 37 | const user = User.getUser(); 38 | if (user === null) { 39 | return false; 40 | } 41 | return user.group === 'rider'; 42 | } 43 | 44 | static isDriver(): boolean { 45 | const user = User.getUser(); 46 | if (user === null) { 47 | return false; 48 | } 49 | return user.group === 'driver'; 50 | } 51 | } 52 | 53 | @Injectable({ 54 | providedIn: 'root' 55 | }) 56 | export class AuthService { 57 | constructor(private http: HttpClient) {} 58 | 59 | signUp( 60 | username: string, 61 | firstName: string, 62 | lastName: string, 63 | password: string, 64 | group: string, 65 | photo: any 66 | ): Observable { 67 | const url = '/api/sign_up/'; 68 | const formData = new FormData(); 69 | formData.append('username', username); 70 | formData.append('first_name', firstName); 71 | formData.append('last_name', lastName); 72 | formData.append('password1', password); 73 | formData.append('password2', password); 74 | formData.append('group', group); 75 | formData.append('photo', photo); 76 | return this.http.request('POST', url, {body: formData}); 77 | } 78 | 79 | logIn(username: string, password: string): Observable { 80 | const url = '/api/log_in/'; 81 | return this.http.post(url, {username, password}).pipe( 82 | tap(user => localStorage.setItem('taxi.user', JSON.stringify(user))) 83 | ); 84 | } 85 | 86 | logOut(): Observable { 87 | const url = '/api/log_out/'; 88 | return this.http.post(url, null).pipe( 89 | finalize(() => localStorage.removeItem('taxi.user')) 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /client/src/app/services/google-maps.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { GoogleMapsService } from './google-maps.service'; 4 | 5 | describe('GoogleMapsService', () => { 6 | let googleMapsService: GoogleMapsService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({ 10 | providers: [ GoogleMapsService ] 11 | }); 12 | googleMapsService = TestBed.get(GoogleMapsService); 13 | }); 14 | 15 | it('should exist', () => { 16 | expect(googleMapsService).toBeTruthy(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /client/src/app/services/google-maps.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | declare var google: any; 5 | 6 | @Injectable() 7 | export class GoogleMapsService { 8 | constructor() {} 9 | 10 | directions( 11 | pickUpAddress: string, 12 | dropOffAddress: string 13 | ): Observable { 14 | const request: any = { 15 | origin: pickUpAddress, 16 | destination: dropOffAddress, 17 | travelMode: 'DRIVING' 18 | }; 19 | const directionsService = new google.maps.DirectionsService(); 20 | return Observable.create(observer => { 21 | directionsService.route(request, (result, status) => { 22 | if (status === 'OK') { 23 | observer.next(result); 24 | } else { 25 | observer.error('Enter two valid addresses.'); 26 | } 27 | observer.complete(); 28 | }); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/src/app/services/is-driver.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { IsDriver } from './is-driver.service'; 2 | import { UserFactory } from '../testing/factories'; 3 | 4 | describe('IsDriver', () => { 5 | it('should allow a driver to access a route', () => { 6 | const isDriver: IsDriver = new IsDriver(); 7 | localStorage.setItem('taxi.user', JSON.stringify( 8 | UserFactory.create({group: 'driver'}) 9 | )); 10 | expect(isDriver.canActivate()).toBeTruthy(); 11 | }); 12 | it('should not allow a non-driver to access a route', () => { 13 | const isDriver: IsDriver = new IsDriver(); 14 | localStorage.setItem('taxi.user', JSON.stringify( 15 | UserFactory.create({group: 'rider'}) 16 | )); 17 | expect(isDriver.canActivate()).toBeFalsy(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /client/src/app/services/is-driver.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate } from '@angular/router'; 3 | import { User } from '../services/auth.service'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class IsDriver implements CanActivate { 9 | canActivate(): boolean { 10 | return User.isDriver(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/src/app/services/is-rider.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { IsRider } from './is-rider.service'; 2 | import { UserFactory } from '../testing/factories'; 3 | 4 | describe('IsRider', () => { 5 | 6 | it('should allow a rider to access a route', () => { 7 | const isRider: IsRider = new IsRider(); 8 | localStorage.setItem('taxi.user', JSON.stringify( 9 | UserFactory.create({group: 'rider'}) 10 | )); 11 | expect(isRider.canActivate()).toBeTruthy(); 12 | }); 13 | 14 | it('should not allow a non-rider to access a route', () => { 15 | const isRider: IsRider = new IsRider(); 16 | localStorage.setItem('taxi.user', JSON.stringify( 17 | UserFactory.create({group: 'driver'}) 18 | )); 19 | expect(isRider.canActivate()).toBeFalsy(); 20 | }); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /client/src/app/services/is-rider.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate } from '@angular/router'; 3 | import { User } from '../services/auth.service'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class IsRider implements CanActivate { 9 | canActivate(): boolean { 10 | return User.isRider(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/src/app/services/trip-detail.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { ActivatedRouteSnapshot } from '@angular/router'; 2 | 3 | import { Observable } from 'rxjs'; 4 | 5 | import { Trip } from '../services/trip.service'; 6 | import { TripDetailResolver } from './trip-detail.resolver'; 7 | import { TripFactory } from '../testing/factories'; 8 | 9 | describe('TripDetailResolver', () => { 10 | it('should resolve a trip', () => { 11 | const tripMock: Trip = TripFactory.create(); 12 | const tripServiceMock: any = { 13 | getTrip: (id: string): Observable => { 14 | return new Observable(observer => { 15 | observer.next(tripMock); 16 | observer.complete(); 17 | }); 18 | } 19 | }; 20 | const tripDetailResolver: TripDetailResolver = new TripDetailResolver(tripServiceMock); 21 | const route: ActivatedRouteSnapshot = new ActivatedRouteSnapshot(); 22 | route.params = {id: tripMock.id}; 23 | tripDetailResolver.resolve(route, null).subscribe(trip => { 24 | expect(trip).toBe(tripMock); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /client/src/app/services/trip-detail.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; 3 | 4 | import { Observable } from 'rxjs'; 5 | 6 | import { Trip, TripService } from '../services/trip.service'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class TripDetailResolver implements Resolve { 12 | constructor(private tripService: TripService) {} 13 | 14 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 15 | return this.tripService.getTrip(route.params.id); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/app/services/trip-list.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | 3 | import { Trip } from '../services/trip.service'; 4 | import { TripListResolver } from './trip-list.resolver'; 5 | import { TripFactory } from '../testing/factories'; 6 | 7 | describe('TripListResolver', () => { 8 | it('should resolve a list of trips', () => { 9 | const tripsMock: Trip[] = [ 10 | TripFactory.create(), 11 | TripFactory.create() 12 | ]; 13 | const tripServiceMock: any = { 14 | getTrips: (): Observable => { 15 | return of(tripsMock); 16 | } 17 | }; 18 | const tripListResolver: TripListResolver = new TripListResolver(tripServiceMock); 19 | tripListResolver.resolve(null, null).subscribe(trips => { 20 | expect(trips).toBe(tripsMock); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/app/services/trip-list.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, Resolve, RouterStateSnapshot 4 | } from '@angular/router'; 5 | 6 | import { Observable } from 'rxjs'; 7 | 8 | import { Trip, TripService } from '../services/trip.service'; 9 | 10 | @Injectable() 11 | export class TripListResolver implements Resolve { 12 | constructor(private tripService: TripService) {} 13 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 14 | return this.tripService.getTrips(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/src/app/services/trip.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpClientTestingModule, HttpTestingController, TestRequest 3 | } from '@angular/common/http/testing'; 4 | import { TestBed } from '@angular/core/testing'; 5 | 6 | import { TripService } from './trip.service'; 7 | import { TripFactory } from '../testing/factories'; 8 | 9 | describe('TripService', () => { 10 | let tripService: TripService; 11 | let httpMock: HttpTestingController; 12 | 13 | beforeEach(() => { 14 | TestBed.configureTestingModule({ 15 | imports: [ 16 | HttpClientTestingModule 17 | ], 18 | providers: [ TripService ] 19 | }); 20 | tripService = TestBed.get(TripService); 21 | httpMock = TestBed.get(HttpTestingController); 22 | }); 23 | 24 | it('should allow a user to get a list of trips', () => { 25 | const trip1 = TripFactory.create(); 26 | const trip2 = TripFactory.create(); 27 | tripService.getTrips().subscribe(trips => { 28 | expect(trips).toEqual([trip1, trip2]); 29 | }); 30 | const request: TestRequest = httpMock.expectOne('/api/trip/'); 31 | request.flush([ 32 | trip1, 33 | trip2 34 | ]); 35 | }); 36 | 37 | it('should allow a user to create a trip', () => { 38 | tripService.webSocket = jasmine.createSpyObj('webSocket', ['next']); 39 | const trip = TripFactory.create(); 40 | tripService.createTrip(trip); 41 | expect(tripService.webSocket.next).toHaveBeenCalledWith({ 42 | type: 'create.trip', 43 | data: { 44 | ...trip, rider: trip.rider.id 45 | } 46 | }); 47 | }); 48 | 49 | it('should allow a user to get a trip by ID', () => { 50 | const tripData = TripFactory.create(); 51 | tripService.getTrip(tripData.id).subscribe(trip => { 52 | expect(trip).toEqual(tripData); 53 | }); 54 | const request: TestRequest = httpMock.expectOne(`/api/trip/${tripData.id}/`); 55 | request.flush(tripData); 56 | }); 57 | 58 | it('should allow a user to update a trip', () => { 59 | tripService.webSocket = jasmine.createSpyObj('webSocket', ['next']); 60 | const trip = TripFactory.create({status: 'IN_PROGRESS'}); 61 | tripService.updateTrip(trip); 62 | expect(tripService.webSocket.next).toHaveBeenCalledWith({ 63 | type: 'update.trip', 64 | data: { 65 | ...trip, driver: trip.driver.id, rider: trip.rider.id 66 | } 67 | }); 68 | }); 69 | 70 | afterEach(() => { 71 | httpMock.verify(); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /client/src/app/services/trip.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | import { Observable } from 'rxjs'; 5 | import { WebSocketSubject } from 'rxjs/webSocket'; 6 | import { map, share } from 'rxjs/operators'; 7 | 8 | import { User } from './auth.service'; 9 | 10 | export class Trip { 11 | public otherUser: User; 12 | 13 | constructor( 14 | public id?: string, 15 | public created?: string, 16 | public updated?: string, 17 | public pick_up_address?: string, 18 | public drop_off_address?: string, 19 | public status?: string, 20 | public driver?: any, 21 | public rider?: any 22 | ) { 23 | this.otherUser = User.isRider() ? this.driver : this.rider; 24 | } 25 | 26 | static create(data: any): Trip { 27 | return new Trip( 28 | data.id, 29 | data.created, 30 | data.updated, 31 | data.pick_up_address, 32 | data.drop_off_address, 33 | data.status, 34 | data.driver ? User.create(data.driver) : null, 35 | User.create(data.rider) 36 | ); 37 | } 38 | } 39 | 40 | @Injectable({ 41 | providedIn: 'root' 42 | }) 43 | export class TripService { 44 | 45 | webSocket: WebSocketSubject; 46 | messages: Observable; 47 | 48 | constructor( 49 | private http: HttpClient 50 | ) {} 51 | 52 | connect(): void { 53 | if (!this.webSocket || this.webSocket.closed) { 54 | this.webSocket = new WebSocketSubject('ws://localhost:8080/taxi/'); 55 | this.messages = this.webSocket.pipe(share()); 56 | this.messages.subscribe(message => console.log(message)); 57 | } 58 | } 59 | 60 | getTrips(): Observable { 61 | return this.http.get('/api/trip/').pipe( 62 | map(trips => trips.map(trip => Trip.create(trip))) 63 | ); 64 | } 65 | 66 | createTrip(trip: Trip): void { 67 | this.connect(); 68 | const message: any = { 69 | type: 'create.trip', 70 | data: { 71 | ...trip, rider: trip.rider.id 72 | } 73 | }; 74 | this.webSocket.next(message); 75 | } 76 | 77 | getTrip(id: string): Observable { 78 | return this.http.get(`/api/trip/${id}/`).pipe( 79 | map(trip => Trip.create(trip)) 80 | ); 81 | } 82 | 83 | updateTrip(trip: Trip): void { 84 | this.connect(); 85 | const message: any = { 86 | type: 'update.trip', 87 | data: { 88 | ...trip, driver: trip.driver.id, rider: trip.rider.id 89 | } 90 | }; 91 | this.webSocket.next(message); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /client/src/app/testing/factories.ts: -------------------------------------------------------------------------------- 1 | import * as faker from 'faker'; 2 | 3 | import { User } from '../services/auth.service'; 4 | import { Trip } from '../services/trip.service'; 5 | 6 | export class UserFactory { 7 | static create(data?: object): User { 8 | return User.create(Object.assign({ 9 | id: faker.random.number(), 10 | username: faker.internet.email(), 11 | first_name: faker.name.firstName(), 12 | last_name: faker.name.lastName(), 13 | group: 'rider', 14 | photo: faker.image.imageUrl() 15 | }, data)); 16 | } 17 | } 18 | 19 | export class TripFactory { 20 | static create(data?: object): Trip { 21 | return Trip.create(Object.assign({ 22 | id: faker.random.uuid(), 23 | created: faker.date.past(), 24 | updated: faker.date.past(), 25 | pick_up_address: faker.address.streetAddress(), 26 | drop_off_address: faker.address.streetAddress(), 27 | status: 'REQUESTED', 28 | driver: UserFactory.create({group: 'driver'}), 29 | rider: UserFactory.create() 30 | }, data)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/taxi-app/8f4e25bac1373ac2312d1b20398a48c21f6130cb/client/src/assets/.gitkeep -------------------------------------------------------------------------------- /client/src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /client/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /client/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | GOOGLE_API_KEY: 'AIzaSyCzvPIX8w3OYxUC1oyqFn_2AjGu3dYLzMk' 4 | }; 5 | -------------------------------------------------------------------------------- /client/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/taxi-app/8f4e25bac1373ac2312d1b20398a48c21f6130cb/client/src/favicon.ico -------------------------------------------------------------------------------- /client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Taxi 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /client/src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // client/src/karma.conf.js 2 | // Karma configuration file, see link for more information 3 | // https://karma-runner.github.io/1.0/config/configuration-file.html 4 | 5 | module.exports = function (config) { 6 | config.set({ 7 | basePath: '', 8 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 9 | plugins: [ 10 | require('karma-jasmine'), 11 | require('karma-chrome-launcher'), 12 | require('karma-jasmine-html-reporter'), 13 | require('karma-coverage-istanbul-reporter'), 14 | require('@angular-devkit/build-angular/plugins/karma') 15 | ], 16 | client: { 17 | clearContext: false // leave Jasmine Spec Runner output visible in browser 18 | }, 19 | coverageIstanbulReporter: { 20 | dir: require('path').join(__dirname, '../coverage'), 21 | reports: ['html', 'lcovonly'], 22 | fixWebpackSourcePaths: true 23 | }, 24 | reporters: ['progress', 'kjhtml'], 25 | port: 9876, 26 | colors: true, 27 | logLevel: config.LOG_INFO, 28 | autoWatch: true, 29 | browsers: ['ChromeHeadlessCustom'], 30 | customLaunchers: { 31 | 'ChromeHeadlessCustom': { 32 | base: 'Chrome', 33 | flags: [ 34 | '--no-sandbox', 35 | '--headless', 36 | '--disable-gpu', 37 | '--disable-translate', 38 | '--disable-extensions', 39 | '--remote-debugging-port=9222' 40 | ] 41 | } 42 | }, 43 | singleRun: false 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /client/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /client/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /client/src/styles.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Patua+One"); 2 | 3 | .logo { 4 | font-family: "Patua One", sans-serif; 5 | font-weight: 400; 6 | } 7 | 8 | .landing { 9 | font-size: 72px; 10 | } 11 | 12 | .middle-center { 13 | top: 0; 14 | right: 0; 15 | bottom: 0; 16 | left: 0; 17 | width: 100%; 18 | height: 120px; 19 | margin: auto auto; 20 | position: absolute; 21 | text-align: center; 22 | } 23 | -------------------------------------------------------------------------------- /client/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /client/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /client/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /client/src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "array-type": false, 8 | "arrow-parens": false, 9 | "deprecation": { 10 | "severity": "warn" 11 | }, 12 | "import-blacklist": [ 13 | true, 14 | "rxjs/Rx" 15 | ], 16 | "interface-name": false, 17 | "max-classes-per-file": false, 18 | "max-line-length": [ 19 | true, 20 | 140 21 | ], 22 | "member-access": false, 23 | "member-ordering": [ 24 | true, 25 | { 26 | "order": [ 27 | "static-field", 28 | "instance-field", 29 | "static-method", 30 | "instance-method" 31 | ] 32 | } 33 | ], 34 | "no-consecutive-blank-lines": false, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-empty": false, 44 | "no-inferrable-types": [ 45 | true, 46 | "ignore-params" 47 | ], 48 | "no-non-null-assertion": true, 49 | "no-redundant-jsdoc": true, 50 | "no-switch-case-fall-through": true, 51 | "no-use-before-declare": true, 52 | "no-var-requires": false, 53 | "object-literal-key-quotes": [ 54 | true, 55 | "as-needed" 56 | ], 57 | "object-literal-sort-keys": false, 58 | "ordered-imports": false, 59 | "quotemark": [ 60 | true, 61 | "single" 62 | ], 63 | "trailing-comma": false, 64 | "no-output-on-prefix": true, 65 | "use-input-property-decorator": true, 66 | "use-output-property-decorator": true, 67 | "use-host-property-decorator": true, 68 | "no-input-rename": true, 69 | "no-output-rename": true, 70 | "use-life-cycle-interface": true, 71 | "use-pipe-transform-interface": true, 72 | "component-class-suffix": true, 73 | "directive-class-suffix": true 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | 5 | taxi-redis: 6 | container_name: taxi-redis 7 | image: redis:5.0.3-alpine 8 | 9 | taxi-server: 10 | build: 11 | context: ./server 12 | volumes: 13 | - media:/usr/src/app/media 14 | - static:/usr/src/app/static 15 | command: daphne --bind 0.0.0.0 --port 8000 taxi.asgi:application 16 | container_name: taxi-server 17 | depends_on: 18 | - taxi-redis 19 | environment: 20 | - REDIS_URL=redis://taxi-redis:6379/0 21 | ports: 22 | - 8001:8000 23 | 24 | taxi-client: 25 | build: 26 | context: ./client 27 | command: ng serve --host 0.0.0.0 28 | container_name: taxi-client 29 | depends_on: 30 | - taxi-server 31 | environment: 32 | - CHROME_BIN=chromium-browser 33 | ports: 34 | - 4201:4200 35 | 36 | nginx: 37 | build: 38 | context: ./nginx 39 | container_name: taxi-nginx 40 | depends_on: 41 | - taxi-server 42 | - taxi-client 43 | ports: 44 | - 8080:80 45 | restart: always 46 | volumes: 47 | - media:/usr/src/app/media 48 | - static:/usr/src/app/static 49 | 50 | 51 | volumes: 52 | media: 53 | static: 54 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.15.9-alpine 2 | 3 | RUN rm /etc/nginx/conf.d/default.conf 4 | 5 | COPY /include.websocket /etc/nginx/app/include.websocket 6 | COPY /include.forwarded /etc/nginx/app/include.forwarded 7 | COPY /dev.conf /etc/nginx/conf.d 8 | -------------------------------------------------------------------------------- /nginx/dev.conf: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | # Listen on port 80 4 | listen 80; 5 | 6 | # Redirect all media requests to a directory on the server 7 | location /media { 8 | alias /usr/src/app/media; 9 | } 10 | 11 | # Redirect all static requests to a directory on the server 12 | location /static { 13 | alias /usr/src/app/static; 14 | } 15 | 16 | # Redirect any requests to admin, api, or taxi 17 | # to the Django server 18 | location ~ ^/(admin|api|taxi) { 19 | proxy_pass http://taxi-server:8000; 20 | proxy_redirect default; 21 | include /etc/nginx/app/include.websocket; 22 | include /etc/nginx/app/include.forwarded; 23 | } 24 | 25 | # Redirect any other requests to the Angular server 26 | location / { 27 | proxy_pass http://taxi-client:4200; 28 | proxy_redirect default; 29 | include /etc/nginx/app/include.websocket; 30 | include /etc/nginx/app/include.forwarded; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /nginx/include.forwarded: -------------------------------------------------------------------------------- 1 | proxy_set_header Host $host; 2 | proxy_set_header X-Real-IP $remote_addr; 3 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 4 | proxy_set_header X-Forwarded-Host $server_name; 5 | -------------------------------------------------------------------------------- /nginx/include.websocket: -------------------------------------------------------------------------------- 1 | proxy_http_version 1.1; 2 | proxy_set_header Upgrade $http_upgrade; 3 | proxy_set_header Connection "upgrade"; 4 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = taxi.settings 3 | python_files = test_websockets.py 4 | -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dockerignore 3 | *.pyc 4 | __pycache__ 5 | env 6 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.2-alpine 2 | 3 | RUN apk update && apk add build-base python-dev py-pip jpeg-dev zlib-dev 4 | ENV LIBRARY_PATH=/lib:/usr/lib 5 | 6 | WORKDIR /usr/src/app 7 | 8 | COPY ./requirements.txt /usr/src/app 9 | 10 | RUN pip install --upgrade pip 11 | RUN pip install -r requirements.txt 12 | 13 | COPY . /usr/src/app 14 | 15 | WORKDIR /usr/src/app/taxi 16 | 17 | RUN python manage.py collectstatic --noinput 18 | -------------------------------------------------------------------------------- /server/media/test.txt: -------------------------------------------------------------------------------- 1 | This is some test text... -------------------------------------------------------------------------------- /server/requirements.txt: -------------------------------------------------------------------------------- 1 | channels-redis==2.3.3 2 | djangorestframework==3.9.2 3 | nose==1.3.7 4 | Pillow==6.0.0 5 | pytest-asyncio==0.10.0 6 | pytest-django==3.4.8 7 | -------------------------------------------------------------------------------- /server/start.sh: -------------------------------------------------------------------------------- 1 | # server/start.sh 2 | #!/bin/bash 3 | 4 | python ./taxi/manage.py migrate 5 | python ./taxi/manage.py collectstatic --noinput 6 | cd taxi && daphne --bind 0.0.0.0 --port 8000 taxi.asgi:application 7 | -------------------------------------------------------------------------------- /server/taxi/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', 'taxi.settings') 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /server/taxi/taxi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/taxi-app/8f4e25bac1373ac2312d1b20398a48c21f6130cb/server/taxi/taxi/__init__.py -------------------------------------------------------------------------------- /server/taxi/taxi/asgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | import django 3 | 4 | from channels.routing import get_default_application 5 | 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'taxi.settings') 7 | 8 | django.setup() 9 | 10 | application = get_default_application() 11 | -------------------------------------------------------------------------------- /server/taxi/taxi/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from channels.auth import AuthMiddlewareStack 4 | from channels.routing import ProtocolTypeRouter, URLRouter 5 | 6 | from trips.consumers import TaxiConsumer 7 | 8 | application = ProtocolTypeRouter({ 9 | 'websocket': AuthMiddlewareStack( 10 | URLRouter([ 11 | path('taxi/', TaxiConsumer), 12 | ]) 13 | ) 14 | }) 15 | -------------------------------------------------------------------------------- /server/taxi/taxi/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for taxi project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.1.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '(uj81jjpj7**=&i9ipekr&n8_$s$7t(3u93*q3d0vkv#v6%#+y' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'channels', 41 | 'rest_framework', 42 | 'trips', 43 | ] 44 | 45 | AUTH_USER_MODEL = 'trips.User' 46 | 47 | MIDDLEWARE = [ 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'django.middleware.common.CommonMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | ] 56 | 57 | ROOT_URLCONF = 'taxi.urls' 58 | 59 | TEMPLATES = [ 60 | { 61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 62 | 'DIRS': [], 63 | 'APP_DIRS': True, 64 | 'OPTIONS': { 65 | 'context_processors': [ 66 | 'django.template.context_processors.debug', 67 | 'django.template.context_processors.request', 68 | 'django.contrib.auth.context_processors.auth', 69 | 'django.contrib.messages.context_processors.messages', 70 | ], 71 | }, 72 | }, 73 | ] 74 | 75 | WSGI_APPLICATION = 'taxi.wsgi.application' 76 | 77 | ASGI_APPLICATION = 'taxi.routing.application' 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.sqlite3', 85 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 86 | } 87 | } 88 | 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 105 | }, 106 | ] 107 | 108 | 109 | # Internationalization 110 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 111 | 112 | LANGUAGE_CODE = 'en-us' 113 | 114 | TIME_ZONE = 'UTC' 115 | 116 | USE_I18N = True 117 | 118 | USE_L10N = True 119 | 120 | USE_TZ = True 121 | 122 | 123 | # Static files (CSS, JavaScript, Images) 124 | # https://docs.djangoproject.com/en/2.1/howto/static-files/ 125 | 126 | STATIC_URL = '/static/' 127 | 128 | STATIC_ROOT = os.path.join(BASE_DIR, '../static') 129 | 130 | MEDIA_URL = '/media/' 131 | 132 | MEDIA_ROOT = os.path.join(BASE_DIR, '../media') 133 | 134 | REDIS_URL = os.getenv('REDIS_URL', 'redis://localhost:6379') 135 | 136 | CHANNEL_LAYERS = { 137 | 'default': { 138 | 'BACKEND': 'channels_redis.core.RedisChannelLayer', 139 | 'CONFIG': { 140 | 'hosts': [REDIS_URL], 141 | }, 142 | }, 143 | } 144 | -------------------------------------------------------------------------------- /server/taxi/taxi/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | 4 | from trips.views import SignUpView, LogInView, LogOutView 5 | 6 | urlpatterns = [ 7 | path('admin/', admin.site.urls), 8 | path('api/sign_up/', SignUpView.as_view(), name='sign_up'), 9 | path('api/log_in/', LogInView.as_view(), name='log_in'), 10 | path('api/log_out/', LogOutView.as_view(), name='log_out'), 11 | path('api/trip/', include('trips.urls', 'trip',)), 12 | ] 13 | -------------------------------------------------------------------------------- /server/taxi/taxi/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for taxi project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'taxi.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /server/taxi/trips/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/taxi-app/8f4e25bac1373ac2312d1b20398a48c21f6130cb/server/taxi/trips/__init__.py -------------------------------------------------------------------------------- /server/taxi/trips/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin 3 | 4 | from .models import Trip, User 5 | 6 | 7 | @admin.register(User) 8 | class UserAdmin(DefaultUserAdmin): 9 | pass 10 | 11 | 12 | @admin.register(Trip) 13 | class TripAdmin(admin.ModelAdmin): 14 | fields = ( 15 | 'id', 'pick_up_address', 'drop_off_address', 'status', 16 | 'driver', 'rider', 17 | 'created', 'updated', 18 | ) 19 | list_display = ( 20 | 'id', 'pick_up_address', 'drop_off_address', 'status', 21 | 'driver', 'rider', 22 | 'created', 'updated', 23 | ) 24 | list_filter = ( 25 | 'status', 26 | ) 27 | readonly_fields = ( 28 | 'id', 'created', 'updated', 29 | ) 30 | -------------------------------------------------------------------------------- /server/taxi/trips/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TripsConfig(AppConfig): 5 | name = 'trips' 6 | -------------------------------------------------------------------------------- /server/taxi/trips/consumers.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from channels.db import database_sync_to_async 4 | from channels.generic.websocket import AsyncJsonWebsocketConsumer 5 | 6 | from trips.models import Trip 7 | from trips.serializers import ReadOnlyTripSerializer, TripSerializer 8 | 9 | 10 | class TaxiConsumer(AsyncJsonWebsocketConsumer): 11 | 12 | def __init__(self, scope): 13 | super().__init__(scope) 14 | 15 | # Keep track of the user's trips. 16 | self.trips = set() 17 | 18 | async def connect(self): 19 | user = self.scope['user'] 20 | if user.is_anonymous: 21 | await self.close() 22 | else: 23 | channel_groups = [] 24 | 25 | # Add a driver to the 'drivers' group. 26 | user_group = await self._get_user_group(self.scope['user']) 27 | if user_group == 'driver': 28 | channel_groups.append(self.channel_layer.group_add( 29 | group='drivers', 30 | channel=self.channel_name 31 | )) 32 | 33 | self.trips = set([ 34 | str(trip_id) for trip_id in await self._get_trips(self.scope['user']) 35 | ]) 36 | for trip in self.trips: 37 | channel_groups.append(self.channel_layer.group_add(trip, self.channel_name)) 38 | asyncio.gather(*channel_groups) 39 | 40 | await self.accept() 41 | 42 | async def receive_json(self, content, **kwargs): 43 | message_type = content.get('type') 44 | print(message_type) 45 | if message_type == 'create.trip': 46 | await self.create_trip(content) 47 | elif message_type == 'update.trip': 48 | await self.update_trip(content) 49 | 50 | async def echo_message(self, event): 51 | await self.send_json(event) 52 | 53 | async def create_trip(self, event): 54 | trip = await self._create_trip(event.get('data')) 55 | trip_id = f'{trip.id}' 56 | trip_data = ReadOnlyTripSerializer(trip).data 57 | 58 | # Send rider requests to all drivers. 59 | await self.channel_layer.group_send(group='drivers', message={ 60 | 'type': 'echo.message', 61 | 'data': trip_data 62 | }) 63 | 64 | if trip_id not in self.trips: 65 | self.trips.add(trip_id) 66 | await self.channel_layer.group_add( 67 | group=trip_id, 68 | channel=self.channel_name 69 | ) 70 | 71 | await self.send_json({ 72 | 'type': 'MESSAGE', 73 | 'data': trip_data 74 | }) 75 | 76 | async def update_trip(self, event): 77 | trip = await self._update_trip(event.get('data')) 78 | trip_id = f'{trip.id}' 79 | trip_data = ReadOnlyTripSerializer(trip).data 80 | 81 | # Send updates to riders that subscribe to this trip. 82 | await self.channel_layer.group_send(group=trip_id, message={ 83 | 'type': 'echo.message', 84 | 'data': trip_data 85 | }) 86 | 87 | if trip_id not in self.trips: 88 | self.trips.add(trip_id) 89 | await self.channel_layer.group_add( 90 | group=trip_id, 91 | channel=self.channel_name 92 | ) 93 | 94 | await self.send_json({ 95 | 'type': 'MESSAGE', 96 | 'data': trip_data 97 | }) 98 | 99 | async def disconnect(self, code): 100 | channel_groups = [ 101 | self.channel_layer.group_discard( 102 | group=trip, 103 | channel=self.channel_name 104 | ) 105 | for trip in self.trips 106 | ] 107 | 108 | # Discard driver from 'drivers' group. 109 | user_group = await self._get_user_group(self.scope['user']) 110 | if user_group == 'driver': 111 | channel_groups.append(self.channel_layer.group_discard( 112 | group='drivers', 113 | channel=self.channel_name 114 | )) 115 | 116 | asyncio.gather(*channel_groups) 117 | self.trips.clear() 118 | 119 | await super().disconnect(code) 120 | 121 | @database_sync_to_async 122 | def _create_trip(self, content): 123 | serializer = TripSerializer(data=content) 124 | serializer.is_valid(raise_exception=True) 125 | trip = serializer.create(serializer.validated_data) 126 | return trip 127 | 128 | @database_sync_to_async 129 | def _get_trips(self, user): 130 | if not user.is_authenticated: 131 | raise Exception('User is not authenticated.') 132 | user_groups = user.groups.values_list('name', flat=True) 133 | if 'driver' in user_groups: 134 | return user.trips_as_driver.exclude( 135 | status=Trip.COMPLETED 136 | ).only('id').values_list('id', flat=True) 137 | else: 138 | return user.trips_as_rider.exclude( 139 | status=Trip.COMPLETED 140 | ).only('id').values_list('id', flat=True) 141 | 142 | @database_sync_to_async 143 | def _get_user_group(self, user): 144 | if not user.is_authenticated: 145 | raise Exception('User is not authenticated.') 146 | return user.groups.first().name 147 | 148 | @database_sync_to_async 149 | def _update_trip(self, content): 150 | instance = Trip.objects.get(id=content.get('id')) 151 | serializer = TripSerializer(data=content) 152 | serializer.is_valid(raise_exception=True) 153 | trip = serializer.update(instance, serializer.validated_data) 154 | return trip 155 | -------------------------------------------------------------------------------- /server/taxi/trips/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-03-08 23:25 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('auth', '0009_alter_user_last_name_max_length'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='User', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('password', models.CharField(max_length=128, verbose_name='password')), 23 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 24 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 25 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 26 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 27 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 28 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 29 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 30 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 31 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 32 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 33 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 34 | ], 35 | options={ 36 | 'verbose_name': 'user', 37 | 'verbose_name_plural': 'users', 38 | 'abstract': False, 39 | }, 40 | managers=[ 41 | ('objects', django.contrib.auth.models.UserManager()), 42 | ], 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /server/taxi/trips/migrations/0002_trip.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-03-09 01:12 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('trips', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Trip', 16 | fields=[ 17 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 18 | ('created', models.DateTimeField(auto_now_add=True)), 19 | ('updated', models.DateTimeField(auto_now=True)), 20 | ('pick_up_address', models.CharField(max_length=255)), 21 | ('drop_off_address', models.CharField(max_length=255)), 22 | ('status', models.CharField(choices=[('REQUESTED', 'REQUESTED'), ('STARTED', 'STARTED'), ('IN_PROGRESS', 'IN_PROGRESS'), ('COMPLETED', 'COMPLETED')], default='REQUESTED', max_length=20)), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /server/taxi/trips/migrations/0003_trip_driver_rider.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-03-09 01:48 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('trips', '0002_trip'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='trip', 17 | name='driver', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='trips_as_driver', to=settings.AUTH_USER_MODEL), 19 | ), 20 | migrations.AddField( 21 | model_name='trip', 22 | name='rider', 23 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='trips_as_rider', to=settings.AUTH_USER_MODEL), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /server/taxi/trips/migrations/0004_user_photo.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-03-09 03:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('trips', '0003_trip_driver_rider'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='user', 15 | name='photo', 16 | field=models.ImageField(blank=True, null=True, upload_to='photos'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /server/taxi/trips/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/taxi-app/8f4e25bac1373ac2312d1b20398a48c21f6130cb/server/taxi/trips/migrations/__init__.py -------------------------------------------------------------------------------- /server/taxi/trips/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.conf import settings 4 | from django.db import models 5 | from django.contrib.auth.models import AbstractUser 6 | from django.shortcuts import reverse 7 | 8 | 9 | class User(AbstractUser): 10 | photo = models.ImageField(upload_to='photos', null=True, blank=True) 11 | 12 | @property 13 | def group(self): 14 | groups = self.groups.all() 15 | return groups[0].name if groups else None 16 | 17 | 18 | class Trip(models.Model): 19 | REQUESTED = 'REQUESTED' 20 | STARTED = 'STARTED' 21 | IN_PROGRESS = 'IN_PROGRESS' 22 | COMPLETED = 'COMPLETED' 23 | STATUSES = ( 24 | (REQUESTED, REQUESTED), 25 | (STARTED, STARTED), 26 | (IN_PROGRESS, IN_PROGRESS), 27 | (COMPLETED, COMPLETED), 28 | ) 29 | 30 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 31 | created = models.DateTimeField(auto_now_add=True) 32 | updated = models.DateTimeField(auto_now=True) 33 | pick_up_address = models.CharField(max_length=255) 34 | drop_off_address = models.CharField(max_length=255) 35 | status = models.CharField(max_length=20, choices=STATUSES, default=REQUESTED) 36 | driver = models.ForeignKey( 37 | settings.AUTH_USER_MODEL, 38 | null=True, 39 | blank=True, 40 | on_delete=models.DO_NOTHING, 41 | related_name='trips_as_driver' 42 | ) 43 | rider = models.ForeignKey( 44 | settings.AUTH_USER_MODEL, 45 | null=True, 46 | blank=True, 47 | on_delete=models.DO_NOTHING, 48 | related_name='trips_as_rider' 49 | ) 50 | 51 | def __str__(self): 52 | return f'{self.id}' 53 | 54 | def get_absolute_url(self): 55 | return reverse('trip:trip_detail', kwargs={'trip_id': self.id}) 56 | -------------------------------------------------------------------------------- /server/taxi/trips/serializers.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urljoin 2 | 3 | from django.conf import settings 4 | from django.contrib.auth import get_user_model 5 | from django.contrib.auth.models import Group 6 | 7 | from rest_framework import serializers 8 | 9 | from .models import Trip 10 | 11 | 12 | class MediaImageField(serializers.ImageField): 13 | def __init__(self, *args, **kwargs): 14 | super().__init__(*args, **kwargs) 15 | 16 | def to_representation(self, value): 17 | if not value: 18 | return None 19 | return urljoin(settings.MEDIA_URL, value.name) 20 | 21 | 22 | class UserSerializer(serializers.ModelSerializer): 23 | password1 = serializers.CharField(write_only=True) 24 | password2 = serializers.CharField(write_only=True) 25 | group = serializers.CharField() 26 | photo = MediaImageField(allow_empty_file=True) 27 | 28 | def validate(self, data): 29 | if data['password1'] != data['password2']: 30 | raise serializers.ValidationError('Passwords must match.') 31 | return data 32 | 33 | def create(self, validated_data): 34 | group_data = validated_data.pop('group') 35 | group, _ = Group.objects.get_or_create(name=group_data) 36 | data = { 37 | key: value for key, value in validated_data.items() 38 | if key not in ('password1', 'password2') 39 | } 40 | data['password'] = validated_data['password1'] 41 | user = self.Meta.model.objects.create_user(**data) 42 | user.groups.add(group) 43 | user.save() 44 | return user 45 | 46 | class Meta: 47 | model = get_user_model() 48 | fields = ( 49 | 'id', 'username', 'password1', 'password2', 50 | 'first_name', 'last_name', 'group', 51 | 'photo', 52 | ) 53 | read_only_fields = ('id',) 54 | 55 | 56 | class TripSerializer(serializers.ModelSerializer): 57 | class Meta: 58 | model = Trip 59 | fields = '__all__' 60 | read_only_fields = ('id', 'created', 'updated',) 61 | 62 | 63 | class ReadOnlyTripSerializer(serializers.ModelSerializer): 64 | driver = UserSerializer(read_only=True) 65 | rider = UserSerializer(read_only=True) 66 | 67 | class Meta: 68 | model = Trip 69 | fields = '__all__' 70 | -------------------------------------------------------------------------------- /server/taxi/trips/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/taxi-app/8f4e25bac1373ac2312d1b20398a48c21f6130cb/server/taxi/trips/tests/__init__.py -------------------------------------------------------------------------------- /server/taxi/trips/tests/test_http.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | 3 | from django.contrib.auth import get_user_model 4 | from django.contrib.auth.models import Group 5 | from django.core.files.uploadedfile import SimpleUploadedFile 6 | 7 | from PIL import Image 8 | from rest_framework import status 9 | from rest_framework.reverse import reverse 10 | from rest_framework.test import APIClient, APITestCase 11 | 12 | from trips.serializers import TripSerializer, UserSerializer 13 | from trips.models import Trip 14 | 15 | PASSWORD = 'pAssw0rd!' 16 | 17 | 18 | def create_user(username='user@example.com', password=PASSWORD, group_name='rider'): 19 | group, _ = Group.objects.get_or_create(name=group_name) 20 | user = get_user_model().objects.create_user( 21 | username=username, password=password) 22 | user.groups.add(group) 23 | user.save() 24 | return user 25 | 26 | 27 | def create_photo_file(): 28 | data = BytesIO() 29 | Image.new('RGB', (100, 100)).save(data, 'PNG') 30 | data.seek(0) 31 | return SimpleUploadedFile('photo.png', data.getvalue()) 32 | 33 | 34 | class AuthenticationTest(APITestCase): 35 | 36 | def setUp(self): 37 | self.client = APIClient() 38 | 39 | def test_user_can_sign_up(self): 40 | photo_file = create_photo_file() 41 | response = self.client.post(reverse('sign_up'), data={ 42 | 'username': 'user@example.com', 43 | 'first_name': 'Test', 44 | 'last_name': 'User', 45 | 'password1': PASSWORD, 46 | 'password2': PASSWORD, 47 | 'group': 'rider', 48 | 'photo': photo_file, 49 | }) 50 | user = get_user_model().objects.last() 51 | self.assertEqual(status.HTTP_201_CREATED, response.status_code) 52 | self.assertEqual(response.data['id'], user.id) 53 | self.assertEqual(response.data['username'], user.username) 54 | self.assertEqual(response.data['first_name'], user.first_name) 55 | self.assertEqual(response.data['last_name'], user.last_name) 56 | self.assertEqual(response.data['group'], user.group) 57 | self.assertIsNotNone(user.photo) 58 | 59 | def test_user_can_log_in(self): 60 | user = create_user() 61 | response = self.client.post(reverse('log_in'), data={ 62 | 'username': user.username, 63 | 'password': PASSWORD, 64 | }) 65 | self.assertEqual(status.HTTP_200_OK, response.status_code) 66 | self.assertEqual(response.data['username'], user.username) 67 | 68 | def test_user_can_log_out(self): 69 | user = create_user() 70 | self.client.login(username=user.username, password=PASSWORD) 71 | response = self.client.post(reverse('log_out')) 72 | self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code) 73 | 74 | 75 | class HttpTripTest(APITestCase): 76 | 77 | def setUp(self): 78 | self.user = create_user() 79 | self.client = APIClient() 80 | self.client.login(username=self.user.username, password=PASSWORD) 81 | 82 | def test_user_can_list_trips(self): 83 | trips = [ 84 | Trip.objects.create( 85 | pick_up_address='A', drop_off_address='B', rider=self.user), 86 | Trip.objects.create( 87 | pick_up_address='B', drop_off_address='C', rider=self.user), 88 | Trip.objects.create( 89 | pick_up_address='C', drop_off_address='D') 90 | ] 91 | response = self.client.get(reverse('trip:trip_list')) 92 | self.assertEqual(status.HTTP_200_OK, response.status_code) 93 | exp_trip_ids = [str(trip.id) for trip in trips[0:2]] 94 | act_trip_ids = [trip.get('id') for trip in response.data] 95 | self.assertCountEqual(act_trip_ids, exp_trip_ids) 96 | 97 | def test_user_can_retrieve_trip_by_id(self): 98 | trip = Trip.objects.create( 99 | pick_up_address='A', drop_off_address='B', rider=self.user) 100 | response = self.client.get(trip.get_absolute_url()) 101 | self.assertEqual(status.HTTP_200_OK, response.status_code) 102 | self.assertEqual(str(trip.id), response.data.get('id')) 103 | -------------------------------------------------------------------------------- /server/taxi/trips/tests/test_websockets.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.contrib.auth.models import Group 3 | from django.test import Client 4 | 5 | from channels.db import database_sync_to_async 6 | from channels.layers import get_channel_layer 7 | from channels.testing import WebsocketCommunicator 8 | from nose.tools import assert_equal, assert_is_none, assert_is_not_none, assert_true 9 | import pytest 10 | 11 | from taxi.routing import application 12 | from trips.models import Trip 13 | 14 | 15 | TEST_CHANNEL_LAYERS = { 16 | 'default': { 17 | 'BACKEND': 'channels.layers.InMemoryChannelLayer', 18 | }, 19 | } 20 | 21 | 22 | @database_sync_to_async 23 | def create_user( 24 | *, 25 | username='rider@example.com', 26 | password='pAssw0rd!', 27 | group='rider' 28 | ): 29 | # Create user. 30 | user = get_user_model().objects.create_user( 31 | username=username, 32 | password=password 33 | ) 34 | 35 | # Create user group. 36 | user_group, _ = Group.objects.get_or_create(name=group) 37 | user.groups.add(user_group) 38 | user.save() 39 | return user 40 | 41 | 42 | async def auth_connect(user): 43 | client = Client() 44 | client.force_login(user=user) 45 | communicator = WebsocketCommunicator( 46 | application=application, 47 | path='/taxi/', 48 | headers=[( 49 | b'cookie', 50 | f'sessionid={client.cookies["sessionid"].value}'.encode('ascii') 51 | )] 52 | ) 53 | connected, _ = await communicator.connect() 54 | assert_true(connected) 55 | return communicator 56 | 57 | 58 | async def connect_and_create_trip( 59 | *, 60 | user, 61 | pick_up_address='A', 62 | drop_off_address='B' 63 | ): 64 | communicator = await auth_connect(user) 65 | await communicator.send_json_to({ 66 | 'type': 'create.trip', 67 | 'data': { 68 | 'pick_up_address': pick_up_address, 69 | 'drop_off_address': drop_off_address, 70 | 'rider': user.id, 71 | } 72 | }) 73 | return communicator 74 | 75 | 76 | async def connect_and_update_trip(*, user, trip, status): 77 | communicator = await auth_connect(user) 78 | await communicator.send_json_to({ 79 | 'type': 'update.trip', 80 | 'data': { 81 | 'id': f'{trip.id}', 82 | 'pick_up_address': trip.pick_up_address, 83 | 'drop_off_address': trip.drop_off_address, 84 | 'status': status, 85 | 'driver': user.id, 86 | } 87 | }) 88 | return communicator 89 | 90 | 91 | @database_sync_to_async 92 | def create_trip(**kwargs): 93 | return Trip.objects.create(**kwargs) 94 | 95 | 96 | @pytest.mark.asyncio 97 | @pytest.mark.django_db(transaction=True) 98 | class TestWebsockets: 99 | 100 | async def test_authorized_user_can_connect(self, settings): 101 | settings.CHANNEL_LAYERS = TEST_CHANNEL_LAYERS 102 | 103 | user = await create_user( 104 | username='rider@example.com', 105 | group='rider' 106 | ) 107 | communicator = await auth_connect(user) 108 | await communicator.disconnect() 109 | 110 | async def test_rider_can_create_trips(self, settings): 111 | settings.CHANNEL_LAYERS = TEST_CHANNEL_LAYERS 112 | 113 | user = await create_user( 114 | username='rider@example.com', 115 | group='rider' 116 | ) 117 | communicator = await connect_and_create_trip(user=user) 118 | 119 | # Receive JSON message from server. 120 | response = await communicator.receive_json_from() 121 | data = response.get('data') 122 | 123 | # Confirm data. 124 | assert_is_not_none(data['id']) 125 | assert_equal('A', data['pick_up_address']) 126 | assert_equal('B', data['drop_off_address']) 127 | assert_equal(Trip.REQUESTED, data['status']) 128 | assert_is_none(data['driver']) 129 | assert_equal(user.username, data['rider'].get('username')) 130 | 131 | await communicator.disconnect() 132 | 133 | async def test_rider_is_added_to_trip_group_on_create(self, settings): 134 | settings.CHANNEL_LAYERS = TEST_CHANNEL_LAYERS 135 | 136 | user = await create_user( 137 | username='rider@example.com', 138 | group='rider' 139 | ) 140 | 141 | # Connect and send JSON message to server. 142 | communicator = await connect_and_create_trip(user=user) 143 | 144 | # Receive JSON message from server. 145 | # Rider should be added to new trip's group. 146 | response = await communicator.receive_json_from() 147 | data = response.get('data') 148 | 149 | trip_id = data['id'] 150 | message = { 151 | 'type': 'echo.message', 152 | 'data': 'This is a test message.' 153 | } 154 | 155 | # Send JSON message to new trip's group. 156 | channel_layer = get_channel_layer() 157 | await channel_layer.group_send(trip_id, message=message) 158 | 159 | # Receive JSON message from server. 160 | response = await communicator.receive_json_from() 161 | 162 | # Confirm data. 163 | assert_equal(message, response) 164 | 165 | await communicator.disconnect() 166 | 167 | async def test_rider_is_added_to_trip_groups_on_connect(self, settings): 168 | settings.CHANNEL_LAYERS = TEST_CHANNEL_LAYERS 169 | 170 | user = await create_user( 171 | username='rider3@example.com', 172 | group='rider' 173 | ) 174 | 175 | # Create trips and link to rider. 176 | trip = await create_trip( 177 | pick_up_address='A', 178 | drop_off_address='B', 179 | rider=user 180 | ) 181 | 182 | # Connect to server. 183 | # Trips for rider should be retrieved. 184 | # Rider should be added to trips' groups. 185 | communicator = await auth_connect(user) 186 | 187 | message = { 188 | 'type': 'echo.message', 189 | 'data': 'This is a test message.' 190 | } 191 | 192 | channel_layer = get_channel_layer() 193 | 194 | # Test sending JSON message to trip group. 195 | await channel_layer.group_send(f'{trip.id}', message=message) 196 | response = await communicator.receive_json_from() 197 | assert_equal(message, response) 198 | 199 | await communicator.disconnect() 200 | 201 | async def test_driver_can_update_trips(self, settings): 202 | settings.CHANNEL_LAYERS = TEST_CHANNEL_LAYERS 203 | 204 | trip = await create_trip( 205 | pick_up_address='A', 206 | drop_off_address='B' 207 | ) 208 | user = await create_user( 209 | username='driver@example.com', 210 | group='driver' 211 | ) 212 | 213 | # Send JSON message to server. 214 | communicator = await connect_and_update_trip( 215 | user=user, 216 | trip=trip, 217 | status=Trip.IN_PROGRESS 218 | ) 219 | 220 | # Receive JSON message from server. 221 | response = await communicator.receive_json_from() 222 | data = response.get('data') 223 | 224 | # Confirm data. 225 | assert_equal(str(trip.id), data['id']) 226 | assert_equal('A', data['pick_up_address']) 227 | assert_equal('B', data['drop_off_address']) 228 | assert_equal(Trip.IN_PROGRESS, data['status']) 229 | assert_equal(user.username, data['driver'].get('username')) 230 | assert_equal(None, data['rider']) 231 | 232 | await communicator.disconnect() 233 | 234 | async def test_driver_is_added_to_trip_group_on_update(self, settings): 235 | settings.CHANNEL_LAYERS = TEST_CHANNEL_LAYERS 236 | 237 | trip = await create_trip( 238 | pick_up_address='A', 239 | drop_off_address='B' 240 | ) 241 | user = await create_user( 242 | username='driver@example.com', 243 | group='driver' 244 | ) 245 | 246 | # Send JSON message to server. 247 | communicator = await connect_and_update_trip( 248 | user=user, 249 | trip=trip, 250 | status=Trip.IN_PROGRESS 251 | ) 252 | 253 | # Receive JSON message from server. 254 | response = await communicator.receive_json_from() 255 | data = response.get('data') 256 | 257 | trip_id = data['id'] 258 | message = { 259 | 'type': 'echo.message', 260 | 'data': 'This is a test message.' 261 | } 262 | 263 | # Send JSON message to trip's group. 264 | channel_layer = get_channel_layer() 265 | await channel_layer.group_send(trip_id, message=message) 266 | 267 | # Receive JSON message from server. 268 | response = await communicator.receive_json_from() 269 | 270 | # Confirm data. 271 | assert_equal(message, response) 272 | 273 | await communicator.disconnect() 274 | 275 | async def test_driver_is_alerted_on_trip_create(self, settings): 276 | settings.CHANNEL_LAYERS = TEST_CHANNEL_LAYERS 277 | 278 | # Listen to the 'drivers' group test channel. 279 | channel_layer = get_channel_layer() 280 | await channel_layer.group_add( 281 | group='drivers', 282 | channel='test_channel' 283 | ) 284 | 285 | user = await create_user( 286 | username='rider@example.com', 287 | group='rider' 288 | ) 289 | 290 | # Send JSON message to server. 291 | communicator = await connect_and_create_trip(user=user) 292 | 293 | # Receive JSON message from server on test channel. 294 | response = await channel_layer.receive('test_channel') 295 | data = response.get('data') 296 | 297 | # Confirm data. 298 | assert_is_not_none(data['id']) 299 | assert_equal(user.username, data['rider'].get('username')) 300 | 301 | await communicator.disconnect() 302 | 303 | async def test_rider_is_alerted_on_trip_update(self, settings): 304 | settings.CHANNEL_LAYERS = TEST_CHANNEL_LAYERS 305 | 306 | trip = await create_trip( 307 | pick_up_address='A', 308 | drop_off_address='B' 309 | ) 310 | 311 | # Listen to the trip group test channel. 312 | channel_layer = get_channel_layer() 313 | await channel_layer.group_add( 314 | group=f'{trip.id}', 315 | channel='test_channel' 316 | ) 317 | 318 | user = await create_user( 319 | username='driver@example.com', 320 | group='driver' 321 | ) 322 | 323 | # Send JSON message to server. 324 | communicator = await connect_and_update_trip( 325 | user=user, 326 | trip=trip, 327 | status=Trip.IN_PROGRESS 328 | ) 329 | 330 | # Receive JSON message from server on test channel. 331 | response = await channel_layer.receive('test_channel') 332 | data = response.get('data') 333 | 334 | # Confirm data. 335 | assert_equal(f'{trip.id}', data['id']) 336 | assert_equal(user.username, data['driver'].get('username')) 337 | 338 | await communicator.disconnect() 339 | -------------------------------------------------------------------------------- /server/taxi/trips/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import TripView 4 | 5 | app_name = 'taxi' 6 | 7 | urlpatterns = [ 8 | path('', TripView.as_view({'get': 'list'}), name='trip_list'), 9 | path('/', TripView.as_view({'get': 'retrieve'}), name='trip_detail'), 10 | ] 11 | -------------------------------------------------------------------------------- /server/taxi/trips/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model, login, logout 2 | from django.contrib.auth.forms import AuthenticationForm 3 | from django.db.models import Q 4 | 5 | from rest_framework import generics, permissions, status, views, viewsets 6 | from rest_framework.response import Response 7 | 8 | from .models import Trip 9 | from .serializers import ReadOnlyTripSerializer, UserSerializer 10 | 11 | 12 | class SignUpView(generics.CreateAPIView): 13 | queryset = get_user_model().objects.all() 14 | serializer_class = UserSerializer 15 | 16 | 17 | class LogInView(views.APIView): 18 | def post(self, request): 19 | form = AuthenticationForm(data=request.data) 20 | if form.is_valid(): 21 | user = form.get_user() 22 | login(request, user=form.get_user()) 23 | return Response(UserSerializer(user).data) 24 | else: 25 | return Response(form.errors, status=status.HTTP_400_BAD_REQUEST) 26 | 27 | 28 | class LogOutView(views.APIView): 29 | permission_classes = (permissions.IsAuthenticated,) 30 | 31 | def post(self, *args, **kwargs): 32 | logout(self.request) 33 | return Response(status=status.HTTP_204_NO_CONTENT) 34 | 35 | 36 | class TripView(viewsets.ReadOnlyModelViewSet): 37 | lookup_field = 'id' 38 | lookup_url_kwarg = 'trip_id' 39 | permission_classes = (permissions.IsAuthenticated,) 40 | serializer_class = ReadOnlyTripSerializer 41 | 42 | def get_queryset(self): 43 | user = self.request.user 44 | if user.group == 'driver': 45 | return Trip.objects.filter( 46 | Q(status=Trip.REQUESTED) | Q(driver=user) 47 | ) 48 | if user.group == 'rider': 49 | return Trip.objects.filter(rider=user) 50 | return Trip.objects.none() 51 | --------------------------------------------------------------------------------