diff --git a/README.md b/README.md index 70a69285..6b1d4326 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,11 @@ Stage 3 creates the server to handle and distribute everything. Stage 4 finally adds support for multiple users. This will enable you to invite other to tasks. -* [ ] Define creator of project (aka "admin") -* [ ] Mark own projects -* [ ] Invite user (only possible by admin) - * [ ] Enter username and to invite user - * [ ] Users should also see projects they've invited to +* [x] Define creator of project (aka "owner") +* [x] Mark own projects +* [x] Invite user (only possible by owner) + * [x] Enter username and to invite user + * [x] Users should also see projects they've invited to ## Stage 5 @@ -103,7 +103,7 @@ Things that would be nice but are not necessary for a prototype. * [ ] From overpass-query / -result * [ ] Internal development * [ ] Use go modules? (may or may not be useful) - * [ ] Create Docker container for client and server + * [x] Create Docker container for client and server # Development diff --git a/client/package-lock.json b/client/package-lock.json index a8ddb368..cdad7ec0 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,6 +1,6 @@ { "name": "simple-task-manager", - "version": "0.3.1", + "version": "0.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/client/package.json b/client/package.json index 3502a5b6..3c82c91c 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "simple-task-manager", - "version": "0.3.1", + "version": "0.4.0", "scripts": { "ng": "ng", "dev": "ng serve --watch", diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 37339efb..5cb432f7 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -15,8 +15,11 @@ import { ProjectComponent } from './project/project.component'; import { TaskListComponent } from './task/task-list.component'; import { TaskDetailsComponent } from './task/task-details.component'; import { TaskMapComponent } from './task/task-map.component'; -import { FooterComponent } from './footer.component'; +import { FooterComponent } from './ui/footer.component'; import { ProjectCreationComponent } from './project/project-creation.component'; +import { TabsComponent } from './ui/tabs.component'; +import { UserListComponent } from './user/user-list.component'; +import { UserInvitationComponent } from './user/user-invitation.component'; @NgModule({ declarations: [ @@ -30,7 +33,10 @@ import { ProjectCreationComponent } from './project/project-creation.component'; TaskDetailsComponent, TaskMapComponent, FooterComponent, - ProjectCreationComponent + ProjectCreationComponent, + TabsComponent, + UserListComponent, + UserInvitationComponent ], imports: [ BrowserModule, diff --git a/client/src/app/auth/auth.component.ts b/client/src/app/auth/auth.component.ts index 2e07601a..91a33521 100644 --- a/client/src/app/auth/auth.component.ts +++ b/client/src/app/auth/auth.component.ts @@ -1,7 +1,7 @@ import { NgZone, Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from './auth.service'; -import { UserService } from './user.service'; +import { UserService } from '../user/user.service'; @Component({ selector: 'app-auth', diff --git a/client/src/app/auth/auth.service.ts b/client/src/app/auth/auth.service.ts index 0873f30a..8cc2981b 100644 --- a/client/src/app/auth/auth.service.ts +++ b/client/src/app/auth/auth.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { environment } from '../../environments/environment'; -import { UserService } from './user.service'; +import { UserService } from '../user/user.service'; import { Router } from '@angular/router'; @Injectable({ diff --git a/client/src/app/manager/manager.component.html b/client/src/app/manager/manager.component.html index 39382be1..9ac2424a 100644 --- a/client/src/app/manager/manager.component.html +++ b/client/src/app/manager/manager.component.html @@ -7,4 +7,5 @@ + diff --git a/client/src/app/manager/manager.component.scss b/client/src/app/manager/manager.component.scss index 7eb26ffd..5e92f9c4 100644 --- a/client/src/app/manager/manager.component.scss +++ b/client/src/app/manager/manager.component.scss @@ -14,3 +14,9 @@ .toolbar > p { color: $very-light-gray; } + +.root-container { + max-width: 500px; + margin-left: auto; + margin-right: auto; +} diff --git a/client/src/app/manager/manager.component.ts b/client/src/app/manager/manager.component.ts index 02a62dc8..70b548bc 100644 --- a/client/src/app/manager/manager.component.ts +++ b/client/src/app/manager/manager.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { UserService } from '../auth/user.service'; +import { UserService } from '../user/user.service'; import { AuthService } from '../auth/auth.service'; import { Router } from '@angular/router'; diff --git a/client/src/app/project/project-creation.component.html b/client/src/app/project/project-creation.component.html index 67269a27..161d08e9 100644 --- a/client/src/app/project/project-creation.component.html +++ b/client/src/app/project/project-creation.component.html @@ -5,16 +5,16 @@
- Name: - -
-
- Max. points per task: - +

Properties:

+
+ Name: + +
+
+ Max. points per task: + +
-
-
-

Divide into squares:

@@ -25,4 +25,7 @@

Divide into squares:

+
+
+
diff --git a/client/src/app/project/project-creation.component.scss b/client/src/app/project/project-creation.component.scss index caeaf1df..34adc6b0 100644 --- a/client/src/app/project/project-creation.component.scss +++ b/client/src/app/project/project-creation.component.scss @@ -2,26 +2,30 @@ :host { height: 100%; + display: flex; + flex-direction: column; + justify-content: stretch; } .root-container { - height: 100%; display: flex; flex-direction: row; justify-content: space-between; + flex-grow: 1; + margin-top: 0px; + min-height: 0px; // important for scrolling (for whatever reason, s. above) } .project-properties-container { display: flex; flex-direction: column; } -.project-properties-container > div { +.project-properties-container div { margin-bottom: $space-base } .map-container { - height: 75%; - width: 50%; + width: 65%; display: flex; flex-direction: column; } diff --git a/client/src/app/project/project-list.component.html b/client/src/app/project/project-list.component.html index f12b3cac..b7c751d6 100644 --- a/client/src/app/project/project-list.component.html +++ b/client/src/app/project/project-list.component.html @@ -1,7 +1,12 @@

Projekte

- {{p.name}} ({{p.id}}) +
+ {{p.name}} ({{p.id}}) +
+
+ Owned by you +
diff --git a/client/src/app/project/project-list.component.scss b/client/src/app/project/project-list.component.scss index 84e58952..f07920ab 100644 --- a/client/src/app/project/project-list.component.scss +++ b/client/src/app/project/project-list.component.scss @@ -1,9 +1,14 @@ @import "../../styles.scss"; +.list-item { + display: flex; + justify-content: space-between; +} + .create-project-button { margin-top: $space-large; } -.id-label { +.light-label { color: $gray-mid; } diff --git a/client/src/app/project/project-list.component.ts b/client/src/app/project/project-list.component.ts index 165ab62b..21e20b2b 100644 --- a/client/src/app/project/project-list.component.ts +++ b/client/src/app/project/project-list.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { ProjectService } from './project.service'; +import { UserService } from './../user/user.service'; import { Project } from './project.material'; import { Router } from '@angular/router'; @@ -13,13 +14,18 @@ export class ProjectListComponent implements OnInit { constructor( private projectService: ProjectService, - private router: Router + private router: Router, + private userService: UserService ) { } ngOnInit(): void { this.projectService.getProjects().subscribe(p => this.projects = p); } + public get currentUser(): string { + return this.userService.getUser(); + } + public onProjectListItemClicked(id: string) { this.router.navigate(['/project', id]); } diff --git a/client/src/app/project/project.component.html b/client/src/app/project/project.component.html index 333bce04..7cb47ddd 100644 --- a/client/src/app/project/project.component.html +++ b/client/src/app/project/project.component.html @@ -3,18 +3,30 @@

Project: {{thisProject?.name}}

-
-

Tasks

- -
-
-
-

Map

- -
-
-

Details

+
+ +
+
+

Tasks

+ +
+
+
+
+

Users

+ + +
+
+
+ +
+

Task Details

+
+

Map

+ +
diff --git a/client/src/app/project/project.component.scss b/client/src/app/project/project.component.scss index 2a35fec4..1355f73e 100644 --- a/client/src/app/project/project.component.scss +++ b/client/src/app/project/project.component.scss @@ -1,5 +1,8 @@ @import '../../styles.scss'; +// Making the task list content scrollable was made with the help of this article: +// https://moduscreate.com/blog/how-to-fix-overflow-issues-in-css-flex-layouts/ + :host { height: 100%; width: 100%; @@ -8,33 +11,34 @@ justify-content: stretch; } +.task-details-container { + margin-bottom: $space-large; +} + .root-container { display: flex; - height: 100%; + flex-grow: 1; margin-top: 0px; + min-height: 0px; // important for scrolling (for whatever reason, s. above) } -.root-container-item { - width: 50%; -} - -.task-list-container { +.task-list-details-container { + width: 35%; + display: flex; + flex-direction: column; + justify-content: space-between; + margin-top: $space-large; margin-right: $space-large; } -.task-details-map-container { +.tab-container { display: flex; flex-direction: column; + min-height: 0px; // important for scrolling (for whatever reason, s. above) } .map-container { - height: 65%; -} -.details-container { - height: 35%; -} - -.map-container { + width: 65%; display: flex; flex-direction: column; } @@ -47,3 +51,7 @@ color: $very-light-gray; margin-left: $space-base; } + +.tab-container-item { + margin-bottom: $space-large; +} diff --git a/client/src/app/project/project.material.ts b/client/src/app/project/project.material.ts index eb41b232..a9d48e6c 100644 --- a/client/src/app/project/project.material.ts +++ b/client/src/app/project/project.material.ts @@ -1,6 +1,8 @@ export class Project { constructor(public id: string, public name: string, - public taskIds: string[] + public taskIds: string[], + public users?: string[], + public owner?: string ) { } } diff --git a/client/src/app/project/project.service.ts b/client/src/app/project/project.service.ts index db9bb359..0e347046 100644 --- a/client/src/app/project/project.service.ts +++ b/client/src/app/project/project.service.ts @@ -1,6 +1,6 @@ -import { Injectable } from '@angular/core'; +import { Injectable, EventEmitter } from '@angular/core'; import { Observable, throwError } from 'rxjs'; -import { map, flatMap } from 'rxjs/operators'; +import { map, flatMap, tap } from 'rxjs/operators'; import { Project } from './project.material'; import { Task } from './../task/task.material'; import { TaskService } from './../task/task.service'; @@ -11,7 +11,7 @@ import { environment } from './../../environments/environment'; providedIn: 'root' }) export class ProjectService { - public projects: Project[] = []; + public projectChanged: EventEmitter = new EventEmitter(); constructor( private taskService: TaskService, @@ -44,4 +44,9 @@ export class ProjectService { return this.http.post(environment.url_projects, JSON.stringify(p)); })); } + + public inviteUser(user: string, id: string): Observable { + return this.http.post(environment.url_projects_users + '?user=' + user + '&project=' + id, '') + .pipe(tap(p => this.projectChanged.emit(p))); + } } diff --git a/client/src/app/task/task-details.component.html b/client/src/app/task/task-details.component.html index 33697d1a..2efc6c73 100644 --- a/client/src/app/task/task-details.component.html +++ b/client/src/app/task/task-details.component.html @@ -2,20 +2,20 @@

Task: {{task?.id}}

-

Assigned to: {{task?.assignedUser}}

+

Assigned to: {{task?.assignedUser}}

-
+
- Points: + Points: / {{task?.maxProcessPoints}}
- Points: + Points: {{task?.processPoints}} / {{task?.maxProcessPoints}} diff --git a/client/src/app/task/task-details.component.scss b/client/src/app/task/task-details.component.scss index 3dde99b7..86364969 100644 --- a/client/src/app/task/task-details.component.scss +++ b/client/src/app/task/task-details.component.scss @@ -15,6 +15,16 @@ margin-left: $space-large; } +.process-point-container { + display: flex; + align-items: center; + height: 40px; // To stay equally high, regardless of what the content is +} + +.points-label { + margin-right: 5px; +} + input { width: 50px; } diff --git a/client/src/app/task/task-details.component.ts b/client/src/app/task/task-details.component.ts index 0d843c4f..b577f752 100644 --- a/client/src/app/task/task-details.component.ts +++ b/client/src/app/task/task-details.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { Observable, of } from 'rxjs'; import { TaskService } from './task.service'; import { Task } from './task.material'; -import { UserService } from '../auth/user.service'; +import { UserService } from '../user/user.service'; @Component({ selector: 'app-task-details', diff --git a/client/src/app/task/task-map.component.ts b/client/src/app/task/task-map.component.ts index 0e4d4a81..8f2b9c92 100644 --- a/client/src/app/task/task-map.component.ts +++ b/client/src/app/task/task-map.component.ts @@ -1,6 +1,6 @@ import { Component, AfterViewInit, Input } from '@angular/core'; import { TaskService } from './task.service'; -import { UserService } from '../auth/user.service'; +import { UserService } from '../user/user.service'; import { Task } from './task.material'; import { Map, View } from 'ol'; import TileLayer from 'ol/layer/Tile'; diff --git a/client/src/app/footer.component.html b/client/src/app/ui/footer.component.html similarity index 100% rename from client/src/app/footer.component.html rename to client/src/app/ui/footer.component.html diff --git a/client/src/app/footer.component.scss b/client/src/app/ui/footer.component.scss similarity index 60% rename from client/src/app/footer.component.scss rename to client/src/app/ui/footer.component.scss index 395fd330..f7291793 100644 --- a/client/src/app/footer.component.scss +++ b/client/src/app/ui/footer.component.scss @@ -1,4 +1,4 @@ -@import "../styles.scss"; +@import "../../styles.scss"; .version-label { margin: $space-large; diff --git a/client/src/app/footer.component.ts b/client/src/app/ui/footer.component.ts similarity index 86% rename from client/src/app/footer.component.ts rename to client/src/app/ui/footer.component.ts index 37f957c3..4f155770 100644 --- a/client/src/app/footer.component.ts +++ b/client/src/app/ui/footer.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { version } from '../../package.json'; +import { version } from '../../../package.json'; @Component({ selector: 'app-footer', diff --git a/client/src/app/ui/tabs.component.html b/client/src/app/ui/tabs.component.html new file mode 100644 index 00000000..a7f75ee2 --- /dev/null +++ b/client/src/app/ui/tabs.component.html @@ -0,0 +1,10 @@ +
+
+ +
+
+
+
+ +
+
diff --git a/client/src/app/ui/tabs.component.scss b/client/src/app/ui/tabs.component.scss new file mode 100644 index 00000000..c1742eb6 --- /dev/null +++ b/client/src/app/ui/tabs.component.scss @@ -0,0 +1,38 @@ +@import "../../styles.scss"; + +:host { + min-height: 0px; + display: flex; + flex-direction: column; +} + +button { + background-color: white; + margin-right: -1px; // get rid of douple sized border between two buttons +} +button.selected:hover { + border-bottom: 3px solid $color-mid; +} + +.selected { + border-bottom: 3px solid $color-mid; + background-color: $color-very-light; +} + +.tab-list { + display: flex; + flex-direction: row; +} + +.tab-content { + border: 1px solid $color-light; + padding: $space-base; + min-height: 0px; + display: flex; + flex-direction: column; + overflow: auto; +} + +.inner-content { + padding-bottom: 10px; +} diff --git a/client/src/app/ui/tabs.component.spec.ts b/client/src/app/ui/tabs.component.spec.ts new file mode 100644 index 00000000..96b5fd02 --- /dev/null +++ b/client/src/app/ui/tabs.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TabsComponent } from './tabs.component'; + +describe('TabsComponent', () => { + let component: TabsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ TabsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TabsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/ui/tabs.component.ts b/client/src/app/ui/tabs.component.ts new file mode 100644 index 00000000..dfe4558f --- /dev/null +++ b/client/src/app/ui/tabs.component.ts @@ -0,0 +1,25 @@ +import { Component, OnInit, Input } from '@angular/core'; + +@Component({ + selector: 'app-tabs', + templateUrl: './tabs.component.html', + styleUrls: ['./tabs.component.scss'] +}) +export class TabsComponent implements OnInit { + @Input() tabs: string[]; + + public tabTitle: string; + public tabIndex: number; + + constructor() { } + + ngOnInit(): void { + this.tabIndex = 0; + this.tabTitle = this.tabs[this.tabIndex]; + } + + public onTabClicked(tabTitle: string) { + this.tabIndex = this.tabs.indexOf(tabTitle); + this.tabTitle = tabTitle; + } +} diff --git a/client/src/app/user/user-invitation.component.html b/client/src/app/user/user-invitation.component.html new file mode 100644 index 00000000..9e4ffe9f --- /dev/null +++ b/client/src/app/user/user-invitation.component.html @@ -0,0 +1,2 @@ + + diff --git a/client/src/app/user/user-invitation.component.scss b/client/src/app/user/user-invitation.component.scss new file mode 100644 index 00000000..0460b8c8 --- /dev/null +++ b/client/src/app/user/user-invitation.component.scss @@ -0,0 +1,5 @@ +@import "../../styles.scss"; + +#userInput { + margin-right: $space-base; +} diff --git a/client/src/app/user/user-invitation.component.spec.ts b/client/src/app/user/user-invitation.component.spec.ts new file mode 100644 index 00000000..d316b281 --- /dev/null +++ b/client/src/app/user/user-invitation.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserInvitationComponent } from './user-invitation.component'; + +describe('UserInvitationComponent', () => { + let component: UserInvitationComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ UserInvitationComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UserInvitationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/user/user-invitation.component.ts b/client/src/app/user/user-invitation.component.ts new file mode 100644 index 00000000..97e658c2 --- /dev/null +++ b/client/src/app/user/user-invitation.component.ts @@ -0,0 +1,26 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { ProjectService } from '../project/project.service'; +import { Project } from '../project/project.material'; + +@Component({ + selector: 'app-user-invitation', + templateUrl: './user-invitation.component.html', + styleUrls: ['./user-invitation.component.scss'] +}) +export class UserInvitationComponent implements OnInit { + @Input() project: Project; + + constructor( + private projectService: ProjectService + ) { } + + ngOnInit(): void { + } + + public onInvitationButtonClicked(userName: string) { + this.projectService.inviteUser(userName, this.project.id) + .subscribe(p => { + this.project = p; + }); + } +} diff --git a/client/src/app/user/user-list.component.html b/client/src/app/user/user-list.component.html new file mode 100644 index 00000000..07a8a5bd --- /dev/null +++ b/client/src/app/user/user-list.component.html @@ -0,0 +1,3 @@ +
+ {{u}} +
diff --git a/client/src/app/user/user-list.component.scss b/client/src/app/user/user-list.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/client/src/app/user/user-list.component.spec.ts b/client/src/app/user/user-list.component.spec.ts new file mode 100644 index 00000000..9d521807 --- /dev/null +++ b/client/src/app/user/user-list.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserListComponent } from './user-list.component'; + +describe('UserListComponent', () => { + let component: UserListComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ UserListComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UserListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/user/user-list.component.ts b/client/src/app/user/user-list.component.ts new file mode 100644 index 00000000..fa946786 --- /dev/null +++ b/client/src/app/user/user-list.component.ts @@ -0,0 +1,19 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { ProjectService } from '../project/project.service'; + +@Component({ + selector: 'app-user-list', + templateUrl: './user-list.component.html', + styleUrls: ['./user-list.component.scss'] +}) +export class UserListComponent implements OnInit { + @Input() users: string[]; + + constructor( + private projectService: ProjectService + ) { } + + ngOnInit(): void { + this.projectService.projectChanged.subscribe(p => this.users = p.users); + } +} diff --git a/client/src/app/auth/user.service.spec.ts b/client/src/app/user/user.service.spec.ts similarity index 85% rename from client/src/app/auth/user.service.spec.ts rename to client/src/app/user/user.service.spec.ts index 3f804c9f..0ec68ffc 100644 --- a/client/src/app/auth/user.service.spec.ts +++ b/client/src/app/user/user.service.spec.ts @@ -1,6 +1,6 @@ import { TestBed } from '@angular/core/testing'; -import { UserService } from './user.service'; +import { UserService } from '../user/user.service'; describe('UserService', () => { let service: UserService; diff --git a/client/src/app/auth/user.service.ts b/client/src/app/user/user.service.ts similarity index 100% rename from client/src/app/auth/user.service.ts rename to client/src/app/user/user.service.ts diff --git a/client/src/environments/environment.ts b/client/src/environments/environment.ts index c97619b9..b7fecaf8 100644 --- a/client/src/environments/environment.ts +++ b/client/src/environments/environment.ts @@ -6,6 +6,7 @@ export const environment = { url_auth: baseUrl + '/oauth_login', url_projects: baseUrl + '/projects', + url_projects_users: baseUrl + '/projects/users', url_tasks: baseUrl + '/tasks', url_task_assignedUser: baseUrl + '/task/assignedUser', url_task_processPoints: baseUrl + '/task/processPoints' diff --git a/client/src/styles.scss b/client/src/styles.scss index e9382e31..d6879193 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -1,6 +1,7 @@ @import "colors.scss"; // Global distances etc. +$space-small: 5px; $space-base: 10px; $space-large: $space-base * 2; $space-huge: $space-base * 4; @@ -48,6 +49,9 @@ button { button:hover { border: 1px solid $color-mid; } +button::-moz-focus-inner { // remove dotted line in Firefoc + border: 0 +} a { color: $color-very-dark; @@ -86,3 +90,11 @@ a:hover { display: flex; align-items: center; } + +input { + border: 1px solid $color-light; + padding: $space-small; +} +input:focus { + border: 1px solid $color-mid; +} diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 00000000..5d5a30d4 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Some logging to see how long building an deployment takes plus the deployment itself. + +clear +date +echo -e "\n\n\n" + +docker-compose up -d --build + +echo -e "\n\n\n" +date diff --git a/server/main.go b/server/main.go index 1216645c..b89b014e 100644 --- a/server/main.go +++ b/server/main.go @@ -14,7 +14,7 @@ import ( "github.com/hauke96/sigolo" ) -const VERSION string = "0.3.1" +const VERSION string = "0.4.0" var ( app = kingpin.New("Simple Task Manager", "A tool dividing an area of the map into smaller tasks.") @@ -60,6 +60,7 @@ func main() { router.HandleFunc("/oauth_callback", oauthCallback).Methods(http.MethodGet) router.HandleFunc("/projects", authenticatedHandler(getProjects)).Methods(http.MethodGet) router.HandleFunc("/projects", authenticatedHandler(addProject)).Methods(http.MethodPost) + router.HandleFunc("/projects/users", authenticatedHandler(addUserToTask)).Methods(http.MethodPost) router.HandleFunc("/tasks", authenticatedHandler(getTasks)).Methods(http.MethodGet) router.HandleFunc("/tasks", authenticatedHandler(addTask)).Methods(http.MethodPost) router.HandleFunc("/task/assignedUser", authenticatedHandler(assignUser)).Methods(http.MethodPost) @@ -146,10 +147,33 @@ func addProject(w http.ResponseWriter, r *http.Request, token *Token) { json.Unmarshal(bodyBytes, &project) // TODO check wether all neccessary fields are set - project = AddProject(project, token.User) + updatedProject := AddProject(&project, token.User) encoder := json.NewEncoder(w) - encoder.Encode(project) + encoder.Encode(updatedProject) +} + +func addUserToTask(w http.ResponseWriter, r *http.Request, token *Token) { + userName, err := getParam("user", r) + if err != nil { + responseBadRequest(w, err.Error()) + return + } + + projectId, err := getParam("project", r) + if err != nil { + responseBadRequest(w, err.Error()) + return + } + + updatedProject, err := AddUser(userName, projectId, token.User) + if err != nil { + responseInternalError(w, err.Error()) + return + } + + encoder := json.NewEncoder(w) + encoder.Encode(updatedProject) } func getTasks(w http.ResponseWriter, r *http.Request, token *Token) { diff --git a/server/project.go b/server/project.go index 79b09379..c928acf8 100644 --- a/server/project.go +++ b/server/project.go @@ -1,40 +1,49 @@ package main +import ( + "errors" + "fmt" +) + type Project struct { Id string `json:"id"` Name string `json:"name"` TaskIDs []string `json:"taskIds"` Users []string `json:"users"` + Owner string `json:"owner"` } var ( - projects []Project + projects []*Project ) func InitProjects() { - projects = make([]Project, 0) - projects = append(projects, Project{ + projects = make([]*Project, 0) + projects = append(projects, &Project{ Id: "p-" + GetId(), Name: "First project", TaskIDs: []string{"t-3", "t-4"}, Users: []string{"hauke-stieler"}, + Owner: "hauke-stieler", }) - projects = append(projects, Project{ + projects = append(projects, &Project{ Id: "p-" + GetId(), Name: "Foo", TaskIDs: []string{"t-5"}, Users: []string{"hauke-stieler", "hauke-stieler-dev"}, + Owner: "hauke-stieler", }) - projects = append(projects, Project{ + projects = append(projects, &Project{ Id: "p-" + GetId(), Name: "Bar", TaskIDs: []string{"t-6", "t-7", "t-8", "t-9", "t-10"}, Users: []string{"hauke-stieler-dev"}, + Owner: "hauke-stieler-dev", }) } -func GetProjects(user string) []Project { - result := make([]Project, 0) +func GetProjects(user string) []*Project { + result := make([]*Project, 0) for _, p := range projects { for _, u := range p.Users { @@ -47,9 +56,10 @@ func GetProjects(user string) []Project { return result } -func AddProject(project Project, user string) Project { +func AddProject(project *Project, user string) *Project { project.Id = "p-" + GetId() project.Users = []string{user} + project.Owner = user projects = append(projects, project) return project } @@ -88,3 +98,36 @@ func VerifyOwnership(user string, taskIds []string) bool { return true } + +func GetProject(id string) (*Project, error) { + for _, p := range projects { + if p.Id == id { + return p, nil + } + } + + return nil, errors.New(fmt.Sprintf("Project with ID '%s' not found", id)) +} + +func AddUser(user, id, potentialOwner string) (*Project, error) { + project, err := GetProject(id) + if err != nil { + return nil, err + } + + // Only the owner is allowed to invite + if project.Owner != potentialOwner { + return nil, errors.New(fmt.Sprintf("User '%s' is not allowed to add another user", potentialOwner)) + } + + // Check if user is already in project. If so, just do nothing and return + for _, u := range project.Users { + if u == user { + return project, nil + } + } + + project.Users = append(project.Users, user) + + return project, nil +} diff --git a/server/project_test.go b/server/project_test.go index afdc0829..fbc0c51d 100644 --- a/server/project_test.go +++ b/server/project_test.go @@ -5,24 +5,27 @@ import ( ) func prepare() { - projects = make([]Project, 0) - projects = append(projects, Project{ + projects = make([]*Project, 0) + projects = append(projects, &Project{ Id: "p-0", Name: "First project", TaskIDs: []string{"t-3", "t-4"}, Users: []string{"Peter"}, + Owner: "Peter", }) - projects = append(projects, Project{ + projects = append(projects, &Project{ Id: "p-1", Name: "Foo", TaskIDs: []string{"t-5"}, Users: []string{"Peter", "Maria"}, + Owner: "Peter", }) - projects = append(projects, Project{ + projects = append(projects, &Project{ Id: "p-2", Name: "Bar", TaskIDs: []string{"t-6", "t-7", "t-8", "t-9", "t-10"}, Users: []string{"Maria"}, + Owner: "Maria", }) } @@ -66,8 +69,8 @@ func TestGetProjects(t *testing.T) { } } -func TestAddProject(t *testing.T) { - projects = make([]Project, 0) +func TestAddAndGetProject(t *testing.T) { + projects = make([]*Project, 0) nextId = 100 // the new project should then have the ID "p-100" p := Project{ @@ -75,8 +78,9 @@ func TestAddProject(t *testing.T) { Name: "Test name", TaskIDs: []string{"t-11"}, Users: []string{"noname-user"}, + Owner: "noname-user", } - AddProject(p, "Maria") + AddProject(&p, "Maria") // Check parameter of the just added Project newProject := GetProjects("Maria")[0] @@ -96,9 +100,52 @@ func TestAddProject(t *testing.T) { t.Errorf("Name is not the same") t.Fail() } + if newProject.Owner != "Maria" { + t.Errorf("Owner does not match") + t.Fail() + } +} + +func TestAddUser(t *testing.T) { + prepare() + + newUser := "new user" + + p, err := AddUser(newUser, "p-0", "Peter") + if err != nil { + t.Error("This should work") + t.Error(err.Error()) + t.Fail() + } + + containsUser := false + for _, u := range p.Users { + if u == newUser { + containsUser = true + break + } + } + if !containsUser { + t.Error("Project should contain new user") + t.Fail() + } + + p, err = AddUser(newUser, "p-2346", "Peter") + if err == nil { + t.Error("This should not work: The project does not exist") + t.Error(err.Error()) + t.Fail() + } + + p, err = AddUser(newUser, "p-0", "Not-Owner-User") + if err == nil { + t.Error("This should not work: A non-owner user tries to add a user") + t.Error(err.Error()) + t.Fail() + } } -func contains(projectIdToFind string, projectsToCheck []Project) bool { +func contains(projectIdToFind string, projectsToCheck []*Project) bool { for _, p := range projectsToCheck { if p.Id == projectIdToFind { return true