Adds the chart and dialog for analog voting
This commit is contained in:
parent
72ff1b1f09
commit
96aa3b0084
@ -31,79 +31,81 @@
|
|||||||
"cleanup-win": "npm run prettify-write & npm run lint-write"
|
"cleanup-win": "npm run prettify-write & npm run lint-write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "~8.2.4",
|
"@angular/animations": "^8.2.14",
|
||||||
"@angular/cdk": "~8.1.4",
|
"@angular/cdk": "~8.1.4",
|
||||||
"@angular/cdk-experimental": "~8.1.4",
|
"@angular/cdk-experimental": "~8.1.4",
|
||||||
"@angular/common": "~8.2.4",
|
"@angular/common": "^8.2.14",
|
||||||
"@angular/compiler": "~8.2.4",
|
"@angular/compiler": "^8.2.14",
|
||||||
"@angular/core": "~8.2.4",
|
"@angular/core": "^8.2.14",
|
||||||
"@angular/forms": "~8.2.4",
|
"@angular/forms": "^8.2.14",
|
||||||
"@angular/material": "~8.1.4",
|
"@angular/material": "~8.1.4",
|
||||||
"@angular/material-moment-adapter": "~8.1.4",
|
"@angular/material-moment-adapter": "~8.1.4",
|
||||||
"@angular/platform-browser": "~8.2.4",
|
"@angular/platform-browser": "^8.2.14",
|
||||||
"@angular/platform-browser-dynamic": "~8.2.4",
|
"@angular/platform-browser-dynamic": "^8.2.14",
|
||||||
"@angular/pwa": "^0.803.1",
|
"@angular/pwa": "^0.803.23",
|
||||||
"@angular/router": "~8.2.4",
|
"@angular/router": "^8.2.14",
|
||||||
"@angular/service-worker": "~8.2.4",
|
"@angular/service-worker": "^8.2.14",
|
||||||
"@ngx-pwa/local-storage": "~8.2.1",
|
"@ngx-pwa/local-storage": "^8.2.4",
|
||||||
"@ngx-translate/core": "~11.0.1",
|
"@ngx-translate/core": "~11.0.1",
|
||||||
"@ngx-translate/http-loader": "^4.0.0",
|
"@ngx-translate/http-loader": "^4.0.0",
|
||||||
"@pebula/ngrid": "1.0.0-rc.16",
|
"@pebula/ngrid": "1.0.0-rc.16",
|
||||||
"@pebula/ngrid-material": "1.0.0-rc.16",
|
"@pebula/ngrid-material": "1.0.0-rc.16",
|
||||||
"@pebula/utils": "1.0.2",
|
"@pebula/utils": "1.0.2",
|
||||||
"@tinymce/tinymce-angular": "^3.2.0",
|
"@tinymce/tinymce-angular": "^3.3.1",
|
||||||
"acorn": "^7.0.0",
|
"acorn": "^7.1.0",
|
||||||
"core-js": "^3.2.1",
|
"chart.js": "^2.9.2",
|
||||||
"css-element-queries": "^1.2.1",
|
"core-js": "^3.6.4",
|
||||||
|
"css-element-queries": "^1.2.3",
|
||||||
"exceljs": "1.15.0",
|
"exceljs": "1.15.0",
|
||||||
"file-saver": "^2.0.2",
|
"file-saver": "^2.0.2",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"lz4js": "^0.2.0",
|
"lz4js": "^0.2.0",
|
||||||
"material-icon-font": "git+https://github.com/petergng/materialIconFont.git",
|
"material-icon-font": "git+https://github.com/petergng/materialIconFont.git",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
|
"ng2-charts": "^2.3.0",
|
||||||
"ng2-pdf-viewer": "^5.3.4",
|
"ng2-pdf-viewer": "^5.3.4",
|
||||||
"ngx-file-drop": "~8.0.7",
|
"ngx-file-drop": "^8.0.8",
|
||||||
"ngx-mat-select-search": "^1.8.0",
|
"ngx-mat-select-search": "^1.8.0",
|
||||||
"ngx-material-timepicker": "^4.0.2",
|
"ngx-material-timepicker": "^4.0.2",
|
||||||
"ngx-papaparse": "^4.0.2",
|
"ngx-papaparse": "^4.0.2",
|
||||||
"pdfmake": "^0.1.58",
|
"pdfmake": "^0.1.63",
|
||||||
"po2json": "^1.0.0-alpha",
|
"po2json": "^1.0.0-beta-2",
|
||||||
"rxjs": "^6.5.2",
|
"rxjs": "^6.5.4",
|
||||||
"tinymce": "^5.0.14",
|
"tinymce": "^5.1.5",
|
||||||
"tslib": "^1.10.0",
|
"tslib": "^1.10.0",
|
||||||
"uuid": "^3.3.2",
|
"uuid": "^3.3.3",
|
||||||
"zone.js": "~0.9.1"
|
"zone.js": "~0.9.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "~0.803.2",
|
"@angular-devkit/build-angular": "^0.803.23",
|
||||||
"@angular/cli": "~8.3.2",
|
"@angular/cli": "^8.3.23",
|
||||||
"@angular/compiler-cli": "~8.2.4",
|
"@angular/compiler-cli": "^8.2.14",
|
||||||
"@angular/language-service": "~8.2.4",
|
"@angular/language-service": "^8.2.14",
|
||||||
"@biesbjerg/ngx-translate-extract": "^3.0.5",
|
"@biesbjerg/ngx-translate-extract": "^3.0.5",
|
||||||
"@compodoc/compodoc": "^1.1.8",
|
"@compodoc/compodoc": "^1.1.11",
|
||||||
"@types/jasmine": "^3.3.9",
|
"@types/jasmine": "^3.5.0",
|
||||||
"@types/jasminewd2": "^2.0.6",
|
"@types/jasminewd2": "^2.0.8",
|
||||||
"@types/node": "~12.7.2",
|
"@types/node": "^12.7.12",
|
||||||
"@types/yargs": "^13.0.0",
|
"@types/yargs": "^13.0.5",
|
||||||
"codelyzer": "^5.0.1",
|
"codelyzer": "^5.2.1",
|
||||||
"husky": "^3.0.4",
|
"husky": "^3.1.0",
|
||||||
"jasmine-core": "~3.4.0",
|
"jasmine-core": "~3.4.0",
|
||||||
"jasmine-spec-reporter": "~4.2.1",
|
"jasmine-spec-reporter": "~4.2.1",
|
||||||
"karma": "^4.1.0",
|
"karma": "^4.4.1",
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
"karma-coverage-istanbul-reporter": "^2.0.5",
|
"karma-coverage-istanbul-reporter": "^2.1.1",
|
||||||
"karma-jasmine": "~2.0.1",
|
"karma-jasmine": "~2.0.1",
|
||||||
"karma-jasmine-html-reporter": "^1.4.0",
|
"karma-jasmine-html-reporter": "^1.5.1",
|
||||||
"npm-license-crawler": "^0.2.1",
|
"npm-license-crawler": "^0.2.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^1.19.1",
|
||||||
"protractor": "^5.4.2",
|
"protractor": "^5.4.2",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"source-map-explorer": "^2.0.1",
|
"source-map-explorer": "^2.2.2",
|
||||||
"ts-node": "~8.3.0",
|
"ts-node": "~8.3.0",
|
||||||
"tslint": "~5.19.0",
|
"tslint": "~5.19.0",
|
||||||
"tsutils": "3.17.1",
|
"tsutils": "3.17.1",
|
||||||
"typescript": "~3.5.3",
|
"typescript": "~3.5.3",
|
||||||
"webpack-bundle-analyzer": "^3.3.2"
|
"webpack-bundle-analyzer": "^3.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import { PrioritizeService } from './core/core-services/prioritize.service';
|
|||||||
import { RoutingStateService } from './core/ui-services/routing-state.service';
|
import { RoutingStateService } from './core/ui-services/routing-state.service';
|
||||||
import { ServertimeService } from './core/core-services/servertime.service';
|
import { ServertimeService } from './core/core-services/servertime.service';
|
||||||
import { ThemeService } from './core/ui-services/theme.service';
|
import { ThemeService } from './core/ui-services/theme.service';
|
||||||
|
import { VotingBannerService } from './core/ui-services/voting-banner.service';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
/**
|
/**
|
||||||
@ -25,6 +26,8 @@ declare global {
|
|||||||
*/
|
*/
|
||||||
interface Array<T> {
|
interface Array<T> {
|
||||||
flatMap(o: any): any[];
|
flatMap(o: any): any[];
|
||||||
|
intersect(a: T[]): T[];
|
||||||
|
mapToObject(f: (item: T) => { [key: string]: any }): { [key: string]: any };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -79,7 +82,8 @@ export class AppComponent {
|
|||||||
dataStoreUpgradeService: DataStoreUpgradeService, // to start it.
|
dataStoreUpgradeService: DataStoreUpgradeService, // to start it.
|
||||||
prioritizeService: PrioritizeService,
|
prioritizeService: PrioritizeService,
|
||||||
pingService: PingService,
|
pingService: PingService,
|
||||||
routingState: RoutingStateService
|
routingState: RoutingStateService,
|
||||||
|
votingBannerService: VotingBannerService // needed for initialisation
|
||||||
) {
|
) {
|
||||||
// manually add the supported languages
|
// manually add the supported languages
|
||||||
translate.addLangs(['en', 'de', 'cs', 'ru']);
|
translate.addLangs(['en', 'de', 'cs', 'ru']);
|
||||||
@ -92,7 +96,7 @@ export class AppComponent {
|
|||||||
|
|
||||||
// change default JS functions
|
// change default JS functions
|
||||||
this.overloadArrayToString();
|
this.overloadArrayToString();
|
||||||
this.overloadFlatMap();
|
this.overloadArrayFunctions();
|
||||||
this.overloadModulo();
|
this.overloadModulo();
|
||||||
|
|
||||||
// Wait until the App reaches a stable state.
|
// Wait until the App reaches a stable state.
|
||||||
@ -138,8 +142,7 @@ export class AppComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds an implementation of flatMap.
|
* Adds an implementation of flatMap and intersect.
|
||||||
* TODO: Remove once flatMap made its way into official JS/TS (ES 2019?)
|
|
||||||
*/
|
*/
|
||||||
private overloadFlatMap(): void {
|
private overloadFlatMap(): void {
|
||||||
Object.defineProperty(Array.prototype, 'flatMap', {
|
Object.defineProperty(Array.prototype, 'flatMap', {
|
||||||
@ -150,6 +153,34 @@ export class AppComponent {
|
|||||||
},
|
},
|
||||||
enumerable: false
|
enumerable: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(Array.prototype, 'intersect', {
|
||||||
|
value: function<T>(other: T[]): T[] {
|
||||||
|
let a = this,
|
||||||
|
b = other;
|
||||||
|
// indexOf to loop over shorter
|
||||||
|
if (b.length > a.length) {
|
||||||
|
[a, b] = [b, a];
|
||||||
|
}
|
||||||
|
return a.filter(e => b.indexOf(e) > -1);
|
||||||
|
},
|
||||||
|
enumerable: false
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(Array.prototype, 'mapToObject', {
|
||||||
|
value: function<T>(f: (item: T) => { [key: string]: any }): { [key: string]: any } {
|
||||||
|
return this.reduce((aggr, item) => {
|
||||||
|
const res = f(item);
|
||||||
|
for (const key in res) {
|
||||||
|
if (res.hasOwnProperty(key)) {
|
||||||
|
aggr[key] = res[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return aggr;
|
||||||
|
}, {});
|
||||||
|
},
|
||||||
|
enumerable: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { BannerDefinition, BannerService } from '../ui-services/banner.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This service handles everything connected with being offline.
|
* This service handles everything connected with being offline.
|
||||||
*
|
*
|
||||||
@ -16,6 +19,16 @@ export class OfflineService {
|
|||||||
* BehaviorSubject to receive further status values.
|
* BehaviorSubject to receive further status values.
|
||||||
*/
|
*/
|
||||||
private offline = new BehaviorSubject<boolean>(false);
|
private offline = new BehaviorSubject<boolean>(false);
|
||||||
|
private bannerDefinition: BannerDefinition = {
|
||||||
|
text: 'Offline mode',
|
||||||
|
icon: 'cloud_off'
|
||||||
|
};
|
||||||
|
|
||||||
|
public constructor(private banner: BannerService, translate: TranslateService) {
|
||||||
|
translate.onLangChange.subscribe(() => {
|
||||||
|
this.bannerDefinition.text = translate.instant(this.bannerDefinition.text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines of you are either in Offline mode or not connected via websocket
|
* Determines of you are either in Offline mode or not connected via websocket
|
||||||
@ -33,7 +46,7 @@ export class OfflineService {
|
|||||||
if (!this.offline.getValue()) {
|
if (!this.offline.getValue()) {
|
||||||
console.log('offline because whoami failed.');
|
console.log('offline because whoami failed.');
|
||||||
}
|
}
|
||||||
this.offline.next(true);
|
this.goOffline();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,7 +56,15 @@ export class OfflineService {
|
|||||||
if (!this.offline.getValue()) {
|
if (!this.offline.getValue()) {
|
||||||
console.log('offline because connection lost.');
|
console.log('offline because connection lost.');
|
||||||
}
|
}
|
||||||
|
this.goOffline();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to set offline status
|
||||||
|
*/
|
||||||
|
private goOffline(): void {
|
||||||
this.offline.next(true);
|
this.offline.next(true);
|
||||||
|
this.banner.addBanner(this.bannerDefinition);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,5 +72,6 @@ export class OfflineService {
|
|||||||
*/
|
*/
|
||||||
public goOnline(): void {
|
public goOnline(): void {
|
||||||
this.offline.next(false);
|
this.offline.next(false);
|
||||||
|
this.banner.removeBanner(this.bannerDefinition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { History } from 'app/shared/models/core/history';
|
import { History } from 'app/shared/models/core/history';
|
||||||
|
import { BannerDefinition, BannerService } from '../ui-services/banner.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds information about OpenSlides. This is not included into other services to
|
* Holds information about OpenSlides. This is not included into other services to
|
||||||
@ -14,6 +15,9 @@ export class OpenSlidesStatusService {
|
|||||||
* in History mode, saves the history point.
|
* in History mode, saves the history point.
|
||||||
*/
|
*/
|
||||||
private history: History = null;
|
private history: History = null;
|
||||||
|
private bannerDefinition: BannerDefinition = {
|
||||||
|
type: 'history'
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns, if OpenSlides is in the history mode.
|
* Returns, if OpenSlides is in the history mode.
|
||||||
@ -27,7 +31,7 @@ export class OpenSlidesStatusService {
|
|||||||
/**
|
/**
|
||||||
* Ctor, does nothing.
|
* Ctor, does nothing.
|
||||||
*/
|
*/
|
||||||
public constructor() {}
|
public constructor(private banner: BannerService) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls the getLocaleString function of the history object, if present.
|
* Calls the getLocaleString function of the history object, if present.
|
||||||
@ -44,6 +48,7 @@ export class OpenSlidesStatusService {
|
|||||||
*/
|
*/
|
||||||
public enterHistoryMode(history: History): void {
|
public enterHistoryMode(history: History): void {
|
||||||
this.history = history;
|
this.history = history;
|
||||||
|
this.banner.addBanner(this.bannerDefinition);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,5 +56,6 @@ export class OpenSlidesStatusService {
|
|||||||
*/
|
*/
|
||||||
public leaveHistoryMode(): void {
|
public leaveHistoryMode(): void {
|
||||||
this.history = null;
|
this.history = null;
|
||||||
|
this.banner.removeBanner(this.bannerDefinition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -187,12 +187,24 @@ export class RelationManagerService {
|
|||||||
const _model: M = target.getModel();
|
const _model: M = target.getModel();
|
||||||
const relation = typeof property === 'string' ? relationsByKey[property] : null;
|
const relation = typeof property === 'string' ? relationsByKey[property] : null;
|
||||||
|
|
||||||
|
// try to find a getter for property
|
||||||
if (property in target) {
|
if (property in target) {
|
||||||
const descriptor = Object.getOwnPropertyDescriptor(viewModelCtor.prototype, property);
|
// iterate over prototype chain
|
||||||
|
let prototypeFunc = viewModelCtor,
|
||||||
|
descriptor = null;
|
||||||
|
do {
|
||||||
|
descriptor = Object.getOwnPropertyDescriptor(prototypeFunc.prototype, property);
|
||||||
|
if (!descriptor || !descriptor.get) {
|
||||||
|
prototypeFunc = Object.getPrototypeOf(prototypeFunc);
|
||||||
|
}
|
||||||
|
} while (!(descriptor && descriptor.get) && prototypeFunc && prototypeFunc.prototype);
|
||||||
|
|
||||||
if (descriptor && descriptor.get) {
|
if (descriptor && descriptor.get) {
|
||||||
|
// if getter was found in prototype chain, bind it with this proxy for right `this` access
|
||||||
result = descriptor.get.bind(viewModel)();
|
result = descriptor.get.bind(viewModel)();
|
||||||
} else {
|
} else {
|
||||||
result = target[property];
|
result = target[property];
|
||||||
|
// console.log(property, target);
|
||||||
}
|
}
|
||||||
} else if (property in _model) {
|
} else if (property in _model) {
|
||||||
result = _model[property];
|
result = _model[property];
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
export interface HasViewModelListObservable<V> {
|
||||||
|
getViewModelListObservable(): Observable<V[]>;
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { AssignmentOptionRepositoryService } from './assignment-option-repository.service';
|
||||||
|
|
||||||
|
describe('AssignmentOptionRepositoryService', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: AssignmentOptionRepositoryService = TestBed.get(AssignmentOptionRepositoryService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,75 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { DataSendService } from 'app/core/core-services/data-send.service';
|
||||||
|
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||||
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
|
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||||
|
import { AssignmentOption } from 'app/shared/models/assignments/assignment-option';
|
||||||
|
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
|
||||||
|
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||||
|
import { ViewAssignmentVote } from 'app/site/assignments/models/view-assignment-vote';
|
||||||
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
|
import { BaseRepository } from '../base-repository';
|
||||||
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
|
|
||||||
|
const AssignmentOptionRelations: RelationDefinition[] = [
|
||||||
|
{
|
||||||
|
type: 'O2M',
|
||||||
|
foreignIdKey: 'option_id',
|
||||||
|
ownKey: 'votes',
|
||||||
|
foreignViewModel: ViewAssignmentVote
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'M2O',
|
||||||
|
ownIdKey: 'poll_id',
|
||||||
|
ownKey: 'poll',
|
||||||
|
foreignViewModel: ViewAssignmentPoll
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'M2O',
|
||||||
|
ownIdKey: 'user_id',
|
||||||
|
ownKey: 'user',
|
||||||
|
foreignViewModel: ViewUser
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository Service for Options.
|
||||||
|
*
|
||||||
|
* Documentation partially provided in {@link BaseRepository}
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AssignmentOptionRepositoryService extends BaseRepository<ViewAssignmentOption, AssignmentOption, object> {
|
||||||
|
public constructor(
|
||||||
|
DS: DataStoreService,
|
||||||
|
dataSend: DataSendService,
|
||||||
|
mapperService: CollectionStringMapperService,
|
||||||
|
viewModelStoreService: ViewModelStoreService,
|
||||||
|
translate: TranslateService,
|
||||||
|
relationManager: RelationManagerService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
DS,
|
||||||
|
dataSend,
|
||||||
|
mapperService,
|
||||||
|
viewModelStoreService,
|
||||||
|
translate,
|
||||||
|
relationManager,
|
||||||
|
AssignmentOption,
|
||||||
|
AssignmentOptionRelations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTitle = (titleInformation: object) => {
|
||||||
|
return 'Option';
|
||||||
|
};
|
||||||
|
|
||||||
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
|
return this.translate.instant(plural ? 'Options' : 'Option');
|
||||||
|
};
|
||||||
|
}
|
@ -3,17 +3,18 @@ import { Injectable } from '@angular/core';
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { DataSendService } from 'app/core/core-services/data-send.service';
|
import { DataSendService } from 'app/core/core-services/data-send.service';
|
||||||
|
import { HttpService } from 'app/core/core-services/http.service';
|
||||||
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||||
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
import { RelationDefinition } from 'app/core/definitions/relations';
|
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||||
import { AssignmentOption } from 'app/shared/models/assignments/assignment-option';
|
import { VotingService } from 'app/core/ui-services/voting.service';
|
||||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||||
|
import { ViewAssignment } from 'app/site/assignments/models/view-assignment';
|
||||||
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
|
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
|
||||||
import { AssignmentPollTitleInformation, ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
import { AssignmentPollTitleInformation, ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||||
import { ViewAssignmentVote } from 'app/site/assignments/models/view-assignment-vote';
|
import { BasePollRepositoryService } from 'app/site/polls/services/base-poll-repository.service';
|
||||||
import { ViewGroup } from 'app/site/users/models/view-group';
|
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { BaseRepository, NestedModelDescriptors } from '../base-repository';
|
|
||||||
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
import { DataStoreService } from '../../core-services/data-store.service';
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
|
|
||||||
@ -29,36 +30,35 @@ const AssignmentPollRelations: RelationDefinition[] = [
|
|||||||
ownIdKey: 'voted_id',
|
ownIdKey: 'voted_id',
|
||||||
ownKey: 'voted',
|
ownKey: 'voted',
|
||||||
foreignViewModel: ViewUser
|
foreignViewModel: ViewUser
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'O2M',
|
||||||
|
ownIdKey: 'options_id',
|
||||||
|
ownKey: 'options',
|
||||||
|
foreignViewModel: ViewAssignmentOption
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'M2O',
|
||||||
|
ownIdKey: 'assignment_id',
|
||||||
|
ownKey: 'assignment',
|
||||||
|
foreignViewModel: ViewAssignment
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const AssignmentPollNestedModelDescriptors: NestedModelDescriptors = {
|
export interface AssignmentAnalogVoteData {
|
||||||
'assignments/assignment-poll': [
|
options: {
|
||||||
{
|
[key: number]: {
|
||||||
ownKey: 'options',
|
Y: number;
|
||||||
foreignViewModel: ViewAssignmentOption,
|
N?: number;
|
||||||
foreignModel: AssignmentOption,
|
A?: number;
|
||||||
order: 'weight',
|
};
|
||||||
relationDefinitionsByKey: {
|
};
|
||||||
user: {
|
votesvalid?: number;
|
||||||
type: 'M2O',
|
votesinvalid?: number;
|
||||||
ownIdKey: 'user_id',
|
votescast?: number;
|
||||||
ownKey: 'user',
|
global_no?: number;
|
||||||
foreignViewModel: ViewUser
|
global_abstain?: number;
|
||||||
},
|
}
|
||||||
votes: {
|
|
||||||
type: 'O2M',
|
|
||||||
foreignIdKey: 'option_id',
|
|
||||||
ownKey: 'votes',
|
|
||||||
foreignViewModel: ViewAssignmentVote
|
|
||||||
}
|
|
||||||
},
|
|
||||||
titles: {
|
|
||||||
getTitle: (viewOption: ViewAssignmentOption) => (viewOption.user ? viewOption.user.getTitle() : '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository Service for Assignments.
|
* Repository Service for Assignments.
|
||||||
@ -68,7 +68,7 @@ const AssignmentPollNestedModelDescriptors: NestedModelDescriptors = {
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class AssignmentPollRepositoryService extends BaseRepository<
|
export class AssignmentPollRepositoryService extends BasePollRepositoryService<
|
||||||
ViewAssignmentPoll,
|
ViewAssignmentPoll,
|
||||||
AssignmentPoll,
|
AssignmentPoll,
|
||||||
AssignmentPollTitleInformation
|
AssignmentPollTitleInformation
|
||||||
@ -89,7 +89,9 @@ export class AssignmentPollRepositoryService extends BaseRepository<
|
|||||||
mapperService: CollectionStringMapperService,
|
mapperService: CollectionStringMapperService,
|
||||||
viewModelStoreService: ViewModelStoreService,
|
viewModelStoreService: ViewModelStoreService,
|
||||||
translate: TranslateService,
|
translate: TranslateService,
|
||||||
relationManager: RelationManagerService
|
relationManager: RelationManagerService,
|
||||||
|
votingService: VotingService,
|
||||||
|
http: HttpService
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
DS,
|
DS,
|
||||||
@ -100,7 +102,9 @@ export class AssignmentPollRepositoryService extends BaseRepository<
|
|||||||
relationManager,
|
relationManager,
|
||||||
AssignmentPoll,
|
AssignmentPoll,
|
||||||
AssignmentPollRelations,
|
AssignmentPollRelations,
|
||||||
AssignmentPollNestedModelDescriptors
|
{},
|
||||||
|
votingService,
|
||||||
|
http
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,4 +115,8 @@ export class AssignmentPollRepositoryService extends BaseRepository<
|
|||||||
public getVerboseName = (plural: boolean = false) => {
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
return this.translate.instant(plural ? 'Polls' : 'Poll');
|
return this.translate.instant(plural ? 'Polls' : 'Poll');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public vote(data: any, poll_id: number): Promise<void> {
|
||||||
|
return this.http.post(`/rest/assignments/assignment-poll/${poll_id}/vote/`, data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { RelationManagerService } from 'app/core/core-services/relation-manager.
|
|||||||
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
import { RelationDefinition } from 'app/core/definitions/relations';
|
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||||
import { AssignmentVote } from 'app/shared/models/assignments/assignment-vote';
|
import { AssignmentVote } from 'app/shared/models/assignments/assignment-vote';
|
||||||
|
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
|
||||||
import { ViewAssignmentVote } from 'app/site/assignments/models/view-assignment-vote';
|
import { ViewAssignmentVote } from 'app/site/assignments/models/view-assignment-vote';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { BaseRepository } from '../base-repository';
|
import { BaseRepository } from '../base-repository';
|
||||||
@ -19,6 +20,12 @@ const AssignmentVoteRelations: RelationDefinition[] = [
|
|||||||
ownIdKey: 'user_id',
|
ownIdKey: 'user_id',
|
||||||
ownKey: 'user',
|
ownKey: 'user',
|
||||||
foreignViewModel: ViewUser
|
foreignViewModel: ViewUser
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'M2O',
|
||||||
|
ownIdKey: 'option_id',
|
||||||
|
ownKey: 'option',
|
||||||
|
foreignViewModel: ViewAssignmentOption
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -66,4 +73,8 @@ export class AssignmentVoteRepositoryService extends BaseRepository<ViewAssignme
|
|||||||
public getVerboseName = (plural: boolean = false) => {
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
return this.translate.instant(plural ? 'Votes' : 'Vote');
|
return this.translate.instant(plural ? 'Votes' : 'Vote');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public getVotesForUser(pollId: number, userId: number): ViewAssignmentVote[] {
|
||||||
|
return this.getViewModelList().filter(vote => vote.option.poll_id === pollId && vote.user_id === userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { BaseViewModel, TitleInformation, ViewModelConstructor } from '../../sit
|
|||||||
import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service';
|
import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service';
|
||||||
import { DataSendService } from '../core-services/data-send.service';
|
import { DataSendService } from '../core-services/data-send.service';
|
||||||
import { DataStoreService } from '../core-services/data-store.service';
|
import { DataStoreService } from '../core-services/data-store.service';
|
||||||
|
import { HasViewModelListObservable } from '../definitions/has-view-model-list-observable';
|
||||||
import { Identifiable } from '../../shared/models/base/identifiable';
|
import { Identifiable } from '../../shared/models/base/identifiable';
|
||||||
import { OnAfterAppsLoaded } from '../definitions/on-after-apps-loaded';
|
import { OnAfterAppsLoaded } from '../definitions/on-after-apps-loaded';
|
||||||
import { RelationManagerService } from '../core-services/relation-manager.service';
|
import { RelationManagerService } from '../core-services/relation-manager.service';
|
||||||
@ -30,7 +31,7 @@ export interface NestedModelDescriptors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export abstract class BaseRepository<V extends BaseViewModel & T, M extends BaseModel, T extends TitleInformation>
|
export abstract class BaseRepository<V extends BaseViewModel & T, M extends BaseModel, T extends TitleInformation>
|
||||||
implements OnAfterAppsLoaded, Collection {
|
implements OnAfterAppsLoaded, Collection, HasViewModelListObservable<V> {
|
||||||
/**
|
/**
|
||||||
* Stores all the viewModel in an object
|
* Stores all the viewModel in an object
|
||||||
*/
|
*/
|
||||||
@ -42,8 +43,8 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
protected viewModelSubjects: { [modelId: number]: BehaviorSubject<V> } = {};
|
protected viewModelSubjects: { [modelId: number]: BehaviorSubject<V> } = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable subject for the whole list. These entries are unsorted an not piped through
|
* Observable subject for the whole list. These entries are unsorted and not piped through
|
||||||
* autodTime. Just use this internally.
|
* auditTime. Just use this internally.
|
||||||
*
|
*
|
||||||
* It's used to debounce messages on the sortedViewModelListSubject
|
* It's used to debounce messages on the sortedViewModelListSubject
|
||||||
*/
|
*/
|
||||||
@ -188,7 +189,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After creating a view model, all functions for models form the repo
|
* After creating a view model, all functions for models from the repo
|
||||||
* are assigned to the new view model.
|
* are assigned to the new view model.
|
||||||
*/
|
*/
|
||||||
protected createViewModelWithTitles(model: M): V {
|
protected createViewModelWithTitles(model: M): V {
|
||||||
@ -269,7 +270,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
this.viewModelStore = {};
|
this.viewModelStore = {};
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* The function used for sorting the data of this repository. The defualt sorts by ID.
|
* The function used for sorting the data of this repository. The default sorts by ID.
|
||||||
*/
|
*/
|
||||||
protected viewModelSortFn: (a: V, b: V) => number = (a: V, b: V) => a.id - b.id;
|
protected viewModelSortFn: (a: V, b: V) => number = (a: V, b: V) => a.id - b.id;
|
||||||
|
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { MotionOptionRepositoryService } from './motion-option-repository.service';
|
||||||
|
|
||||||
|
describe('MotionOptionRepositoryService', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: MotionOptionRepositoryService = TestBed.get(MotionOptionRepositoryService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,68 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { DataSendService } from 'app/core/core-services/data-send.service';
|
||||||
|
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||||
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
|
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||||
|
import { MotionOption } from 'app/shared/models/motions/motion-option';
|
||||||
|
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
||||||
|
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||||
|
import { ViewMotionVote } from 'app/site/motions/models/view-motion-vote';
|
||||||
|
import { BaseRepository } from '../base-repository';
|
||||||
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
|
|
||||||
|
const MotionOptionRelations: RelationDefinition[] = [
|
||||||
|
{
|
||||||
|
type: 'O2M',
|
||||||
|
foreignIdKey: 'option_id',
|
||||||
|
ownKey: 'votes',
|
||||||
|
foreignViewModel: ViewMotionVote
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'M2O',
|
||||||
|
ownIdKey: 'poll_id',
|
||||||
|
ownKey: 'poll',
|
||||||
|
foreignViewModel: ViewMotionPoll
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository Service for Options.
|
||||||
|
*
|
||||||
|
* Documentation partially provided in {@link BaseRepository}
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class MotionOptionRepositoryService extends BaseRepository<ViewMotionOption, MotionOption, object> {
|
||||||
|
public constructor(
|
||||||
|
DS: DataStoreService,
|
||||||
|
dataSend: DataSendService,
|
||||||
|
mapperService: CollectionStringMapperService,
|
||||||
|
viewModelStoreService: ViewModelStoreService,
|
||||||
|
translate: TranslateService,
|
||||||
|
relationManager: RelationManagerService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
DS,
|
||||||
|
dataSend,
|
||||||
|
mapperService,
|
||||||
|
viewModelStoreService,
|
||||||
|
translate,
|
||||||
|
relationManager,
|
||||||
|
MotionOption,
|
||||||
|
MotionOptionRelations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTitle = (titleInformation: object) => {
|
||||||
|
return 'Option';
|
||||||
|
};
|
||||||
|
|
||||||
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
|
return this.translate.instant(plural ? 'Options' : 'Option');
|
||||||
|
};
|
||||||
|
}
|
@ -3,17 +3,17 @@ import { Injectable } from '@angular/core';
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { DataSendService } from 'app/core/core-services/data-send.service';
|
import { DataSendService } from 'app/core/core-services/data-send.service';
|
||||||
|
import { HttpService } from 'app/core/core-services/http.service';
|
||||||
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||||
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
import { RelationDefinition } from 'app/core/definitions/relations';
|
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||||
import { MotionOption } from 'app/shared/models/motions/motion-option';
|
import { VotingService } from 'app/core/ui-services/voting.service';
|
||||||
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
|
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
|
||||||
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
||||||
import { MotionPollTitleInformation, ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
import { MotionPollTitleInformation, ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||||
import { ViewMotionVote } from 'app/site/motions/models/view-motion-vote';
|
import { BasePollRepositoryService } from 'app/site/polls/services/base-poll-repository.service';
|
||||||
import { ViewGroup } from 'app/site/users/models/view-group';
|
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { BaseRepository, NestedModelDescriptors } from '../base-repository';
|
|
||||||
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
import { DataStoreService } from '../../core-services/data-store.service';
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
|
|
||||||
@ -29,30 +29,15 @@ const MotionPollRelations: RelationDefinition[] = [
|
|||||||
ownIdKey: 'voted_id',
|
ownIdKey: 'voted_id',
|
||||||
ownKey: 'voted',
|
ownKey: 'voted',
|
||||||
foreignViewModel: ViewUser
|
foreignViewModel: ViewUser
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'O2M',
|
||||||
|
ownIdKey: 'options_id',
|
||||||
|
ownKey: 'options',
|
||||||
|
foreignViewModel: ViewMotionOption
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const MotionPollNestedModelDescriptors: NestedModelDescriptors = {
|
|
||||||
'motions/motion-poll': [
|
|
||||||
{
|
|
||||||
ownKey: 'options',
|
|
||||||
foreignViewModel: ViewMotionOption,
|
|
||||||
foreignModel: MotionOption,
|
|
||||||
relationDefinitionsByKey: {
|
|
||||||
votes: {
|
|
||||||
type: 'O2M',
|
|
||||||
foreignIdKey: 'option_id',
|
|
||||||
ownKey: 'votes',
|
|
||||||
foreignViewModel: ViewMotionVote
|
|
||||||
}
|
|
||||||
},
|
|
||||||
titles: {
|
|
||||||
getTitle: (viewOption: ViewMotionOption) => ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository Service for Assignments.
|
* Repository Service for Assignments.
|
||||||
*
|
*
|
||||||
@ -61,7 +46,7 @@ const MotionPollNestedModelDescriptors: NestedModelDescriptors = {
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class MotionPollRepositoryService extends BaseRepository<
|
export class MotionPollRepositoryService extends BasePollRepositoryService<
|
||||||
ViewMotionPoll,
|
ViewMotionPoll,
|
||||||
MotionPoll,
|
MotionPoll,
|
||||||
MotionPollTitleInformation
|
MotionPollTitleInformation
|
||||||
@ -72,7 +57,9 @@ export class MotionPollRepositoryService extends BaseRepository<
|
|||||||
mapperService: CollectionStringMapperService,
|
mapperService: CollectionStringMapperService,
|
||||||
viewModelStoreService: ViewModelStoreService,
|
viewModelStoreService: ViewModelStoreService,
|
||||||
translate: TranslateService,
|
translate: TranslateService,
|
||||||
relationManager: RelationManagerService
|
relationManager: RelationManagerService,
|
||||||
|
votingService: VotingService,
|
||||||
|
http: HttpService
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
DS,
|
DS,
|
||||||
@ -83,7 +70,9 @@ export class MotionPollRepositoryService extends BaseRepository<
|
|||||||
relationManager,
|
relationManager,
|
||||||
MotionPoll,
|
MotionPoll,
|
||||||
MotionPollRelations,
|
MotionPollRelations,
|
||||||
MotionPollNestedModelDescriptors
|
{},
|
||||||
|
votingService,
|
||||||
|
http
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,4 +83,8 @@ export class MotionPollRepositoryService extends BaseRepository<
|
|||||||
public getVerboseName = (plural: boolean = false) => {
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
return this.translate.instant(plural ? 'Polls' : 'Poll');
|
return this.translate.instant(plural ? 'Polls' : 'Poll');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public vote(vote: 'Y' | 'N' | 'A', poll_id: number): Promise<void> {
|
||||||
|
return this.http.post(`/rest/motions/motion-poll/${poll_id}/vote/`, JSON.stringify(vote));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { RelationManagerService } from 'app/core/core-services/relation-manager.
|
|||||||
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
import { RelationDefinition } from 'app/core/definitions/relations';
|
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||||
import { MotionVote } from 'app/shared/models/motions/motion-vote';
|
import { MotionVote } from 'app/shared/models/motions/motion-vote';
|
||||||
|
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
||||||
import { ViewMotionVote } from 'app/site/motions/models/view-motion-vote';
|
import { ViewMotionVote } from 'app/site/motions/models/view-motion-vote';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { BaseRepository } from '../base-repository';
|
import { BaseRepository } from '../base-repository';
|
||||||
@ -19,6 +20,12 @@ const MotionVoteRelations: RelationDefinition[] = [
|
|||||||
ownIdKey: 'user_id',
|
ownIdKey: 'user_id',
|
||||||
ownKey: 'user',
|
ownKey: 'user',
|
||||||
foreignViewModel: ViewUser
|
foreignViewModel: ViewUser
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'M2O',
|
||||||
|
ownIdKey: 'option_id',
|
||||||
|
ownKey: 'option',
|
||||||
|
foreignViewModel: ViewMotionOption
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -72,6 +72,13 @@ export class GroupRepositoryService extends BaseRepository<ViewGroup, Group, Gro
|
|||||||
return this.translate.instant(plural ? 'Groups' : 'Group');
|
return this.translate.instant(plural ? 'Groups' : 'Group');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public getNameForIds(...ids: number[]): string {
|
||||||
|
return this.getSortedViewModelList()
|
||||||
|
.filter(group => ids.includes(group.id))
|
||||||
|
.map(group => this.translate.instant(group.getTitle()))
|
||||||
|
.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles the given permisson.
|
* Toggles the given permisson.
|
||||||
*
|
*
|
||||||
|
12
client/src/app/core/ui-services/banner.service.spec.ts
Normal file
12
client/src/app/core/ui-services/banner.service.spec.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { BannerService } from './banner.service';
|
||||||
|
|
||||||
|
describe('BannerService', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({}));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: BannerService = TestBed.get(BannerService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
68
client/src/app/core/ui-services/banner.service.ts
Normal file
68
client/src/app/core/ui-services/banner.service.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
|
export interface BannerDefinition {
|
||||||
|
type?: string;
|
||||||
|
class?: string;
|
||||||
|
icon?: string;
|
||||||
|
text?: string;
|
||||||
|
bgColor?: string;
|
||||||
|
color?: string;
|
||||||
|
link?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service handling the active banners at the top of the site. Banners are defined via a BannerDefinition
|
||||||
|
* and are removed by reference so the service adding a banner has to store the reference to remove it later
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class BannerService {
|
||||||
|
public readonly BANNER_HEIGHT = 20;
|
||||||
|
|
||||||
|
public activeBanners: BehaviorSubject<BannerDefinition[]> = new BehaviorSubject<BannerDefinition[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a banner to the list of active banners. Skip the banner if it's already in the list
|
||||||
|
* @param toAdd the banner to add
|
||||||
|
*/
|
||||||
|
public addBanner(toAdd: BannerDefinition): void {
|
||||||
|
if (!this.activeBanners.value.find(banner => banner === toAdd)) {
|
||||||
|
const newBanners = this.activeBanners.value.concat([toAdd]);
|
||||||
|
this.activeBanners.next(newBanners);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces a banner with another. Convenience method to prevent flickering
|
||||||
|
* @param toAdd the banner to add
|
||||||
|
* @param toRemove the banner to remove
|
||||||
|
*/
|
||||||
|
public replaceBanner(toRemove: BannerDefinition, toAdd: BannerDefinition): void {
|
||||||
|
if (toRemove) {
|
||||||
|
const newArray = Array.from(this.activeBanners.value);
|
||||||
|
const idx = newArray.findIndex(banner => banner === toRemove);
|
||||||
|
if (idx === -1) {
|
||||||
|
throw new Error("The given banner couldn't be found.");
|
||||||
|
} else {
|
||||||
|
newArray[idx] = toAdd;
|
||||||
|
this.activeBanners.next(newArray); // no need for this.update since the length doesn't change
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.addBanner(toAdd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* removes the given banner
|
||||||
|
* @param toRemove the banner to remove
|
||||||
|
*/
|
||||||
|
public removeBanner(toRemove: BannerDefinition): void {
|
||||||
|
if (toRemove) {
|
||||||
|
const newBanners = this.activeBanners.value.filter(banner => banner !== toRemove);
|
||||||
|
this.activeBanners.next(newBanners);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -534,6 +534,8 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
|
|||||||
if (item[filter.property].id === option.condition) {
|
if (item[filter.property].id === option.condition) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
} else if (typeof item[filter.property] === 'function') {
|
||||||
|
return item[filter.property]() === option.condition;
|
||||||
} else if (item[filter.property] === option.condition) {
|
} else if (item[filter.property] === option.condition) {
|
||||||
return true;
|
return true;
|
||||||
} else if (item[filter.property].toString() === option.condition) {
|
} else if (item[filter.property].toString() === option.condition) {
|
||||||
|
67
client/src/app/core/ui-services/base-poll-dialog.service.ts
Normal file
67
client/src/app/core/ui-services/base-poll-dialog.service.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { ComponentType } from '@angular/cdk/portal';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material';
|
||||||
|
|
||||||
|
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
|
||||||
|
import { Collection } from 'app/shared/models/base/collection';
|
||||||
|
import { PollState, PollType } from 'app/shared/models/poll/base-poll';
|
||||||
|
import { mediumDialogSettings } from 'app/shared/utils/dialog-settings';
|
||||||
|
import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.component';
|
||||||
|
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||||
|
import { PollService } from '../../site/polls/services/poll.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class for showing a poll dialog. Has to be subclassed to provide the right `PollService`
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export abstract class BasePollDialogService<V extends ViewBasePoll> {
|
||||||
|
protected dialogComponent: ComponentType<BasePollDialogComponent>;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private mapper: CollectionStringMapperService,
|
||||||
|
private service: PollService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the dialog to enter votes and edit the meta-info for a poll.
|
||||||
|
*
|
||||||
|
* @param data Passing the (existing or new) data for the poll
|
||||||
|
*/
|
||||||
|
public async openDialog(poll: Partial<V> & Collection): Promise<void> {
|
||||||
|
if (!poll.poll) {
|
||||||
|
this.service.fillDefaultPollData(poll);
|
||||||
|
}
|
||||||
|
const dialogRef = this.dialog.open(this.dialogComponent, {
|
||||||
|
data: poll,
|
||||||
|
...mediumDialogSettings
|
||||||
|
});
|
||||||
|
const result = await dialogRef.afterClosed().toPromise();
|
||||||
|
if (result) {
|
||||||
|
const repo = this.mapper.getRepository(poll.collectionString);
|
||||||
|
if (!poll.poll) {
|
||||||
|
await repo.create(result);
|
||||||
|
} else {
|
||||||
|
let update = result;
|
||||||
|
if (poll.state !== PollState.Created) {
|
||||||
|
update = {
|
||||||
|
title: result.title,
|
||||||
|
onehundred_percent_base: result.onehundred_percent_base,
|
||||||
|
majority_method: result.majority_method,
|
||||||
|
description: result.description
|
||||||
|
};
|
||||||
|
if (poll.type === PollType.Analog) {
|
||||||
|
update = {
|
||||||
|
...update,
|
||||||
|
votes: result.votes,
|
||||||
|
publish_immediately: result.publish_immediately
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await repo.patch(update, <V>poll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +0,0 @@
|
|||||||
import { inject, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { E2EImportsModule } from 'e2e-imports.module';
|
|
||||||
|
|
||||||
import { PollService } from './poll.service';
|
|
||||||
|
|
||||||
describe('PollService', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [E2EImportsModule],
|
|
||||||
providers: [PollService]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be created', inject([PollService], (service: PollService) => {
|
|
||||||
expect(service).toBeTruthy();
|
|
||||||
}));
|
|
||||||
});
|
|
@ -1,216 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
import { _ } from 'app/core/translate/translation-marker';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The possible keys of a poll object that represent numbers.
|
|
||||||
* TODO Should be 'key of MotionPoll|AssinmentPoll if type of key is number'
|
|
||||||
*/
|
|
||||||
export type CalculablePollKey =
|
|
||||||
| 'votesvalid'
|
|
||||||
| 'votesinvalid'
|
|
||||||
| 'votescast'
|
|
||||||
| 'yes'
|
|
||||||
| 'no'
|
|
||||||
| 'abstain'
|
|
||||||
| 'votesno'
|
|
||||||
| 'votesabstain';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: may be obsolete if the server switches to lower case only
|
|
||||||
* (lower case variants are already in CalculablePollKey)
|
|
||||||
*/
|
|
||||||
export type PollVoteValue = 'Yes' | 'No' | 'Abstain' | 'Votes';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface representing possible majority calculation methods. The implementing
|
|
||||||
* calc function should return an integer number that must be reached for the
|
|
||||||
* option to successfully fulfill the quorum, or null if disabled
|
|
||||||
*/
|
|
||||||
export interface MajorityMethod {
|
|
||||||
value: string;
|
|
||||||
display_name: string;
|
|
||||||
calc: (base: number) => number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to round up the passed value of a poll.
|
|
||||||
*
|
|
||||||
* @param value The calculated value of 100%-base.
|
|
||||||
* @param addOne Flag, if the result should be increased by 1.
|
|
||||||
*
|
|
||||||
* @returns The necessary value to get the majority.
|
|
||||||
*/
|
|
||||||
export const calcMajority = (value: number, addOne: boolean = false) => {
|
|
||||||
return Math.ceil(value) + (addOne ? 1 : 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of available majority methods, used in motion and assignment polls
|
|
||||||
*/
|
|
||||||
export const PollMajorityMethod: MajorityMethod[] = [
|
|
||||||
{
|
|
||||||
value: 'simple_majority',
|
|
||||||
display_name: 'Simple majority',
|
|
||||||
calc: base => calcMajority(base * 0.5, true)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'two-thirds_majority',
|
|
||||||
display_name: 'Two-thirds majority',
|
|
||||||
calc: base => calcMajority((base / 3) * 2)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'three-quarters_majority',
|
|
||||||
display_name: 'Three-quarters majority',
|
|
||||||
calc: base => calcMajority((base / 4) * 3)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'disabled',
|
|
||||||
display_name: 'Disabled',
|
|
||||||
calc: a => null
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared service class for polls. Used by child classes {@link MotionPollService}
|
|
||||||
* and {@link AssignmentPollService}
|
|
||||||
*/
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export abstract class PollService {
|
|
||||||
/**
|
|
||||||
* The chosen and currently used base for percentage calculations. Is
|
|
||||||
* supposed to be set by a config service
|
|
||||||
*/
|
|
||||||
public percentBase: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default majority method (to be set set per config).
|
|
||||||
*/
|
|
||||||
public defaultMajorityMethod: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The majority method currently in use
|
|
||||||
*/
|
|
||||||
public majorityMethod: MajorityMethod;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An array of value - label pairs for special value signifiers.
|
|
||||||
* TODO: Should be given by the server, and editable. For now they are hard
|
|
||||||
* coded
|
|
||||||
*/
|
|
||||||
private _specialPollVotes: [number, string][] = [
|
|
||||||
[-1, 'majority'],
|
|
||||||
[-2, 'undocumented']
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getter for the special vote values
|
|
||||||
*
|
|
||||||
* @returns an array of special (non-positive) numbers used in polls and
|
|
||||||
* their descriptive strings
|
|
||||||
*/
|
|
||||||
public get specialPollVotes(): [number, string][] {
|
|
||||||
return this._specialPollVotes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* empty constructor
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public constructor() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets an icon for a Poll Key
|
|
||||||
*
|
|
||||||
* @param key yes, no, abstain or something like that
|
|
||||||
* @returns a string for material-icons to represent the icon for
|
|
||||||
* this key(e.g. yes: positive sign, no: negative sign)
|
|
||||||
*/
|
|
||||||
public getIcon(key: CalculablePollKey): string {
|
|
||||||
switch (key) {
|
|
||||||
case 'yes':
|
|
||||||
return 'thumb_up';
|
|
||||||
case 'no':
|
|
||||||
case 'votesno':
|
|
||||||
return 'thumb_down';
|
|
||||||
case 'abstain':
|
|
||||||
case 'votesabstain':
|
|
||||||
return 'not_interested';
|
|
||||||
// TODO case 'votescast':
|
|
||||||
// sum
|
|
||||||
case 'votesvalid':
|
|
||||||
return 'check';
|
|
||||||
case 'votesinvalid':
|
|
||||||
return 'cancel';
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a label for a poll Key
|
|
||||||
*
|
|
||||||
* @param key yes, no, abstain or something like that
|
|
||||||
* @returns A short descriptive name for the poll keys
|
|
||||||
*/
|
|
||||||
public getLabel(key: CalculablePollKey | PollVoteValue): string {
|
|
||||||
switch (key.toLowerCase()) {
|
|
||||||
case 'yes':
|
|
||||||
return 'Yes';
|
|
||||||
case 'no':
|
|
||||||
case 'votesno':
|
|
||||||
return 'No';
|
|
||||||
case 'abstain':
|
|
||||||
case 'votesabstain':
|
|
||||||
return 'Abstain';
|
|
||||||
case 'votescast':
|
|
||||||
return _('Total votes cast');
|
|
||||||
case 'votesvalid':
|
|
||||||
return _('Valid votes');
|
|
||||||
case 'votesinvalid':
|
|
||||||
return _('Invalid votes');
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* retrieve special labels for a poll value
|
|
||||||
* {@link specialPollVotes}. Positive values will return as string
|
|
||||||
* representation of themselves
|
|
||||||
*
|
|
||||||
* @param value check value for special numbers
|
|
||||||
* @returns the label for a non-positive value, according to
|
|
||||||
*/
|
|
||||||
public getSpecialLabel(value: number): string {
|
|
||||||
if (value >= 0) {
|
|
||||||
return value.toString();
|
|
||||||
// TODO: toLocaleString(lang); but translateService is not usable here, thus lang is not well defined
|
|
||||||
}
|
|
||||||
const vote = this.specialPollVotes.find(special => special[0] === value);
|
|
||||||
return vote ? vote[1] : 'Undocumented special (negative) value';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the progress bar class for a decision key
|
|
||||||
*
|
|
||||||
* @param key a calculable poll key (like yes or no)
|
|
||||||
* @returns a css class designing a progress bar in a color, or an empty string
|
|
||||||
*/
|
|
||||||
public getProgressBarColor(key: CalculablePollKey | PollVoteValue): string {
|
|
||||||
switch (key.toLowerCase()) {
|
|
||||||
case 'yes':
|
|
||||||
return 'progress-green';
|
|
||||||
case 'no':
|
|
||||||
return 'progress-red';
|
|
||||||
case 'abstain':
|
|
||||||
return 'progress-yellow';
|
|
||||||
case 'votes':
|
|
||||||
return 'progress-green';
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,18 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { VotingBannerService } from './voting-banner.service';
|
||||||
|
|
||||||
|
describe('VotingBannerService', () => {
|
||||||
|
beforeEach(() =>
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: VotingBannerService = TestBed.get(VotingBannerService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
65
client/src/app/core/ui-services/voting-banner.service.ts
Normal file
65
client/src/app/core/ui-services/voting-banner.service.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||||
|
import { PollListObservableService } from 'app/site/polls/services/poll-list-observable.service';
|
||||||
|
import { BannerDefinition, BannerService } from './banner.service';
|
||||||
|
import { OpenSlidesStatusService } from '../core-services/openslides-status.service';
|
||||||
|
import { VotingService } from './voting.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class VotingBannerService {
|
||||||
|
private currentBanner: BannerDefinition;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
pollListObservableService: PollListObservableService,
|
||||||
|
private banner: BannerService,
|
||||||
|
private translate: TranslateService,
|
||||||
|
private OSStatus: OpenSlidesStatusService,
|
||||||
|
private votingService: VotingService
|
||||||
|
) {
|
||||||
|
pollListObservableService.getViewModelListObservable().subscribe(polls => this.checkForVotablePolls(polls));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks all polls for votable ones and displays a banner for them
|
||||||
|
* @param polls the updated poll list
|
||||||
|
*/
|
||||||
|
private checkForVotablePolls(polls: ViewBasePoll[]): void {
|
||||||
|
// display no banner if in history mode
|
||||||
|
if (this.OSStatus.isInHistoryMode && this.currentBanner) {
|
||||||
|
this.banner.removeBanner(this.currentBanner);
|
||||||
|
this.currentBanner = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pollsToVote = polls.filter(poll => this.votingService.canVote(poll) && !poll.user_has_voted);
|
||||||
|
if (pollsToVote.length === 1) {
|
||||||
|
const poll = pollsToVote[0];
|
||||||
|
const banner = {
|
||||||
|
text: this.translate.instant('Click here to vote on the poll') + ` "${poll.title}"!`,
|
||||||
|
link: poll.parentLink,
|
||||||
|
bgColor: 'green'
|
||||||
|
};
|
||||||
|
this.banner.replaceBanner(this.currentBanner, banner);
|
||||||
|
this.currentBanner = banner;
|
||||||
|
} else if (pollsToVote.length > 1) {
|
||||||
|
const banner = {
|
||||||
|
text:
|
||||||
|
this.translate.instant('You have') +
|
||||||
|
` ${pollsToVote.length} ` +
|
||||||
|
this.translate.instant('polls to vote on!'),
|
||||||
|
link: '/polls/',
|
||||||
|
bgColor: 'green'
|
||||||
|
};
|
||||||
|
this.banner.replaceBanner(this.currentBanner, banner);
|
||||||
|
this.currentBanner = banner;
|
||||||
|
} else {
|
||||||
|
this.banner.removeBanner(this.currentBanner);
|
||||||
|
this.currentBanner = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
client/src/app/core/ui-services/voting.service.spec.ts
Normal file
18
client/src/app/core/ui-services/voting.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { VotingService } from './voting.service';
|
||||||
|
|
||||||
|
describe('VotingService', () => {
|
||||||
|
beforeEach(() =>
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: VotingService = TestBed.get(VotingService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
70
client/src/app/core/ui-services/voting.service.ts
Normal file
70
client/src/app/core/ui-services/voting.service.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { PollState, PollType } from 'app/shared/models/poll/base-poll';
|
||||||
|
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||||
|
import { OperatorService } from '../core-services/operator.service';
|
||||||
|
|
||||||
|
export enum VotingError {
|
||||||
|
POLL_WRONG_STATE = 1, // 1 so we can check with negation
|
||||||
|
POLL_WRONG_TYPE,
|
||||||
|
USER_HAS_NO_PERMISSION,
|
||||||
|
USER_IS_ANONYMOUS,
|
||||||
|
USER_NOT_PRESENT,
|
||||||
|
USER_HAS_VOTED
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VotingErrorVerbose = {
|
||||||
|
1: "You can't vote on this poll right now because it's not in the 'Started' state.",
|
||||||
|
2: "You can't vote on this poll because its type is set to analog voting.",
|
||||||
|
3: "You don't have permission to vote on this poll.",
|
||||||
|
4: 'You have to be logged in to be able to vote.',
|
||||||
|
5: 'You have to be present to vote on a poll.',
|
||||||
|
6: "You have already voted on this poll. You can't change your vote in a pseudoanonymous poll."
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class VotingService {
|
||||||
|
public constructor(private operator: OperatorService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks whether the operator can vote on the given poll
|
||||||
|
*/
|
||||||
|
public canVote(poll: ViewBasePoll): boolean {
|
||||||
|
return !this.getVotePermissionError(poll);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks whether the operator can vote on the given poll
|
||||||
|
* @returns null if no errors exist (= user can vote) or else a VotingError
|
||||||
|
*/
|
||||||
|
public getVotePermissionError(poll: ViewBasePoll): VotingError | void {
|
||||||
|
const user = this.operator.viewUser;
|
||||||
|
if (this.operator.isAnonymous) {
|
||||||
|
return VotingError.USER_IS_ANONYMOUS;
|
||||||
|
}
|
||||||
|
if (!poll.groups_id.intersect(user.groups_id).length) {
|
||||||
|
return VotingError.USER_HAS_NO_PERMISSION;
|
||||||
|
}
|
||||||
|
if (poll.type === PollType.Analog) {
|
||||||
|
return VotingError.POLL_WRONG_TYPE;
|
||||||
|
}
|
||||||
|
if (poll.state !== PollState.Started) {
|
||||||
|
return VotingError.POLL_WRONG_STATE;
|
||||||
|
}
|
||||||
|
if (!user.is_present) {
|
||||||
|
return VotingError.USER_NOT_PRESENT;
|
||||||
|
}
|
||||||
|
if (poll.type === PollType.Pseudoanonymous && poll.user_has_voted) {
|
||||||
|
return VotingError.USER_HAS_VOTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getVotePermissionErrorVerbose(poll: ViewBasePoll): string | void {
|
||||||
|
const error = this.getVotePermissionError(poll);
|
||||||
|
if (error) {
|
||||||
|
return VotingErrorVerbose[error];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -108,7 +108,7 @@ export class AttachmentControlComponent extends BaseFormControlComponent<ViewMed
|
|||||||
protected initializeForm(): void {
|
protected initializeForm(): void {
|
||||||
this.contentForm = this.fb.control([]);
|
this.contentForm = this.fb.control([]);
|
||||||
}
|
}
|
||||||
protected updateForm(value: ViewMediafile[]): void {
|
protected updateForm(value: ViewMediafile[] | null): void {
|
||||||
this.contentForm.setValue(value);
|
this.contentForm.setValue(value || []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
<div *ngFor="let banner of banners"
|
||||||
|
class="banner"
|
||||||
|
[ngClass]="(banner.type === 'history' ? 'history-mode-indicator' : '') + ' ' + (banner.class ? banner.class : '')"
|
||||||
|
[ngSwitch]="banner.type"
|
||||||
|
[style.background-color]="banner.bgColor"
|
||||||
|
[style.color]="banner.color"
|
||||||
|
>
|
||||||
|
<ng-container *ngSwitchCase="'history'">
|
||||||
|
<span translate>You are using the history mode of OpenSlides. Changes will not be saved.</span>
|
||||||
|
<span>({{ getHistoryTimestamp() }})</span>
|
||||||
|
<a (click)="timeTravel.resumeTime()" translate>Exit</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchDefault>
|
||||||
|
<a [routerLink]="banner.link" [style.cursor]="banner.link ? 'pointer' : 'default'">
|
||||||
|
<mat-icon>{{ banner.icon }}</mat-icon> <span>{{ banner.text }}</span>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
@ -0,0 +1,43 @@
|
|||||||
|
.banner {
|
||||||
|
position: relative; // was fixed before to prevent the overflow
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
background-color: blue;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
$font-size: 16px;
|
||||||
|
width: $font-size;
|
||||||
|
height: $font-size;
|
||||||
|
font-size: $font-size;
|
||||||
|
|
||||||
|
& + span {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-mode-indicator {
|
||||||
|
background: repeating-linear-gradient(45deg, #ffee00, #ffee00 10px, #070600 10px, #000000 20px);
|
||||||
|
|
||||||
|
span,
|
||||||
|
a {
|
||||||
|
padding: 2px;
|
||||||
|
color: #000000;
|
||||||
|
background: #ffee00;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { BannerComponent } from './banner.component';
|
||||||
|
|
||||||
|
describe('BannerComponent', () => {
|
||||||
|
let component: BannerComponent;
|
||||||
|
let fixture: ComponentFixture<BannerComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BannerComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
40
client/src/app/shared/components/banner/banner.component.ts
Normal file
40
client/src/app/shared/components/banner/banner.component.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { OpenSlidesStatusService } from 'app/core/core-services/openslides-status.service';
|
||||||
|
import { TimeTravelService } from 'app/core/core-services/time-travel.service';
|
||||||
|
import { BannerDefinition, BannerService } from 'app/core/ui-services/banner.service';
|
||||||
|
import { langToLocale } from 'app/shared/utils/lang-to-locale';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'os-banner',
|
||||||
|
templateUrl: './banner.component.html',
|
||||||
|
styleUrls: ['./banner.component.scss']
|
||||||
|
})
|
||||||
|
export class BannerComponent implements OnInit {
|
||||||
|
public banners: BannerDefinition[] = [];
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private OSStatus: OpenSlidesStatusService,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
public timeTravel: TimeTravelService,
|
||||||
|
private banner: BannerService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.banner.activeBanners.subscribe(banners => {
|
||||||
|
this.banners = banners;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the timestamp for the current point in history mode.
|
||||||
|
* Tries to detect the ideal timestamp format using the translation service
|
||||||
|
*
|
||||||
|
* @returns the timestamp as string
|
||||||
|
*/
|
||||||
|
public getHistoryTimestamp(): string {
|
||||||
|
return this.OSStatus.getHistoryTimeStamp(langToLocale(this.translate.currentLang));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
<ol class="breadcrumb-list">
|
||||||
|
<li *ngFor="let breadcrumb of breadcrumbList" class="breadcrumb" [ngClass]="{ active: breadcrumb.active }">
|
||||||
|
<ng-container *ngIf="breadcrumb.active">
|
||||||
|
<span>
|
||||||
|
{{ breadcrumb.label }}
|
||||||
|
</span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="!breadcrumb.active">
|
||||||
|
<span
|
||||||
|
(click)="breadcrumb.action ? breadcrumb.action() : null"
|
||||||
|
[ngClass]="{ 'accent-foreground has-action': breadcrumb.action }"
|
||||||
|
>
|
||||||
|
{{ breadcrumb.label }}
|
||||||
|
</span>
|
||||||
|
</ng-container>
|
||||||
|
</li>
|
||||||
|
</ol>
|
@ -0,0 +1,25 @@
|
|||||||
|
$breadcrumb-content: var(--breadcrumb-content);
|
||||||
|
|
||||||
|
.breadcrumb-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
& + & {
|
||||||
|
padding-left: 8px;
|
||||||
|
&::before {
|
||||||
|
padding-right: 8px;
|
||||||
|
content: $breadcrumb-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span.has-action {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { BreadcrumbComponent } from './breadcrumb.component';
|
||||||
|
|
||||||
|
describe('BreadcrumbComponent', () => {
|
||||||
|
let component: BreadcrumbComponent;
|
||||||
|
let fixture: ComponentFixture<BreadcrumbComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BreadcrumbComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,79 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes, how one breadcrumb can look like.
|
||||||
|
*/
|
||||||
|
export interface Breadcrumb {
|
||||||
|
label: string;
|
||||||
|
action: () => any;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'os-breadcrumb',
|
||||||
|
templateUrl: './breadcrumb.component.html',
|
||||||
|
styleUrls: ['./breadcrumb.component.scss']
|
||||||
|
})
|
||||||
|
export class BreadcrumbComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* A list of all breadcrumbs, that should be rendered.
|
||||||
|
*
|
||||||
|
* @param labels A list of strings or the interface `Breadcrumb`.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public set breadcrumbs(labels: string[] | Breadcrumb[]) {
|
||||||
|
this.breadcrumbList = [];
|
||||||
|
for (const breadcrumb of labels) {
|
||||||
|
if (typeof breadcrumb === 'string') {
|
||||||
|
this.breadcrumbList.push({ label: breadcrumb, action: null });
|
||||||
|
} else {
|
||||||
|
this.breadcrumbList.push(breadcrumb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current active index, if not the last one.
|
||||||
|
*
|
||||||
|
* @param index The index as number.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public set activeIndex(index: number) {
|
||||||
|
for (const breadcrumb of this.breadcrumbList) {
|
||||||
|
breadcrumb.active = false;
|
||||||
|
}
|
||||||
|
this.breadcrumbList[index].active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the separator for the breadcrumbs.
|
||||||
|
*
|
||||||
|
* @param style The new separator as string (character).
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public set breadcrumbStyle(style: string) {
|
||||||
|
document.documentElement.style.setProperty('--breadcrumb-content', `'${style}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of the breadcrumbs built by the input.
|
||||||
|
*/
|
||||||
|
public breadcrumbList: Breadcrumb[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default constructor.
|
||||||
|
*/
|
||||||
|
public constructor() {
|
||||||
|
this.breadcrumbStyle = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OnInit.
|
||||||
|
* Sets the last breadcrumb as the active breadcrumb if not defined before.
|
||||||
|
*/
|
||||||
|
public ngOnInit(): void {
|
||||||
|
if (this.breadcrumbList.length && !this.breadcrumbList.some(breadcrumb => breadcrumb.active)) {
|
||||||
|
this.breadcrumbList[this.breadcrumbList.length - 1].active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
<div class="charts-wrapper">
|
||||||
|
<canvas
|
||||||
|
*ngIf="type === 'bar' || type === 'line' || type === 'horizontalBar'"
|
||||||
|
baseChart
|
||||||
|
[datasets]="chartData"
|
||||||
|
[labels]="labels"
|
||||||
|
[legend]="showLegend"
|
||||||
|
[options]="chartOptions"
|
||||||
|
[chartType]="type"
|
||||||
|
(chartClick)="select.emit($event)"
|
||||||
|
(chartHover)="hover.emit($event)"
|
||||||
|
>
|
||||||
|
</canvas>
|
||||||
|
<canvas
|
||||||
|
*ngIf="type === 'pie' || type === 'doughnut'"
|
||||||
|
baseChart
|
||||||
|
[data]="circleData"
|
||||||
|
[labels]="circleLabels"
|
||||||
|
[colors]="circleColors"
|
||||||
|
[chartType]="type"
|
||||||
|
[legend]="showLegend"
|
||||||
|
></canvas>
|
||||||
|
</div>
|
@ -0,0 +1,4 @@
|
|||||||
|
.charts-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
import { async, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
describe('ChartsComponent', () => {
|
||||||
|
// let component: ChartsComponent;
|
||||||
|
// let fixture: ComponentFixture<ChartsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// fixture = TestBed.createComponent(ChartsComponent);
|
||||||
|
// component = fixture.componentInstance;
|
||||||
|
// fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
// expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
189
client/src/app/shared/components/charts/charts.component.ts
Normal file
189
client/src/app/shared/components/charts/charts.component.ts
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { MatSnackBar } from '@angular/material';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { ChartOptions } from 'chart.js';
|
||||||
|
import { Label } from 'ng2-charts';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The different supported chart-types.
|
||||||
|
*/
|
||||||
|
export type ChartType = 'line' | 'bar' | 'pie' | 'doughnut' | 'horizontalBar';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the events the chart is fired, when hovering or clicking on it.
|
||||||
|
*/
|
||||||
|
interface ChartEvent {
|
||||||
|
event: MouseEvent;
|
||||||
|
active: {}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One single collection in an arry.
|
||||||
|
*/
|
||||||
|
export interface ChartDate {
|
||||||
|
data: number[];
|
||||||
|
label: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
hoverBackgroundColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An alias for an array of `ChartDate`.
|
||||||
|
*/
|
||||||
|
export type ChartData = ChartDate[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for the chart-library.
|
||||||
|
*
|
||||||
|
* It takes the passed data to fit the different types of the library.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'os-charts',
|
||||||
|
templateUrl: './charts.component.html',
|
||||||
|
styleUrls: ['./charts.component.scss']
|
||||||
|
})
|
||||||
|
export class ChartsComponent extends BaseViewComponent {
|
||||||
|
/**
|
||||||
|
* Sets the data as an observable.
|
||||||
|
*
|
||||||
|
* The data is prepared and splitted to dynamic use of bar/line or doughnut/pie chart.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public set data(dataObservable: Observable<ChartData>) {
|
||||||
|
this.subscriptions.push(
|
||||||
|
dataObservable.subscribe(data => {
|
||||||
|
this.chartData = data;
|
||||||
|
this.circleData = data.flatMap((date: ChartDate) => date.data);
|
||||||
|
this.circleLabels = data.map(date => date.label);
|
||||||
|
this.circleColors = [
|
||||||
|
{
|
||||||
|
backgroundColor: data.map(date => date.backgroundColor),
|
||||||
|
hoverBackgroundColor: data.map(date => date.hoverBackgroundColor)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the chart. Defaults to `'bar'`.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public set type(type: ChartType) {
|
||||||
|
if (type === 'horizontalBar') {
|
||||||
|
this.setupHorizontalBar();
|
||||||
|
}
|
||||||
|
this._type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get type(): ChartType {
|
||||||
|
return this._type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to show the legend.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public showLegend = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The labels for the separated sections.
|
||||||
|
* Each label represent one section, e.g. one year.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public labels: Label[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the position of the legend.
|
||||||
|
* Defaults to `'top'`.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public set legendPosition(position: Chart.PositionType) {
|
||||||
|
this.chartOptions.legend.position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires an event, when the user clicks on the chart.
|
||||||
|
*/
|
||||||
|
@Output()
|
||||||
|
public select = new EventEmitter<ChartEvent>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires an event, when the user hovers over the chart.
|
||||||
|
*/
|
||||||
|
@Output()
|
||||||
|
public hover = new EventEmitter<ChartEvent>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The general data for the chart.
|
||||||
|
* This is only needed for `type == 'bar' || 'line'`
|
||||||
|
*/
|
||||||
|
public chartData: ChartData = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data for circle-like charts, like 'doughnut' or 'pie'.
|
||||||
|
*/
|
||||||
|
public circleData: number[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The labels for circle-like charts, like 'doughnut' or 'pie'.
|
||||||
|
*/
|
||||||
|
public circleLabels: Label[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The colors for circle-like charts, like 'doughnut' or 'pie'.
|
||||||
|
*/
|
||||||
|
public circleColors: { backgroundColor?: string[]; hoverBackgroundColor?: string[] }[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The options used for the charts.
|
||||||
|
*/
|
||||||
|
public chartOptions: ChartOptions = {
|
||||||
|
responsive: true,
|
||||||
|
legend: {
|
||||||
|
position: 'top',
|
||||||
|
labels: {
|
||||||
|
fontSize: 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: { xAxes: [{}], yAxes: [{ ticks: { beginAtZero: true } }] },
|
||||||
|
plugins: {
|
||||||
|
datalabels: {
|
||||||
|
anchor: 'end',
|
||||||
|
align: 'end'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the type of the chart - defaults to `bar`.
|
||||||
|
*/
|
||||||
|
private _type: ChartType = 'bar';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param title
|
||||||
|
* @param translate
|
||||||
|
* @param matSnackbar
|
||||||
|
* @param cd
|
||||||
|
*/
|
||||||
|
public constructor(title: Title, protected translate: TranslateService, matSnackbar: MatSnackBar) {
|
||||||
|
super(title, translate, matSnackbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the chart-options, if the `horizontalBar` is used.
|
||||||
|
*/
|
||||||
|
private setupHorizontalBar(): void {
|
||||||
|
this.chartOptions.scales = Object.assign(this.chartOptions.scales, {
|
||||||
|
xAxes: [{ stacked: true }],
|
||||||
|
yAxes: [{ stacked: true }]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
<div class="check-input--container">
|
||||||
|
<mat-form-field>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
class="check-input--input"
|
||||||
|
[type]="inputType"
|
||||||
|
[formControl]="contentForm"
|
||||||
|
[placeholder]="placeholder"
|
||||||
|
[min]="0"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-checkbox
|
||||||
|
*ngIf="checkboxLabel"
|
||||||
|
[name]="'checkbox'"
|
||||||
|
[ngModel]="isChecked"
|
||||||
|
(change)="checkboxStateChanged($event.checked)"
|
||||||
|
>
|
||||||
|
{{ checkboxLabel }}
|
||||||
|
</mat-checkbox>
|
||||||
|
<div *ngIf="!checkboxLabel" class="placeholder"></div>
|
||||||
|
</div>
|
@ -0,0 +1,10 @@
|
|||||||
|
.check-input--container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { CheckInputComponent } from './check-input.component';
|
||||||
|
|
||||||
|
describe('CheckInputComponent', () => {
|
||||||
|
let component: CheckInputComponent;
|
||||||
|
let fixture: ComponentFixture<CheckInputComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CheckInputComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,147 @@
|
|||||||
|
import { Component, forwardRef, Input, OnInit } from '@angular/core';
|
||||||
|
import { ControlValueAccessor, FormBuilder, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
import { MatSnackBar } from '@angular/material';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'os-check-input',
|
||||||
|
templateUrl: './check-input.component.html',
|
||||||
|
styleUrls: ['./check-input.component.scss'],
|
||||||
|
providers: [{ provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => CheckInputComponent) }]
|
||||||
|
})
|
||||||
|
export class CheckInputComponent extends BaseViewComponent implements OnInit, ControlValueAccessor {
|
||||||
|
/**
|
||||||
|
* Type of the used input.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public inputType = 'text';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The placeholder for the form-field.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public placeholder: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value received, if the checkbox is checked.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public checkboxValue: number | string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label for the checkbox.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public checkboxLabel: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model for the state of the checkbox.
|
||||||
|
*/
|
||||||
|
public isChecked = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The form-control-reference.
|
||||||
|
*/
|
||||||
|
public contentForm: FormControl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default constructor.
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
title: Title,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
matSnackbar: MatSnackBar,
|
||||||
|
private fb: FormBuilder
|
||||||
|
) {
|
||||||
|
super(title, translate, matSnackbar);
|
||||||
|
this.initForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OnInit.
|
||||||
|
* Subscribes to value-changes of the form-control.
|
||||||
|
*/
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.subscriptions.push(this.contentForm.valueChanges.subscribe(value => this.sendValue(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to handle checkbox-state-changed-event.
|
||||||
|
*/
|
||||||
|
public checkboxStateChanged(checked: boolean): void {
|
||||||
|
this.isChecked = checked;
|
||||||
|
if (checked) {
|
||||||
|
this.contentForm.disable({ emitEvent: false });
|
||||||
|
} else {
|
||||||
|
this.contentForm.enable({ emitEvent: false });
|
||||||
|
}
|
||||||
|
this.sendValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value from the FormControl
|
||||||
|
*
|
||||||
|
* @param obj the value from the parent form. Type "any" is required by the interface
|
||||||
|
*/
|
||||||
|
public writeValue(obj: string | number): void {
|
||||||
|
if (obj) {
|
||||||
|
if (obj === this.checkboxValue) {
|
||||||
|
this.checkboxStateChanged(true);
|
||||||
|
} else {
|
||||||
|
this.contentForm.patchValue(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hands changes back to the parent form
|
||||||
|
*
|
||||||
|
* @param fn the function to propagate the changes
|
||||||
|
*/
|
||||||
|
public registerOnChange(fn: any): void {
|
||||||
|
this.propagateChange = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To satisfy the interface.
|
||||||
|
*
|
||||||
|
* @param fn
|
||||||
|
*/
|
||||||
|
public registerOnTouched(fn: any): void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To satisfy the interface
|
||||||
|
*
|
||||||
|
* @param isDisabled
|
||||||
|
*/
|
||||||
|
public setDisabledState?(isDisabled: boolean): void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to determine which information to give to the parent form
|
||||||
|
*/
|
||||||
|
private propagateChange = (_: any) => {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initially build the form-control.
|
||||||
|
*/
|
||||||
|
private initForm(): void {
|
||||||
|
this.contentForm = this.fb.control('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the given value by the propagateChange-funtion.
|
||||||
|
*
|
||||||
|
* @param value Optional parameter to pass a value to send.
|
||||||
|
*/
|
||||||
|
private sendValue(value?: string | number): void {
|
||||||
|
if (this.isChecked) {
|
||||||
|
this.propagateChange(this.checkboxValue);
|
||||||
|
} else {
|
||||||
|
this.propagateChange(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -41,7 +41,6 @@
|
|||||||
<mat-form-field *ngIf="searchList">
|
<mat-form-field *ngIf="searchList">
|
||||||
<os-search-value-selector
|
<os-search-value-selector
|
||||||
formControlName="list"
|
formControlName="list"
|
||||||
[fullWidth]="true"
|
|
||||||
[inputListValues]="searchList"
|
[inputListValues]="searchList"
|
||||||
[placeholder]="searchListLabel"
|
[placeholder]="searchListLabel"
|
||||||
></os-search-value-selector>
|
></os-search-value-selector>
|
||||||
|
@ -20,7 +20,7 @@ import { distinctUntilChanged, filter } from 'rxjs/operators';
|
|||||||
|
|
||||||
import { OperatorService, Permission } from 'app/core/core-services/operator.service';
|
import { OperatorService, Permission } from 'app/core/core-services/operator.service';
|
||||||
import { StorageService } from 'app/core/core-services/storage.service';
|
import { StorageService } from 'app/core/core-services/storage.service';
|
||||||
import { BaseRepository } from 'app/core/repositories/base-repository';
|
import { HasViewModelListObservable } from 'app/core/definitions/has-view-model-list-observable';
|
||||||
import { BaseFilterListService } from 'app/core/ui-services/base-filter-list.service';
|
import { BaseFilterListService } from 'app/core/ui-services/base-filter-list.service';
|
||||||
import { BaseSortListService } from 'app/core/ui-services/base-sort-list.service';
|
import { BaseSortListService } from 'app/core/ui-services/base-sort-list.service';
|
||||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||||
@ -63,7 +63,7 @@ export interface ColumnRestriction {
|
|||||||
* @example
|
* @example
|
||||||
* ```html
|
* ```html
|
||||||
* <os-list-view-table
|
* <os-list-view-table
|
||||||
* [repo]="motionRepo"
|
* [listObservableProvider]="motionRepo"
|
||||||
* [filterService]="filterService"
|
* [filterService]="filterService"
|
||||||
* [sortService]="sortService"
|
* [sortService]="sortService"
|
||||||
* [columns]="motionColumnDefinition"
|
* [columns]="motionColumnDefinition"
|
||||||
@ -99,7 +99,7 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
|
|||||||
* The required repository
|
* The required repository
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
public repo: BaseRepository<V, M, any>;
|
public listObservableProvider: HasViewModelListObservable<V>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The currently active sorting service for the list view
|
* The currently active sorting service for the list view
|
||||||
@ -109,7 +109,7 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The currently active filter service for the list view. It is supposed to
|
* The currently active filter service for the list view. It is supposed to
|
||||||
* be a FilterListService extendingFilterListService.
|
* be a FilterListService extending FilterListService.
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
public filterService: BaseFilterListService<V>;
|
public filterService: BaseFilterListService<V>;
|
||||||
@ -322,15 +322,6 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
|
|||||||
return this.dataSource.length;
|
return this.dataSource.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns the repositories `viewModelListObservable`
|
|
||||||
*/
|
|
||||||
private get viewModelListObservable(): Observable<V[]> {
|
|
||||||
if (this.repo) {
|
|
||||||
return this.repo.getViewModelListObservable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define which columns to hide. Uses the input-property
|
* Define which columns to hide. Uses the input-property
|
||||||
* "hide" to hide individual columns
|
* "hide" to hide individual columns
|
||||||
@ -477,23 +468,24 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
|
|||||||
* to the used search and filter services
|
* to the used search and filter services
|
||||||
*/
|
*/
|
||||||
private getListObservable(): void {
|
private getListObservable(): void {
|
||||||
if (this.repo && this.viewModelListObservable) {
|
if (this.listObservableProvider) {
|
||||||
|
const listObservable = this.listObservableProvider.getViewModelListObservable();
|
||||||
if (this.filterService && this.sortService) {
|
if (this.filterService && this.sortService) {
|
||||||
// filtering and sorting
|
// filtering and sorting
|
||||||
this.filterService.initFilters(this.viewModelListObservable);
|
this.filterService.initFilters(listObservable);
|
||||||
this.sortService.initSorting(this.filterService.outputObservable);
|
this.sortService.initSorting(this.filterService.outputObservable);
|
||||||
this.dataListObservable = this.sortService.outputObservable;
|
this.dataListObservable = this.sortService.outputObservable;
|
||||||
} else if (this.filterService) {
|
} else if (this.filterService) {
|
||||||
// only filter service
|
// only filter service
|
||||||
this.filterService.initFilters(this.viewModelListObservable);
|
this.filterService.initFilters(listObservable);
|
||||||
this.dataListObservable = this.filterService.outputObservable;
|
this.dataListObservable = this.filterService.outputObservable;
|
||||||
} else if (this.sortService) {
|
} else if (this.sortService) {
|
||||||
// only sorting
|
// only sorting
|
||||||
this.sortService.initSorting(this.viewModelListObservable);
|
this.sortService.initSorting(listObservable);
|
||||||
this.dataListObservable = this.sortService.outputObservable;
|
this.dataListObservable = this.sortService.outputObservable;
|
||||||
} else {
|
} else {
|
||||||
// none of both
|
// none of both
|
||||||
this.dataListObservable = this.viewModelListObservable;
|
this.dataListObservable = listObservable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,23 @@
|
|||||||
<mat-select [formControl]="contentForm" [multiple]="multiple">
|
<mat-select [formControl]="contentForm" [multiple]="multiple" [panelClass]="{ 'os-search-value-selector': multiple }">
|
||||||
<ngx-mat-select-search [formControl]="searchValue"></ngx-mat-select-search>
|
<ngx-mat-select-search [formControl]="searchValue"></ngx-mat-select-search>
|
||||||
|
<ng-container *ngIf="multiple && showChips">
|
||||||
|
<div #chipPlaceholder>
|
||||||
|
<div class="os-search-value-selector-chip-container" [style.width]="width">
|
||||||
|
<mat-chip-list class="chip-list" [selectable]="false">
|
||||||
|
<mat-chip
|
||||||
|
*ngFor="let item of selectedItems"
|
||||||
|
[removable]="true"
|
||||||
|
(removed)="removeItem(item.id)"
|
||||||
|
[disableRipple]="true"
|
||||||
|
>{{ item.getTitle() }} <mat-icon matChipRemove>cancel</mat-icon></mat-chip
|
||||||
|
>
|
||||||
|
</mat-chip-list>
|
||||||
|
</div>
|
||||||
|
<div class="os-search-value-selector-chip-placeholder"></div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
<ng-container *ngIf="!multiple && includeNone">
|
<ng-container *ngIf="!multiple && includeNone">
|
||||||
<mat-option [value]="null">
|
<mat-option>
|
||||||
{{ noneTitle | translate }}
|
{{ noneTitle | translate }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
.os-search-value-selector {
|
||||||
|
max-height: 312px !important ;
|
||||||
|
}
|
||||||
|
|
||||||
|
.os-search-value-selector-chip-container {
|
||||||
|
position: absolute;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||||
|
top: 52px;
|
||||||
|
width: 100%;
|
||||||
|
background: white;
|
||||||
|
z-index: 100;
|
||||||
|
min-height: 41px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.os-search-value-selector-chip-placeholder {
|
||||||
|
padding: 8px;
|
||||||
|
width: 100%;
|
||||||
|
background: white;
|
||||||
|
min-height: 39px;
|
||||||
|
}
|
@ -1,12 +1,13 @@
|
|||||||
import { FocusMonitor } from '@angular/cdk/a11y';
|
import { FocusMonitor } from '@angular/cdk/a11y';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
Input,
|
Input,
|
||||||
Optional,
|
Optional,
|
||||||
Self
|
Self,
|
||||||
|
ViewChild,
|
||||||
|
ViewEncapsulation
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormBuilder, FormControl, NgControl } from '@angular/forms';
|
import { FormBuilder, FormControl, NgControl } from '@angular/forms';
|
||||||
import { MatFormFieldControl } from '@angular/material';
|
import { MatFormFieldControl } from '@angular/material';
|
||||||
@ -43,9 +44,13 @@ import { Selectable } from '../selectable';
|
|||||||
templateUrl: './search-value-selector.component.html',
|
templateUrl: './search-value-selector.component.html',
|
||||||
styleUrls: ['./search-value-selector.component.scss'],
|
styleUrls: ['./search-value-selector.component.scss'],
|
||||||
providers: [{ provide: MatFormFieldControl, useExisting: SearchValueSelectorComponent }],
|
providers: [{ provide: MatFormFieldControl, useExisting: SearchValueSelectorComponent }],
|
||||||
|
encapsulation: ViewEncapsulation.None,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class SearchValueSelectorComponent extends BaseFormControlComponent<Selectable[]> {
|
export class SearchValueSelectorComponent extends BaseFormControlComponent<Selectable[]> {
|
||||||
|
@ViewChild('chipPlaceholder', { static: false })
|
||||||
|
public chipPlaceholder: ElementRef<HTMLElement>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decide if this should be a single or multi-select-field
|
* Decide if this should be a single or multi-select-field
|
||||||
*/
|
*/
|
||||||
@ -59,13 +64,10 @@ export class SearchValueSelectorComponent extends BaseFormControlComponent<Selec
|
|||||||
public includeNone = false;
|
public includeNone = false;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
public noneTitle = '–';
|
public showChips = true;
|
||||||
|
|
||||||
/**
|
|
||||||
* Boolean, whether the component should be rendered with full width.
|
|
||||||
*/
|
|
||||||
@Input()
|
@Input()
|
||||||
public fullWidth = false;
|
public noneTitle = '–';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The inputlist subject. Subscribes to it and updates the selector, if the subject
|
* The inputlist subject. Subscribes to it and updates the selector, if the subject
|
||||||
@ -92,8 +94,18 @@ export class SearchValueSelectorComponent extends BaseFormControlComponent<Selec
|
|||||||
return Array.isArray(this.contentForm.value) ? !this.contentForm.value.length : !this.contentForm.value;
|
return Array.isArray(this.contentForm.value) ? !this.contentForm.value.length : !this.contentForm.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get selectedItems(): Selectable[] {
|
||||||
|
return this.selectableItems && this.contentForm.value
|
||||||
|
? this.selectableItems.filter(item => this.contentForm.value.includes(item.id))
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
public controlType = 'search-value-selector';
|
public controlType = 'search-value-selector';
|
||||||
|
|
||||||
|
public get width(): string {
|
||||||
|
return this.chipPlaceholder ? `${this.chipPlaceholder.nativeElement.clientWidth - 16}px` : '100%';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All items
|
* All items
|
||||||
*/
|
*/
|
||||||
@ -104,7 +116,6 @@ export class SearchValueSelectorComponent extends BaseFormControlComponent<Selec
|
|||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
cd: ChangeDetectorRef,
|
|
||||||
fb: FormBuilder,
|
fb: FormBuilder,
|
||||||
@Optional() @Self() public ngControl: NgControl,
|
@Optional() @Self() public ngControl: NgControl,
|
||||||
fm: FocusMonitor,
|
fm: FocusMonitor,
|
||||||
@ -143,6 +154,15 @@ export class SearchValueSelectorComponent extends BaseFormControlComponent<Selec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removeItem(itemId: number): void {
|
||||||
|
const items = <number[]>this.contentForm.value;
|
||||||
|
items.splice(
|
||||||
|
items.findIndex(item => item === itemId),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
this.contentForm.setValue(items);
|
||||||
|
}
|
||||||
|
|
||||||
public onContainerClick(event: MouseEvent): void {
|
public onContainerClick(event: MouseEvent): void {
|
||||||
if ((event.target as Element).tagName.toLowerCase() !== 'select') {
|
if ((event.target as Element).tagName.toLowerCase() !== 'select') {
|
||||||
// this.element.nativeElement.querySelector('select').focus();
|
// this.element.nativeElement.querySelector('select').focus();
|
||||||
@ -155,7 +175,6 @@ export class SearchValueSelectorComponent extends BaseFormControlComponent<Selec
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected updateForm(value: Selectable[] | null): void {
|
protected updateForm(value: Selectable[] | null): void {
|
||||||
const nextValue = value;
|
this.contentForm.setValue(value);
|
||||||
this.contentForm.setValue(nextValue);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,10 @@
|
|||||||
import { AssignmentOption } from './assignment-option';
|
import { AssignmentOption } from './assignment-option';
|
||||||
import { BasePoll, BasePollWithoutNestedModels } from '../poll/base-poll';
|
import { BasePoll } from '../poll/base-poll';
|
||||||
|
|
||||||
export enum AssignmentPollmethods {
|
export enum AssignmentPollMethods {
|
||||||
'yn' = 'yn',
|
YN = 'YN',
|
||||||
'yna' = 'yna',
|
YNA = 'YNA',
|
||||||
'votes' = 'votes'
|
Votes = 'votes'
|
||||||
}
|
|
||||||
|
|
||||||
export interface AssignmentPollWithoutNestedModels extends BasePollWithoutNestedModels {
|
|
||||||
pollmethod: AssignmentPollmethods;
|
|
||||||
votes_amount: number;
|
|
||||||
allow_multiple_votes_per_candidate: boolean;
|
|
||||||
global_no: boolean;
|
|
||||||
global_abstain: boolean;
|
|
||||||
assignment_id: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,9 +15,15 @@ export class AssignmentPoll extends BasePoll<AssignmentPoll, AssignmentOption> {
|
|||||||
public static COLLECTIONSTRING = 'assignments/assignment-poll';
|
public static COLLECTIONSTRING = 'assignments/assignment-poll';
|
||||||
|
|
||||||
public id: number;
|
public id: number;
|
||||||
|
public assignment_id: number;
|
||||||
|
public pollmethod: AssignmentPollMethods;
|
||||||
|
public votes_amount: number;
|
||||||
|
public allow_multiple_votes_per_candidate: boolean;
|
||||||
|
public global_no: boolean;
|
||||||
|
public global_abstain: boolean;
|
||||||
|
public description: string;
|
||||||
|
|
||||||
public constructor(input?: any) {
|
public constructor(input?: any) {
|
||||||
super(AssignmentPoll.COLLECTIONSTRING, input);
|
super(AssignmentPoll.COLLECTIONSTRING, input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export interface AssignmentPoll extends AssignmentPollWithoutNestedModels {}
|
|
||||||
|
@ -109,7 +109,7 @@ export abstract class BaseFormControlComponent<T> extends MatFormFieldControl<T>
|
|||||||
|
|
||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
fm.monitor(element.nativeElement, true).subscribe(origin => {
|
fm.monitor(element.nativeElement, true).subscribe(origin => {
|
||||||
this.focused = !!origin;
|
this.focused = origin === 'mouse' || origin === 'touch';
|
||||||
this.stateChanges.next();
|
this.stateChanges.next();
|
||||||
}),
|
}),
|
||||||
this.contentForm.valueChanges.subscribe(nextValue => this.push(nextValue))
|
this.contentForm.valueChanges.subscribe(nextValue => this.push(nextValue))
|
||||||
|
@ -1,19 +1,10 @@
|
|||||||
import { BasePoll, BasePollWithoutNestedModels } from '../poll/base-poll';
|
import { BasePoll } from '../poll/base-poll';
|
||||||
import { MotionOption } from './motion-option';
|
import { MotionOption } from './motion-option';
|
||||||
|
|
||||||
export enum MotionPollMethods {
|
export enum MotionPollMethods {
|
||||||
YN = 'YN',
|
YN = 'YN',
|
||||||
YNA = 'YNA'
|
YNA = 'YNA'
|
||||||
}
|
}
|
||||||
export const MotionPollMethodsVerbose = {
|
|
||||||
YN: 'Yes/No',
|
|
||||||
YNA: 'Yes/No/Abstain'
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface MotionPollWithoutNestedModels extends BasePollWithoutNestedModels {
|
|
||||||
motion_id: number;
|
|
||||||
pollmethod: MotionPollMethods;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class representing a poll for a motion.
|
* Class representing a poll for a motion.
|
||||||
@ -22,13 +13,10 @@ export class MotionPoll extends BasePoll<MotionPoll, MotionOption> {
|
|||||||
public static COLLECTIONSTRING = 'motions/motion-poll';
|
public static COLLECTIONSTRING = 'motions/motion-poll';
|
||||||
|
|
||||||
public id: number;
|
public id: number;
|
||||||
|
public motion_id: number;
|
||||||
|
public pollmethod: MotionPollMethods;
|
||||||
|
|
||||||
public constructor(input?: any) {
|
public constructor(input?: any) {
|
||||||
super(MotionPoll.COLLECTIONSTRING, input);
|
super(MotionPoll.COLLECTIONSTRING, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get pollmethodVerbose(): string {
|
|
||||||
return MotionPollMethodsVerbose[this.pollmethod];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export interface MotionPoll extends MotionPollWithoutNestedModels {}
|
|
||||||
|
@ -5,6 +5,7 @@ export abstract class BaseOption<T> extends BaseDecimalModel<T> {
|
|||||||
public yes: number;
|
public yes: number;
|
||||||
public no: number;
|
public no: number;
|
||||||
public abstain: number;
|
public abstain: number;
|
||||||
|
public poll_id: number;
|
||||||
|
|
||||||
protected getDecimalFields(): (keyof BaseOption<T>)[] {
|
protected getDecimalFields(): (keyof BaseOption<T>)[] {
|
||||||
return ['yes', 'no', 'abstain'];
|
return ['yes', 'no', 'abstain'];
|
||||||
|
@ -1,6 +1,15 @@
|
|||||||
import { BaseDecimalModel } from '../base/base-decimal-model';
|
import { BaseDecimalModel } from '../base/base-decimal-model';
|
||||||
import { BaseOption } from './base-option';
|
import { BaseOption } from './base-option';
|
||||||
|
|
||||||
|
export enum PollColor {
|
||||||
|
yes = '#9fd773',
|
||||||
|
no = '#cc6c5b',
|
||||||
|
abstain = '#a6a6a6',
|
||||||
|
votesvalid = '#e2e2e2',
|
||||||
|
votesinvalid = '#e2e2e2',
|
||||||
|
votescast = '#e2e2e2'
|
||||||
|
}
|
||||||
|
|
||||||
export enum PollState {
|
export enum PollState {
|
||||||
Created = 1,
|
Created = 1,
|
||||||
Started,
|
Started,
|
||||||
@ -8,41 +17,21 @@ export enum PollState {
|
|||||||
Published
|
Published
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PollStateVerbose = {
|
|
||||||
1: 'Created',
|
|
||||||
2: 'Started',
|
|
||||||
3: 'Finished',
|
|
||||||
4: 'Published'
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum PollType {
|
export enum PollType {
|
||||||
Analog = 'analog',
|
Analog = 'analog',
|
||||||
Named = 'named',
|
Named = 'named',
|
||||||
Pseudoanonymous = 'pseudoanonymous'
|
Pseudoanonymous = 'pseudoanonymous'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PollTypeVerbose = {
|
|
||||||
analog: 'Analog',
|
|
||||||
named: 'Named',
|
|
||||||
pseudoanonymous: 'Pseudoanonymous'
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum PercentBase {
|
export enum PercentBase {
|
||||||
YN = 'YN',
|
YN = 'YN',
|
||||||
YNA = 'YNA',
|
YNA = 'YNA',
|
||||||
Valid = 'valid',
|
Valid = 'valid',
|
||||||
|
Votes = 'votes',
|
||||||
Cast = 'cast',
|
Cast = 'cast',
|
||||||
Disabled = 'disabled'
|
Disabled = 'disabled'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PercentBaseVerbose = {
|
|
||||||
YN: 'Yes/No',
|
|
||||||
YNA: 'Yes/No/Abstain',
|
|
||||||
valid: 'Valid votes',
|
|
||||||
cast: 'Casted votes',
|
|
||||||
disabled: 'Disabled'
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum MajorityMethod {
|
export enum MajorityMethod {
|
||||||
Simple = 'simple',
|
Simple = 'simple',
|
||||||
TwoThirds = 'two_thirds',
|
TwoThirds = 'two_thirds',
|
||||||
@ -50,47 +39,20 @@ export enum MajorityMethod {
|
|||||||
Disabled = 'disabled'
|
Disabled = 'disabled'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MajorityMethodVerbose = {
|
export abstract class BasePoll<T = any, O extends BaseOption<any> = any> extends BaseDecimalModel<T> {
|
||||||
simple: 'Simple',
|
public state: PollState;
|
||||||
two_thirds: 'Two Thirds',
|
public type: PollType;
|
||||||
three_quarters: 'Three Quarters',
|
public title: string;
|
||||||
disabled: 'Disabled'
|
public votesvalid: number;
|
||||||
};
|
public votesinvalid: number;
|
||||||
|
public votescast: number;
|
||||||
export interface BasePollWithoutNestedModels {
|
public groups_id: number[];
|
||||||
state: PollState;
|
public voted_id: number[];
|
||||||
type: PollType;
|
public majority_method: MajorityMethod;
|
||||||
title: string;
|
public onehundred_percent_base: PercentBase;
|
||||||
votesvalid: number;
|
public user_has_voted: boolean;
|
||||||
votesinvalid: number;
|
|
||||||
votescast: number;
|
|
||||||
groups_id: number[];
|
|
||||||
voted_id: number[];
|
|
||||||
majority_method: MajorityMethod;
|
|
||||||
onehundred_percent_base: PercentBase;
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class BasePoll<T, O extends BaseOption<any>> extends BaseDecimalModel<T> {
|
|
||||||
public options: O[];
|
|
||||||
|
|
||||||
protected getDecimalFields(): (keyof BasePoll<T, O>)[] {
|
protected getDecimalFields(): (keyof BasePoll<T, O>)[] {
|
||||||
return ['votesvalid', 'votesinvalid', 'votescast'];
|
return ['votesvalid', 'votesinvalid', 'votescast'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public get stateVerbose(): string {
|
|
||||||
return PollStateVerbose[this.state];
|
|
||||||
}
|
|
||||||
|
|
||||||
public get typeVerbose(): string {
|
|
||||||
return PollTypeVerbose[this.type];
|
|
||||||
}
|
|
||||||
|
|
||||||
public get majorityMethodVerbose(): string {
|
|
||||||
return MajorityMethodVerbose[this.majority_method];
|
|
||||||
}
|
|
||||||
|
|
||||||
public get percentBaseVerbose(): string {
|
|
||||||
return PercentBaseVerbose[this.onehundred_percent_base];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export interface BasePoll<T, O extends BaseOption<any>> extends BasePollWithoutNestedModels {}
|
|
||||||
|
@ -1,11 +1,31 @@
|
|||||||
import { BaseDecimalModel } from '../base/base-decimal-model';
|
import { BaseDecimalModel } from '../base/base-decimal-model';
|
||||||
|
|
||||||
|
export type VoteValue = 'Y' | 'N' | 'A';
|
||||||
|
|
||||||
|
export const VoteValueVerbose = {
|
||||||
|
Y: 'Yes',
|
||||||
|
N: 'No',
|
||||||
|
A: 'Abstain'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GeneralValueVerbose = {
|
||||||
|
votesvalid: 'Votes valid',
|
||||||
|
votesinvalid: 'Votes invalid',
|
||||||
|
votescast: 'Votes cast',
|
||||||
|
votesno: 'Votes No',
|
||||||
|
votesabstain: 'Votes abstain'
|
||||||
|
};
|
||||||
|
|
||||||
export abstract class BaseVote<T> extends BaseDecimalModel<T> {
|
export abstract class BaseVote<T> extends BaseDecimalModel<T> {
|
||||||
public weight: number;
|
public weight: number;
|
||||||
public value: 'Y' | 'N' | 'A';
|
public value: VoteValue;
|
||||||
public option_id: number;
|
public option_id: number;
|
||||||
public user_id?: number;
|
public user_id?: number;
|
||||||
|
|
||||||
|
public get valueVerbose(): string {
|
||||||
|
return VoteValueVerbose[this.value];
|
||||||
|
}
|
||||||
|
|
||||||
protected getDecimalFields(): (keyof BaseVote<T>)[] {
|
protected getDecimalFields(): (keyof BaseVote<T>)[] {
|
||||||
return ['weight'];
|
return ['weight'];
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
import { MatSliderModule } from '@angular/material/slider';
|
import { MatSliderModule } from '@angular/material/slider';
|
||||||
|
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
import { MatSortModule } from '@angular/material/sort';
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
import { MatStepperModule } from '@angular/material/stepper';
|
import { MatStepperModule } from '@angular/material/stepper';
|
||||||
@ -64,6 +65,7 @@ import { PblNgridTargetEventsModule } from '@pebula/ngrid/target-events';
|
|||||||
|
|
||||||
// time picker because angular still doesnt offer one!!
|
// time picker because angular still doesnt offer one!!
|
||||||
import { NgxMaterialTimepickerModule } from 'ngx-material-timepicker';
|
import { NgxMaterialTimepickerModule } from 'ngx-material-timepicker';
|
||||||
|
import { ChartsModule } from 'ng2-charts';
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { HeadBarComponent } from './components/head-bar/head-bar.component';
|
import { HeadBarComponent } from './components/head-bar/head-bar.component';
|
||||||
@ -109,6 +111,14 @@ import { GlobalSpinnerComponent } from 'app/site/common/components/global-spinne
|
|||||||
import { HeightResizingDirective } from './directives/height-resizing.directive';
|
import { HeightResizingDirective } from './directives/height-resizing.directive';
|
||||||
import { TrustPipe } from './pipes/trust.pipe';
|
import { TrustPipe } from './pipes/trust.pipe';
|
||||||
import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
||||||
|
import { BreadcrumbComponent } from './components/breadcrumb/breadcrumb.component';
|
||||||
|
import { ChartsComponent } from './components/charts/charts.component';
|
||||||
|
import { CheckInputComponent } from './components/check-input/check-input.component';
|
||||||
|
import { BannerComponent } from './components/banner/banner.component';
|
||||||
|
import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.component';
|
||||||
|
import { PollFormComponent } from 'app/site/polls/components/poll-form/poll-form.component';
|
||||||
|
import { MotionPollDialogComponent } from 'app/site/motions/modules/motion-poll/motion-poll-dialog/motion-poll-dialog.component';
|
||||||
|
import { AssignmentPollDialogComponent } from 'app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Share Module for all "dumb" components and pipes.
|
* Share Module for all "dumb" components and pipes.
|
||||||
@ -158,6 +168,7 @@ import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
|||||||
MatStepperModule,
|
MatStepperModule,
|
||||||
MatTabsModule,
|
MatTabsModule,
|
||||||
MatSliderModule,
|
MatSliderModule,
|
||||||
|
MatSlideToggleModule,
|
||||||
MatDividerModule,
|
MatDividerModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
OpenSlidesTranslateModule.forChild(),
|
OpenSlidesTranslateModule.forChild(),
|
||||||
@ -171,7 +182,8 @@ import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
|||||||
PblNgridMaterialModule,
|
PblNgridMaterialModule,
|
||||||
PblNgridTargetEventsModule,
|
PblNgridTargetEventsModule,
|
||||||
PdfViewerModule,
|
PdfViewerModule,
|
||||||
NgxMaterialTimepickerModule
|
NgxMaterialTimepickerModule,
|
||||||
|
ChartsModule
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@ -205,6 +217,7 @@ import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
|||||||
MatButtonToggleModule,
|
MatButtonToggleModule,
|
||||||
MatStepperModule,
|
MatStepperModule,
|
||||||
MatSliderModule,
|
MatSliderModule,
|
||||||
|
MatSlideToggleModule,
|
||||||
MatDividerModule,
|
MatDividerModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
NgxMatSelectSearchModule,
|
NgxMatSelectSearchModule,
|
||||||
@ -257,8 +270,16 @@ import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
|||||||
OverlayComponent,
|
OverlayComponent,
|
||||||
PreviewComponent,
|
PreviewComponent,
|
||||||
NgxMaterialTimepickerModule,
|
NgxMaterialTimepickerModule,
|
||||||
|
ChartsModule,
|
||||||
TrustPipe,
|
TrustPipe,
|
||||||
LocalizedDatePipe
|
LocalizedDatePipe,
|
||||||
|
BreadcrumbComponent,
|
||||||
|
ChartsComponent,
|
||||||
|
CheckInputComponent,
|
||||||
|
BannerComponent,
|
||||||
|
PollFormComponent,
|
||||||
|
MotionPollDialogComponent,
|
||||||
|
AssignmentPollDialogComponent
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
PermsDirective,
|
PermsDirective,
|
||||||
@ -305,7 +326,14 @@ import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
|||||||
PreviewComponent,
|
PreviewComponent,
|
||||||
HeightResizingDirective,
|
HeightResizingDirective,
|
||||||
TrustPipe,
|
TrustPipe,
|
||||||
LocalizedDatePipe
|
LocalizedDatePipe,
|
||||||
|
BreadcrumbComponent,
|
||||||
|
ChartsComponent,
|
||||||
|
CheckInputComponent,
|
||||||
|
BannerComponent,
|
||||||
|
PollFormComponent,
|
||||||
|
MotionPollDialogComponent,
|
||||||
|
AssignmentPollDialogComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
@ -330,7 +358,9 @@ import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
|||||||
ChoiceDialogComponent,
|
ChoiceDialogComponent,
|
||||||
ProjectionDialogComponent,
|
ProjectionDialogComponent,
|
||||||
ProgressSnackBarComponent,
|
ProgressSnackBarComponent,
|
||||||
SuperSearchComponent
|
SuperSearchComponent,
|
||||||
|
MotionPollDialogComponent,
|
||||||
|
AssignmentPollDialogComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SharedModule {}
|
export class SharedModule {}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
<os-list-view-table
|
<os-list-view-table
|
||||||
[repo]="repo"
|
[listObservableProvider]="repo"
|
||||||
[vScrollFixed]="64"
|
[vScrollFixed]="64"
|
||||||
[filterService]="filterService"
|
[filterService]="filterService"
|
||||||
[columns]="tableColumnDefinition"
|
[columns]="tableColumnDefinition"
|
||||||
|
@ -3,11 +3,13 @@ import { RouterModule, Routes } from '@angular/router';
|
|||||||
|
|
||||||
import { AssignmentDetailComponent } from './components/assignment-detail/assignment-detail.component';
|
import { AssignmentDetailComponent } from './components/assignment-detail/assignment-detail.component';
|
||||||
import { AssignmentListComponent } from './components/assignment-list/assignment-list.component';
|
import { AssignmentListComponent } from './components/assignment-list/assignment-list.component';
|
||||||
|
import { AssignmentPollDetailComponent } from './components/assignment-poll-detail/assignment-poll-detail.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', component: AssignmentListComponent, pathMatch: 'full' },
|
{ path: '', component: AssignmentListComponent, pathMatch: 'full' },
|
||||||
{ path: 'new', component: AssignmentDetailComponent, data: { basePerm: 'assignments.can_manage' } },
|
{ path: 'new', component: AssignmentDetailComponent, data: { basePerm: 'assignments.can_manage' } },
|
||||||
{ path: ':id', component: AssignmentDetailComponent, data: { basePerm: 'assignments.can_see' } }
|
{ path: ':id', component: AssignmentDetailComponent, data: { basePerm: 'assignments.can_see' } },
|
||||||
|
{ path: 'polls', children: [{ path: ':id', component: AssignmentPollDetailComponent }] }
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { AppConfig } from '../../core/definitions/app-config';
|
import { AppConfig } from '../../core/definitions/app-config';
|
||||||
|
import { AssignmentOptionRepositoryService } from 'app/core/repositories/assignments/assignment-option-repository.service';
|
||||||
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
|
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
|
||||||
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
|
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
|
||||||
import { AssignmentVoteRepositoryService } from 'app/core/repositories/assignments/assignment-vote-repository.service';
|
import { AssignmentVoteRepositoryService } from 'app/core/repositories/assignments/assignment-vote-repository.service';
|
||||||
|
import { AssignmentOption } from 'app/shared/models/assignments/assignment-option';
|
||||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { AssignmentVote } from 'app/shared/models/assignments/assignment-vote';
|
import { AssignmentVote } from 'app/shared/models/assignments/assignment-vote';
|
||||||
import { Assignment } from '../../shared/models/assignments/assignment';
|
import { Assignment } from '../../shared/models/assignments/assignment';
|
||||||
import { ViewAssignment } from './models/view-assignment';
|
import { ViewAssignment } from './models/view-assignment';
|
||||||
|
import { ViewAssignmentOption } from './models/view-assignment-option';
|
||||||
import { ViewAssignmentPoll } from './models/view-assignment-poll';
|
import { ViewAssignmentPoll } from './models/view-assignment-poll';
|
||||||
import { ViewAssignmentVote } from './models/view-assignment-vote';
|
import { ViewAssignmentVote } from './models/view-assignment-vote';
|
||||||
|
|
||||||
@ -27,6 +30,11 @@ export const AssignmentsAppConfig: AppConfig = {
|
|||||||
model: AssignmentVote,
|
model: AssignmentVote,
|
||||||
viewModel: ViewAssignmentVote,
|
viewModel: ViewAssignmentVote,
|
||||||
repository: AssignmentVoteRepositoryService
|
repository: AssignmentVoteRepositoryService
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: AssignmentOption,
|
||||||
|
viewModel: ViewAssignmentOption,
|
||||||
|
repository: AssignmentOptionRepositoryService
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
mainMenuEntries: [
|
mainMenuEntries: [
|
||||||
|
@ -3,7 +3,8 @@ import { NgModule } from '@angular/core';
|
|||||||
|
|
||||||
import { AssignmentDetailComponent } from './components/assignment-detail/assignment-detail.component';
|
import { AssignmentDetailComponent } from './components/assignment-detail/assignment-detail.component';
|
||||||
import { AssignmentListComponent } from './components/assignment-list/assignment-list.component';
|
import { AssignmentListComponent } from './components/assignment-list/assignment-list.component';
|
||||||
import { AssignmentPollDialogComponent } from './components/assignment-poll-dialog/assignment-poll-dialog.component';
|
import { AssignmentPollDetailComponent } from './components/assignment-poll-detail/assignment-poll-detail.component';
|
||||||
|
import { AssignmentPollVoteComponent } from './components/assignment-poll-vote/assignment-poll-vote.component';
|
||||||
import { AssignmentPollComponent } from './components/assignment-poll/assignment-poll.component';
|
import { AssignmentPollComponent } from './components/assignment-poll/assignment-poll.component';
|
||||||
import { AssignmentsRoutingModule } from './assignments-routing.module';
|
import { AssignmentsRoutingModule } from './assignments-routing.module';
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
@ -11,11 +12,11 @@ import { SharedModule } from '../../shared/shared.module';
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule, AssignmentsRoutingModule, SharedModule],
|
imports: [CommonModule, AssignmentsRoutingModule, SharedModule],
|
||||||
declarations: [
|
declarations: [
|
||||||
AssignmentListComponent,
|
|
||||||
AssignmentDetailComponent,
|
AssignmentDetailComponent,
|
||||||
|
AssignmentListComponent,
|
||||||
AssignmentPollComponent,
|
AssignmentPollComponent,
|
||||||
AssignmentPollDialogComponent
|
AssignmentPollDetailComponent,
|
||||||
],
|
AssignmentPollVoteComponent
|
||||||
entryComponents: [AssignmentPollDialogComponent]
|
]
|
||||||
})
|
})
|
||||||
export class AssignmentsModule {}
|
export class AssignmentsModule {}
|
||||||
|
@ -63,8 +63,26 @@
|
|||||||
<ng-container [ngTemplateOutlet]="assignmentFormTemplate"></ng-container>
|
<ng-container [ngTemplateOutlet]="assignmentFormTemplate"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!editAssignment">
|
<div *ngIf="!editAssignment">
|
||||||
|
<!-- assignment meta infos-->
|
||||||
<ng-container [ngTemplateOutlet]="metaInfoTemplate"></ng-container>
|
<ng-container [ngTemplateOutlet]="metaInfoTemplate"></ng-container>
|
||||||
<ng-container [ngTemplateOutlet]="contentTemplate"></ng-container>
|
<!-- votable polls -->
|
||||||
|
<ng-container *ngIf="assignment">
|
||||||
|
<ng-container *ngFor="let poll of assignment.polls; trackBy: trackByIndex">
|
||||||
|
<mat-card class="os-card" *ngIf="poll.canBeVotedFor">
|
||||||
|
<os-assignment-poll [poll]="poll"> </os-assignment-poll>
|
||||||
|
</mat-card>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<!-- candidates list -->
|
||||||
|
<ng-container [ngTemplateOutlet]="candidatesTemplate"></ng-container>
|
||||||
|
<!-- closed polls -->
|
||||||
|
<ng-container *ngIf="assignment">
|
||||||
|
<ng-container *ngFor="let poll of assignment.polls; trackBy: trackByIndex">
|
||||||
|
<mat-card class="os-card" *ngIf="!poll.canBeVotedFor">
|
||||||
|
<os-assignment-poll [poll]="poll"> </os-assignment-poll>
|
||||||
|
</mat-card>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -116,46 +134,10 @@
|
|||||||
</mat-card>
|
</mat-card>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #contentTemplate>
|
|
||||||
<mat-card class="os-card">
|
|
||||||
<ng-container *ngIf="assignment && !assignment.isFinished" [ngTemplateOutlet]="candidatesTemplate">
|
|
||||||
</ng-container>
|
|
||||||
<!-- TODO related agenda item to create/updade: internal status; parent_id ? -->
|
|
||||||
<ng-container [ngTemplateOutlet]="pollTemplate"></ng-container>
|
|
||||||
<!-- TODO different status/display if finished -->
|
|
||||||
</mat-card>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<!-- poll template -->
|
|
||||||
<ng-template #pollTemplate>
|
|
||||||
<div class="ballot-controls-grid">
|
|
||||||
<div class="ballot-title" *ngIf="assignment && assignment.polls && assignment.polls.length">
|
|
||||||
<h3 translate>Election result</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ballot-button" *ngIf="assignment && hasPerms('createPoll')">
|
|
||||||
<button mat-button (click)="createPoll()">
|
|
||||||
<mat-icon color="primary">poll</mat-icon>
|
|
||||||
<span translate>New ballot</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<mat-tab-group
|
|
||||||
(selectedTabChange)="onTabChange()"
|
|
||||||
*ngIf="assignment && assignment.polls && assignment.polls.length"
|
|
||||||
>
|
|
||||||
<!-- TODO avoid animation/switching on update -->
|
|
||||||
<mat-tab *ngFor="let poll of assignment.polls; let i = index; trackBy: trackByIndex" [label]="poll.title">
|
|
||||||
<os-assignment-poll [assignment]="assignment" [poll]="poll"> </os-assignment-poll>
|
|
||||||
</mat-tab>
|
|
||||||
</mat-tab-group>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template #candidatesTemplate>
|
<ng-template #candidatesTemplate>
|
||||||
<!-- Candidates -->
|
<mat-card class="os-card">
|
||||||
|
<ng-container *ngIf="assignment && !assignment.isFinished">
|
||||||
<h3 translate>Candidates</h3>
|
<h3 translate>Candidates</h3>
|
||||||
|
|
||||||
<!-- Candidate List -->
|
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="candidates-list"
|
class="candidates-list"
|
||||||
@ -165,7 +147,7 @@
|
|||||||
[input]="assignment.assignment_related_users"
|
[input]="assignment.assignment_related_users"
|
||||||
[live]="true"
|
[live]="true"
|
||||||
[count]="true"
|
[count]="true"
|
||||||
[enable]="hasPerms('manage')"
|
[enable]="hasPerms('addOthers')"
|
||||||
(sortEvent)="onSortingChange($event)"
|
(sortEvent)="onSortingChange($event)"
|
||||||
>
|
>
|
||||||
<!-- implicit item references into the component using ng-template slot -->
|
<!-- implicit item references into the component using ng-template slot -->
|
||||||
@ -174,7 +156,6 @@
|
|||||||
<button
|
<button
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
matTooltip="{{ 'Remove candidate' | translate }}"
|
matTooltip="{{ 'Remove candidate' | translate }}"
|
||||||
*osPerms="'assignments.can_manage'"
|
|
||||||
(click)="removeUser(item)"
|
(click)="removeUser(item)"
|
||||||
>
|
>
|
||||||
<mat-icon>clear</mat-icon>
|
<mat-icon>clear</mat-icon>
|
||||||
@ -218,10 +199,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider
|
<div class="ballot-button" *ngIf="assignment && hasPerms('createPoll')">
|
||||||
*ngIf="assignment && assignment.polls && assignment.polls.length"
|
<button mat-button (click)="openDialog()">
|
||||||
class="candidate-list-separator"
|
<mat-icon color="primary">poll</mat-icon>
|
||||||
></mat-divider>
|
<span translate>New ballot</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</mat-card>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<!-- Form -->
|
<!-- Form -->
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { AssignmentDetailComponent } from './assignment-detail.component';
|
import { AssignmentDetailComponent } from './assignment-detail.component';
|
||||||
|
import { AssignmentPollVoteComponent } from '../assignment-poll-vote/assignment-poll-vote.component';
|
||||||
import { AssignmentPollComponent } from '../assignment-poll/assignment-poll.component';
|
import { AssignmentPollComponent } from '../assignment-poll/assignment-poll.component';
|
||||||
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
||||||
|
|
||||||
@ -11,7 +12,7 @@ describe('AssignmentDetailComponent', () => {
|
|||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [E2EImportsModule],
|
imports: [E2EImportsModule],
|
||||||
declarations: [AssignmentDetailComponent, AssignmentPollComponent]
|
declarations: [AssignmentDetailComponent, AssignmentPollComponent, AssignmentPollVoteComponent]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -22,7 +22,9 @@ import { LocalPermissionsService } from 'app/site/motions/services/local-permiss
|
|||||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { AssignmentPdfExportService } from '../../services/assignment-pdf-export.service';
|
import { AssignmentPdfExportService } from '../../services/assignment-pdf-export.service';
|
||||||
|
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||||
import { AssignmentPhases, ViewAssignment } from '../../models/view-assignment';
|
import { AssignmentPhases, ViewAssignment } from '../../models/view-assignment';
|
||||||
|
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||||
import { ViewAssignmentRelatedUser } from '../../models/view-assignment-related-user';
|
import { ViewAssignmentRelatedUser } from '../../models/view-assignment-related-user';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -173,7 +175,8 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
private tagRepo: TagRepositoryService,
|
private tagRepo: TagRepositoryService,
|
||||||
private promptService: PromptService,
|
private promptService: PromptService,
|
||||||
private pdfService: AssignmentPdfExportService,
|
private pdfService: AssignmentPdfExportService,
|
||||||
private mediafileRepo: MediafileRepositoryService
|
private mediafileRepo: MediafileRepositoryService,
|
||||||
|
private pollDialog: AssignmentPollDialogService
|
||||||
) {
|
) {
|
||||||
super(title, translate, matSnackBar);
|
super(title, translate, matSnackBar);
|
||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
@ -302,8 +305,12 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
/**
|
/**
|
||||||
* Creates a new Poll
|
* Creates a new Poll
|
||||||
*/
|
*/
|
||||||
public async createPoll(): Promise<void> {
|
public openDialog(): void {
|
||||||
// await this.repo.createPoll(this.assignment).catch(this.raiseError);
|
this.pollDialog.openDialog({
|
||||||
|
collectionString: ViewAssignmentPoll.COLLECTIONSTRING,
|
||||||
|
assignment_id: this.assignment.id,
|
||||||
|
assignment: this.assignment
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
<os-list-view-table
|
<os-list-view-table
|
||||||
[repo]="repo"
|
[listObservableProvider]="repo"
|
||||||
[filterService]="filterService"
|
[filterService]="filterService"
|
||||||
[sortService]="sortService"
|
[sortService]="sortService"
|
||||||
[columns]="tableColumnDefinition"
|
[columns]="tableColumnDefinition"
|
||||||
|
@ -12,7 +12,7 @@ import { AssignmentRepositoryService } from 'app/core/repositories/assignments/a
|
|||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||||
import { BaseListViewComponent } from 'app/site/base/base-list-view';
|
import { BaseListViewComponent } from 'app/site/base/base-list-view';
|
||||||
import { AssignmentFilterListService } from '../../services/assignment-filter.service';
|
import { AssignmentFilterListService } from '../../services/assignment-filter-list.service';
|
||||||
import { AssignmentPdfExportService } from '../../services/assignment-pdf-export.service';
|
import { AssignmentPdfExportService } from '../../services/assignment-pdf-export.service';
|
||||||
import { AssignmentSortListService } from '../../services/assignment-sort-list.service';
|
import { AssignmentSortListService } from '../../services/assignment-sort-list.service';
|
||||||
import { AssignmentPhases, ViewAssignment } from '../../models/view-assignment';
|
import { AssignmentPhases, ViewAssignment } from '../../models/view-assignment';
|
||||||
|
@ -0,0 +1,78 @@
|
|||||||
|
<os-head-bar
|
||||||
|
[goBack]="true"
|
||||||
|
[nav]="false"
|
||||||
|
[hasMainButton]="poll && (poll.state === 2 || poll.state === 3)"
|
||||||
|
[mainButtonIcon]="'edit'"
|
||||||
|
[mainActionTooltip]="'Edit' | translate"
|
||||||
|
(mainEvent)="openDialog()"
|
||||||
|
>
|
||||||
|
<div class="title-slot">
|
||||||
|
<h2 *ngIf="!!poll">{{ poll.title }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="menu-slot" *osPerms="'agenda.can_manage'; or: 'agenda.can_see_list_of_speakers'">
|
||||||
|
<button type="button" mat-icon-button [matMenuTriggerFor]="pollDetailMenu">
|
||||||
|
<mat-icon>more_vert</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</os-head-bar>
|
||||||
|
|
||||||
|
<mat-card class="os-card">
|
||||||
|
<ng-container [ngTemplateOutlet]="viewTemplate"></ng-container>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<!-- Detailview for poll -->
|
||||||
|
<ng-template #viewTemplate>
|
||||||
|
<ng-container *ngIf="poll">
|
||||||
|
<h1>{{ poll.title }}</h1>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<os-breadcrumb [breadcrumbs]="breadcrumbs" [breadcrumbStyle]="'>'"></os-breadcrumb>
|
||||||
|
<div class="poll-content">
|
||||||
|
<div>{{ 'Current state' | translate }}: {{ poll.stateVerbose | translate }}</div>
|
||||||
|
<div *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
||||||
|
{{ 'Groups' | translate }}:
|
||||||
|
<span *ngFor="let group of poll.groups">{{ group.getTitle() | translate }}</span>
|
||||||
|
</div>
|
||||||
|
<div>{{ 'Poll type' | translate }}: {{ poll.typeVerbose | translate }}</div>
|
||||||
|
<div>{{ 'Poll method' | translate }}: {{ poll.pollmethodVerbose | translate }}</div>
|
||||||
|
<div>{{ 'Majority method' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div>
|
||||||
|
<div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="poll.state === 3 || poll.state === 4">
|
||||||
|
<h2 translate>Result</h2>
|
||||||
|
|
||||||
|
<div *ngIf="poll.type === 'named'" style="display: grid; grid-template-columns: auto repeat({{ poll.options.length }}, max-content);">
|
||||||
|
<!-- top left cell is empty -->
|
||||||
|
<div></div>
|
||||||
|
<!-- header (the assignment related users) -->
|
||||||
|
<ng-container *ngFor="let option of poll.options">
|
||||||
|
<div *ngIf="option.user">{{ option.user.full_name }}</div>
|
||||||
|
<div *ngIf="!option.user">{{ "Unknown user" | translate}}</div>
|
||||||
|
</ng-container>
|
||||||
|
<!-- rows -->
|
||||||
|
<ng-container *ngFor="let obj of votesByUser | keyvalue">
|
||||||
|
<div *ngIf="obj.value.user">{{ obj.value.user.full_name }}</div>
|
||||||
|
<div *ngIf="!obj.value.user">{{ "Unknown user" | translate}}</div>
|
||||||
|
<ng-container *ngFor="let option of poll.options">
|
||||||
|
<div>{{ obj.value.votes[option.user_id]}}</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<!-- More Menu -->
|
||||||
|
<mat-menu #pollDetailMenu="matMenu">
|
||||||
|
<os-projector-button [menuItem]="true" [object]="poll" *osPerms="'core.can_manage_projector'"></os-projector-button>
|
||||||
|
<button mat-menu-item *ngIf="poll && poll.type === 'named'" (click)="pseudoanonymizePoll()">
|
||||||
|
<mat-icon>questionmark</mat-icon>
|
||||||
|
<span translate>Pseudoanonymize</span>
|
||||||
|
</button>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<button mat-menu-item (click)="deletePoll()">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
<span translate>Delete</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
@ -0,0 +1,27 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { AssignmentPollDetailComponent } from './assignment-poll-detail.component';
|
||||||
|
|
||||||
|
describe('AssignmentPollDetailComponent', () => {
|
||||||
|
let component: AssignmentPollDetailComponent;
|
||||||
|
let fixture: ComponentFixture<AssignmentPollDetailComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule],
|
||||||
|
declarations: [AssignmentPollDetailComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AssignmentPollDetailComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,59 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatSnackBar } from '@angular/material';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
|
||||||
|
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
||||||
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
|
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||||
|
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
|
||||||
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
|
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||||
|
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||||
|
import { ViewAssignmentVote } from '../../models/view-assignment-vote';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'os-assignment-poll-detail',
|
||||||
|
templateUrl: './assignment-poll-detail.component.html',
|
||||||
|
styleUrls: ['./assignment-poll-detail.component.scss']
|
||||||
|
})
|
||||||
|
export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewAssignmentPoll> {
|
||||||
|
public votesByUser: { [key: number]: { user: ViewUser; votes: { [key: number]: ViewAssignmentVote } } };
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
title: Title,
|
||||||
|
translate: TranslateService,
|
||||||
|
matSnackbar: MatSnackBar,
|
||||||
|
repo: AssignmentPollRepositoryService,
|
||||||
|
route: ActivatedRoute,
|
||||||
|
groupRepo: GroupRepositoryService,
|
||||||
|
prompt: PromptService,
|
||||||
|
pollDialog: AssignmentPollDialogService
|
||||||
|
) {
|
||||||
|
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onPollLoaded(): void {
|
||||||
|
const votes = {};
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
for (const option of this.poll.options) {
|
||||||
|
for (const vote of option.votes) {
|
||||||
|
if (!votes[vote.user_id]) {
|
||||||
|
votes[vote.user_id] = {
|
||||||
|
user: vote.user,
|
||||||
|
votes: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
votes[vote.user_id].votes[option.user_id] =
|
||||||
|
this.poll.pollmethod === AssignmentPollMethods.Votes ? vote.weight : vote.valueVerbose;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(votes, this.poll, this.poll.options);
|
||||||
|
this.votesByUser = votes;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +1,52 @@
|
|||||||
<h2 translate>Voting result</h2>
|
<os-poll-form [data]="pollData" [pollMethods]="assignmentPollMethods" #pollForm></os-poll-form>
|
||||||
<div class="meta-text">
|
<ng-container *ngIf="pollForm.contentForm.get('type').value === 'analog'">
|
||||||
<span translate>Special values</span>:<br />
|
|
||||||
<mat-chip>-1</mat-chip> = <span translate>majority</span>
|
|
||||||
<mat-chip color="accent">-2</mat-chip> =
|
|
||||||
<span translate>undocumented</span>
|
|
||||||
</div>
|
|
||||||
<div class="spacer-top-10"></div>
|
|
||||||
<div class="width-600">
|
|
||||||
<!-- Candidate values -->
|
<!-- Candidate values -->
|
||||||
<div [ngClass]="getGridClass()" *ngFor="let candidate of data.options">
|
<form [formGroup]="dialogVoteForm">
|
||||||
<div class="candidate-name">
|
<div formGroupName="options">
|
||||||
{{ candidate.user.full_name }}
|
<div *ngFor="let option of options" class="votes-grid">
|
||||||
|
<div>
|
||||||
|
<span *ngIf="option.user">{{ option.user.getFullName() }}</span>
|
||||||
|
<span *ngIf="!option.user">No user {{ option.candidate_id }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div *ngFor="let key of optionPollKeys" class="votes">
|
|
||||||
<mat-form-field>
|
<div>
|
||||||
<input type="number"
|
<div *ngFor="let value of analogPollValues" [formGroupName]="option.user_id">
|
||||||
matInput
|
<os-check-input
|
||||||
[value]="getValue(key, candidate)"
|
[placeholder]="voteValueVerbose[value] | translate"
|
||||||
(change)="setValue(key, candidate, $event.target.value)"
|
[checkboxValue]="-1"
|
||||||
/>
|
inputType="number"
|
||||||
<mat-label> {{ key | translate }}</mat-label>
|
[checkboxLabel]="'Majority' | translate"
|
||||||
</mat-form-field>
|
[formControlName]="value"
|
||||||
|
></os-check-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngFor="let value of sumValues" class="votes-grid">
|
||||||
|
<div></div>
|
||||||
|
<os-check-input
|
||||||
|
[placeholder]="generalValueVerbose[value] | translate"
|
||||||
|
[checkboxValue]="-1"
|
||||||
|
inputType="number"
|
||||||
|
[checkboxLabel]="'Majority' | translate"
|
||||||
|
[formControlName]="value"
|
||||||
|
></os-check-input>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<div class="spacer-top-20">
|
||||||
|
<mat-checkbox
|
||||||
|
[(ngModel)]="publishImmediately"
|
||||||
|
(change)="publishStateChanged($event.checked)"
|
||||||
|
>
|
||||||
|
<span translate>Publish immediately</span>
|
||||||
|
</mat-checkbox>
|
||||||
|
<mat-error *ngIf="!dialogVoteForm.valid" translate>
|
||||||
|
If you want to publish after creating, you have to fill at least one of the fields.
|
||||||
|
</mat-error>
|
||||||
|
</div>
|
||||||
<!-- Summary values -->
|
<!-- Summary values -->
|
||||||
<div *ngFor="let sumValue of sumValues" class="sum-value">
|
<!-- <div *ngFor="let sumValue of sumValues" class="sum-value">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@ -34,9 +56,14 @@
|
|||||||
/>
|
/>
|
||||||
<mat-label>{{ pollService.getLabel(sumValue) | translate }}</mat-label>
|
<mat-label>{{ pollService.getLabel(sumValue) | translate }}</mat-label>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</ng-container>
|
||||||
<div class="submit-buttons">
|
<mat-divider></mat-divider>
|
||||||
<button mat-button (click)="submit()">{{ 'Save' | translate }}</button>
|
<div mat-dialog-actions>
|
||||||
<button mat-button (click)="cancel()">{{ 'Cancel' | translate }}</button>
|
<button mat-button (click)="submitPoll()" [disabled]="!pollForm.contentForm || pollForm.contentForm.invalid || dialogVoteForm.invalid">
|
||||||
|
<span translate>Save</span>
|
||||||
|
</button>
|
||||||
|
<button mat-button [mat-dialog-close]="false">
|
||||||
|
<span translate>Cancel</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,36 +19,12 @@
|
|||||||
border-bottom: 1px solid grey;
|
border-bottom: 1px solid grey;
|
||||||
}
|
}
|
||||||
|
|
||||||
.votes-grid-1 {
|
.votes-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 5px;
|
grid-gap: 5px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
grid-template-columns: auto 60px;
|
align-items: baseline;
|
||||||
align-items: center;
|
grid-template-columns: auto max-content;
|
||||||
.mat-form-field {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: more elegant way. Only grid-template-columns is different
|
|
||||||
.votes-grid-2 {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: 5px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
align-items: center;
|
|
||||||
grid-template-columns: auto 60px 60px;
|
|
||||||
.mat-form-field {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: more elegant way. Only grid-template-columns is different
|
|
||||||
.votes-grid-3 {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: 5px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
align-items: center;
|
|
||||||
grid-template-columns: auto 60px 60px 60px;
|
|
||||||
.mat-form-field {
|
.mat-form-field {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,22 @@
|
|||||||
import { Component, Inject } from '@angular/core';
|
import { Component, Inject, OnInit, ViewChild } from '@angular/core';
|
||||||
|
import { FormBuilder, Validators } from '@angular/forms';
|
||||||
|
import { MatSnackBar } from '@angular/material';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { CalculablePollKey, PollVoteValue } from 'app/core/ui-services/poll.service';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||||
|
import { GeneralValueVerbose, VoteValue, VoteValueVerbose } from 'app/shared/models/poll/base-vote';
|
||||||
|
import { AssignmentPollMethodsVerbose } from 'app/site/assignments/models/view-assignment-poll';
|
||||||
|
import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.component';
|
||||||
|
import { PollFormComponent } from 'app/site/polls/components/poll-form/poll-form.component';
|
||||||
|
import { CalculablePollKey, PollVoteValue } from 'app/site/polls/services/poll.service';
|
||||||
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { ViewAssignmentOption } from '../../models/view-assignment-option';
|
import { ViewAssignmentOption } from '../../models/view-assignment-option';
|
||||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||||
|
|
||||||
/**
|
type OptionsObject = { user_id: number; user: ViewUser }[];
|
||||||
* Vote entries included once for summary (e.g. total votes cast)
|
|
||||||
*/
|
|
||||||
type summaryPollKey = 'votescast' | 'votesvalid' | 'votesinvalid' | 'votesno' | 'votesabstain';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A dialog for updating the values of an assignment-related poll.
|
* A dialog for updating the values of an assignment-related poll.
|
||||||
@ -18,7 +26,7 @@ type summaryPollKey = 'votescast' | 'votesvalid' | 'votesinvalid' | 'votesno' |
|
|||||||
templateUrl: './assignment-poll-dialog.component.html',
|
templateUrl: './assignment-poll-dialog.component.html',
|
||||||
styleUrls: ['./assignment-poll-dialog.component.scss']
|
styleUrls: ['./assignment-poll-dialog.component.scss']
|
||||||
})
|
})
|
||||||
export class AssignmentPollDialogComponent {
|
export class AssignmentPollDialogComponent extends BasePollDialogComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
* The actual poll data to work on
|
* The actual poll data to work on
|
||||||
*/
|
*/
|
||||||
@ -27,13 +35,8 @@ export class AssignmentPollDialogComponent {
|
|||||||
/**
|
/**
|
||||||
* The summary values that will have fields in the dialog
|
* The summary values that will have fields in the dialog
|
||||||
*/
|
*/
|
||||||
public get sumValues(): summaryPollKey[] {
|
public get sumValues(): string[] {
|
||||||
const generalValues: summaryPollKey[] = ['votesvalid', 'votesinvalid', 'votescast'];
|
return ['votesvalid', 'votesinvalid', 'votescast'];
|
||||||
if (this.data.pollmethod === 'votes') {
|
|
||||||
return ['votesno', 'votesabstain', ...generalValues];
|
|
||||||
} else {
|
|
||||||
return generalValues;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,39 +45,118 @@ export class AssignmentPollDialogComponent {
|
|||||||
*/
|
*/
|
||||||
public specialValues: [number, string][];
|
public specialValues: [number, string][];
|
||||||
|
|
||||||
|
@ViewChild('pollForm', { static: true })
|
||||||
|
protected pollForm: PollFormComponent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* vote entries for each option in this component. Is empty if method
|
* vote entries for each option in this component. Is empty if method
|
||||||
* requires one vote per candidate
|
* requires one vote per candidate
|
||||||
*/
|
*/
|
||||||
public optionPollKeys: PollVoteValue[];
|
public analogPollValues: VoteValue[];
|
||||||
|
|
||||||
|
public voteValueVerbose = VoteValueVerbose;
|
||||||
|
public generalValueVerbose = GeneralValueVerbose;
|
||||||
|
|
||||||
|
public assignmentPollMethods = AssignmentPollMethodsVerbose;
|
||||||
|
|
||||||
|
public options: OptionsObject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor. Retrieves necessary metadata from the pollService,
|
* Constructor. Retrieves necessary metadata from the pollService,
|
||||||
* injects the poll itself
|
* injects the poll itself
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
public dialogRef: MatDialogRef<AssignmentPollDialogComponent>,
|
private fb: FormBuilder,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: ViewAssignmentPoll
|
title: Title,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
matSnackbar: MatSnackBar,
|
||||||
|
public dialogRef: MatDialogRef<BasePollDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public pollData: Partial<ViewAssignmentPoll>
|
||||||
) {
|
) {
|
||||||
switch (this.data.pollmethod) {
|
super(title, translate, matSnackbar, dialogRef);
|
||||||
case 'votes':
|
}
|
||||||
this.optionPollKeys = ['Votes'];
|
|
||||||
break;
|
public ngOnInit(): void {
|
||||||
case 'yn':
|
// on new poll creation, poll.options does not exist, so we have to build a substitute from the assignment candidates
|
||||||
this.optionPollKeys = ['Yes', 'No'];
|
this.options = this.pollData.options
|
||||||
break;
|
? this.pollData.options
|
||||||
case 'yna':
|
: this.pollData.assignment.candidates.map(
|
||||||
this.optionPollKeys = ['Yes', 'No', 'Abstain'];
|
user => ({
|
||||||
break;
|
user_id: user.id,
|
||||||
|
user: user
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.pollForm.contentForm.get('pollmethod').valueChanges.subscribe(() => {
|
||||||
|
this.createDialog();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setAnalogPollValues(): void {
|
||||||
|
const pollmethod = this.pollForm.contentForm.get('pollmethod').value;
|
||||||
|
this.analogPollValues = ['Y'];
|
||||||
|
if (pollmethod !== AssignmentPollMethods.Votes) {
|
||||||
|
this.analogPollValues.push('N');
|
||||||
|
}
|
||||||
|
if (pollmethod === AssignmentPollMethods.YNA) {
|
||||||
|
this.analogPollValues.push('A');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateDialogVoteForm(data: Partial<ViewAssignmentPoll>): void {
|
||||||
|
const update = {
|
||||||
|
options: {},
|
||||||
|
votesvalid: data.votesvalid,
|
||||||
|
votesinvalid: data.votesinvalid,
|
||||||
|
votescast: data.votescast
|
||||||
|
};
|
||||||
|
for (const option of data.options) {
|
||||||
|
const votes: any = {};
|
||||||
|
votes.Y = option.yes;
|
||||||
|
if (data.pollmethod !== AssignmentPollMethods.Votes) {
|
||||||
|
votes.N = option.no;
|
||||||
|
}
|
||||||
|
if (data.pollmethod === AssignmentPollMethods.YNA) {
|
||||||
|
votes.A = option.abstain;
|
||||||
|
}
|
||||||
|
update.options[option.user_id] = votes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dialogVoteForm) {
|
||||||
|
const result = this.undoReplaceEmptyValues(update);
|
||||||
|
this.dialogVoteForm.setValue(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the dialog, submitting nothing. Triggered by the cancel button and
|
* Pre-executed method to initialize the dialog-form depending on the poll-method.
|
||||||
* default angular cancelling behavior
|
|
||||||
*/
|
*/
|
||||||
public cancel(): void {
|
private createDialog(): void {
|
||||||
this.dialogRef.close();
|
this.setAnalogPollValues();
|
||||||
|
|
||||||
|
this.dialogVoteForm = this.fb.group({
|
||||||
|
options: this.fb.group(
|
||||||
|
// create a form group for each option with the user id as key
|
||||||
|
this.options.mapToObject(option => ({
|
||||||
|
[option.user_id]: this.fb.group(
|
||||||
|
// for each user, create a form group with a control for each valid input (Y, N, A)
|
||||||
|
this.analogPollValues.mapToObject(value => ({
|
||||||
|
[value]: ['', [Validators.min(-2)]]
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
// insert all used global fields
|
||||||
|
...this.sumValues.mapToObject(sumValue => ({
|
||||||
|
[sumValue]: ['', [Validators.min(-2)]]
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
if (this.pollData.poll) {
|
||||||
|
this.updateDialogVoteForm(this.pollData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -163,10 +245,6 @@ export class AssignmentPollDialogComponent {
|
|||||||
* @param weight
|
* @param weight
|
||||||
*/
|
*/
|
||||||
public setSumValue(value: any /*SummaryPollKey*/, weight: string): void {
|
public setSumValue(value: any /*SummaryPollKey*/, weight: string): void {
|
||||||
this.data[value] = parseFloat(weight);
|
this.pollData[value] = parseFloat(weight);
|
||||||
}
|
|
||||||
|
|
||||||
public getGridClass(): string {
|
|
||||||
return `votes-grid-${this.optionPollKeys.length}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
<ng-container *ngIf="poll">
|
||||||
|
<ng-container *ngIf="vmanager.canVote(poll)">
|
||||||
|
<span *ngIf="poll.pollmethod === 'votes'">{{ "You can distribute" | translate }} {{ poll.votes_amount }} {{ "votes" | translate }}.</span>
|
||||||
|
<form *ngIf="voteForm" [formGroup]="voteForm" class="voting-grid">
|
||||||
|
<ng-container *ngFor="let option of poll.options">
|
||||||
|
<div>
|
||||||
|
<span *ngIf="option.user">{{ option.user.getFullName() }}</span>
|
||||||
|
<span *ngIf="!option.user">No user {{ option.candidate_id }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="current-vote">
|
||||||
|
<ng-container *ngIf="currentVotes[option.user_id] !== null">
|
||||||
|
({{ "Current" | translate }}: {{ getCurrentVoteVerbose(option.user_id) | translate }})
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-radio-group
|
||||||
|
name="votes-{{ poll.id }}-{{ option.id }}"
|
||||||
|
[formControlName]="option.id"
|
||||||
|
*ngIf="poll.pollmethod !== 'votes'"
|
||||||
|
>
|
||||||
|
<mat-radio-button value="Y">
|
||||||
|
<span translate>Yes</span>
|
||||||
|
</mat-radio-button>
|
||||||
|
<mat-radio-button value="N">
|
||||||
|
<span translate>No</span>
|
||||||
|
</mat-radio-button>
|
||||||
|
<mat-radio-button value="A" *ngIf="poll.pollmethod === 'YNA'">
|
||||||
|
<span translate>Abstain</span>
|
||||||
|
</mat-radio-button>
|
||||||
|
</mat-radio-group>
|
||||||
|
|
||||||
|
<mat-form-field *ngIf="poll.pollmethod === 'votes'" class="vote-input">
|
||||||
|
<input matInput type="number" min="0" [formControlName]="option.id">
|
||||||
|
</mat-form-field>
|
||||||
|
</ng-container>
|
||||||
|
</form>
|
||||||
|
<div class="right-align">
|
||||||
|
<button mat-button mat-button-default (click)="saveVotes()" [disabled]="!voteForm || voteForm.invalid || voteForm.pristine">
|
||||||
|
<span translate>Save</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="!vmanager.canVote(poll)">
|
||||||
|
<span>{{ vmanager.getVotePermissionErrorVerbose(poll) | translate }}</span>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
@ -0,0 +1,12 @@
|
|||||||
|
.current-vote {
|
||||||
|
color: #777;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voting-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
align-items: baseline;
|
||||||
|
grid-template-columns: auto max-content max-content;
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { AssignmentPollVoteComponent } from './assignment-poll-vote.component';
|
||||||
|
|
||||||
|
describe('AssignmentPollVoteComponent', () => {
|
||||||
|
let component: AssignmentPollVoteComponent;
|
||||||
|
let fixture: ComponentFixture<AssignmentPollVoteComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule],
|
||||||
|
declarations: [AssignmentPollVoteComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AssignmentPollVoteComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,86 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
import { MatSnackBar } from '@angular/material';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
|
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
|
||||||
|
import { AssignmentVoteRepositoryService } from 'app/core/repositories/assignments/assignment-vote-repository.service';
|
||||||
|
import { VotingService } from 'app/core/ui-services/voting.service';
|
||||||
|
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||||
|
import { VoteValueVerbose } from 'app/shared/models/poll/base-vote';
|
||||||
|
import { BasePollVoteComponent } from 'app/site/polls/components/base-poll-vote.component';
|
||||||
|
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||||
|
import { ViewAssignmentVote } from '../../models/view-assignment-vote';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'os-assignment-poll-vote',
|
||||||
|
templateUrl: './assignment-poll-vote.component.html',
|
||||||
|
styleUrls: ['./assignment-poll-vote.component.scss']
|
||||||
|
})
|
||||||
|
export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssignmentPoll> implements OnInit {
|
||||||
|
public pollMethods = AssignmentPollMethods;
|
||||||
|
|
||||||
|
public voteForm: FormGroup;
|
||||||
|
|
||||||
|
/** holds the currently saved votes */
|
||||||
|
public currentVotes: { [key: number]: string | number | null } = {};
|
||||||
|
|
||||||
|
private votes: ViewAssignmentVote[];
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
title: Title,
|
||||||
|
translate: TranslateService,
|
||||||
|
matSnackbar: MatSnackBar,
|
||||||
|
vmanager: VotingService,
|
||||||
|
operator: OperatorService,
|
||||||
|
private voteRepo: AssignmentVoteRepositoryService,
|
||||||
|
private pollRepo: AssignmentPollRepositoryService,
|
||||||
|
private formBuilder: FormBuilder
|
||||||
|
) {
|
||||||
|
super(title, translate, matSnackbar, vmanager, operator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.voteRepo.getViewModelListObservable().subscribe(votes => {
|
||||||
|
this.votes = votes;
|
||||||
|
this.updateVotes();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateVotes(): void {
|
||||||
|
if (this.user && this.votes && this.poll) {
|
||||||
|
const filtered = this.votes.filter(
|
||||||
|
vote => vote.option.poll_id === this.poll.id && vote.user_id === this.user.id
|
||||||
|
);
|
||||||
|
this.voteForm = this.formBuilder.group(
|
||||||
|
this.poll.options.reduce((obj, option) => {
|
||||||
|
obj[option.id] = ['', [Validators.required]];
|
||||||
|
return obj;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
|
for (const option of this.poll.options) {
|
||||||
|
const curr_vote = filtered.find(vote => vote.option.id === option.id);
|
||||||
|
this.currentVotes[option.user_id] = curr_vote
|
||||||
|
? this.poll.pollmethod === AssignmentPollMethods.Votes
|
||||||
|
? curr_vote.weight
|
||||||
|
: curr_vote.value
|
||||||
|
: null;
|
||||||
|
this.voteForm.get(option.id.toString()).setValue(this.currentVotes[option.user_id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public saveVotes(): void {
|
||||||
|
this.pollRepo.vote(this.voteForm.value, this.poll.id).catch(this.raiseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCurrentVoteVerbose(user_id: number): string {
|
||||||
|
const curr_vote = this.currentVotes[user_id];
|
||||||
|
return this.poll.pollmethod === AssignmentPollMethods.Votes ? curr_vote : VoteValueVerbose[curr_vote];
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@
|
|||||||
<!-- Buttons -->
|
<!-- Buttons -->
|
||||||
<button
|
<button
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
*osPerms="'assignments.can_manage'; "core.can_manage_projector""
|
*osPerms="'assignments.can_manage'; 'core.can_manage_projector'"
|
||||||
[matMenuTriggerFor]="pollItemMenu"
|
[matMenuTriggerFor]="pollItemMenu"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
@ -11,21 +11,21 @@
|
|||||||
</button>
|
</button>
|
||||||
<mat-menu #pollItemMenu="matMenu">
|
<mat-menu #pollItemMenu="matMenu">
|
||||||
<div *osPerms="'assignments.can_manage'">
|
<div *osPerms="'assignments.can_manage'">
|
||||||
<button mat-menu-item (click)="printBallot()">
|
<button mat-menu-item (click)="openDialog()">
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
<span translate>Edit</span>
|
||||||
|
</button>
|
||||||
|
<!-- <button mat-menu-item (click)="printBallot()">
|
||||||
<mat-icon>local_printshop</mat-icon>
|
<mat-icon>local_printshop</mat-icon>
|
||||||
<span translate>Print ballot paper</span>
|
<span translate>Print ballot paper</span>
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item *ngIf="!assignment.isFinished" (click)="enterVotes()">
|
|
||||||
<mat-icon>edit</mat-icon>
|
|
||||||
<span translate>Enter votes</span>
|
|
||||||
</button>
|
|
||||||
<button mat-menu-item (click)="togglePublished()">
|
<button mat-menu-item (click)="togglePublished()">
|
||||||
<mat-icon>
|
<mat-icon>
|
||||||
{{ poll.published ? 'visibility_off' : 'visibility' }}
|
{{ poll.published ? 'visibility_off' : 'visibility' }}
|
||||||
</mat-icon>
|
</mat-icon>
|
||||||
<span *ngIf="!poll.published" translate>Publish</span>
|
<span *ngIf="!poll.published" translate>Publish</span>
|
||||||
<span *ngIf="poll.published" translate>Unpublish</span>
|
<span *ngIf="poll.published" translate>Unpublish</span>
|
||||||
</button>
|
</button> -->
|
||||||
</div>
|
</div>
|
||||||
<div *osPerms="'core.can_manage_projector'">
|
<div *osPerms="'core.can_manage_projector'">
|
||||||
<os-projector-button menuItem="true" [object]="poll"></os-projector-button>
|
<os-projector-button menuItem="true" [object]="poll"></os-projector-button>
|
||||||
@ -40,8 +40,38 @@
|
|||||||
</mat-menu>
|
</mat-menu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="poll-main-content" *ngIf="poll.options">
|
<div class="poll-properties">
|
||||||
<div *ngIf="pollData">
|
<!-- <mat-chip *ngIf="pollService.isElectronicVotingEnabled">{{ poll.typeVerbose }}</mat-chip> -->
|
||||||
|
<mat-chip
|
||||||
|
class="poll-state active"
|
||||||
|
[matMenuTriggerFor]="triggerMenu"
|
||||||
|
[ngClass]="poll.stateVerbose.toLowerCase()"
|
||||||
|
>
|
||||||
|
{{ poll.stateVerbose }}
|
||||||
|
</mat-chip>
|
||||||
|
<!-- <mat-chip
|
||||||
|
class="poll-state active"
|
||||||
|
*ngIf="poll.state !== 2"
|
||||||
|
[matMenuTriggerFor]="triggerMenu"
|
||||||
|
[ngClass]="poll.stateVerbose.toLowerCase()"
|
||||||
|
>
|
||||||
|
{{ poll.stateVerbose }}
|
||||||
|
</mat-chip> -->
|
||||||
|
<!-- <mat-chip class="poll-state" *ngIf="poll.state === 2" [ngClass]="poll.stateVerbose.toLowerCase()">
|
||||||
|
{{ poll.stateVerbose }}
|
||||||
|
</mat-chip> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
<a routerLink="/assignments/polls/{{ poll.id }}">
|
||||||
|
{{ poll.title }}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<os-assignment-poll-vote *ngIf="poll.canBeVotedFor" [poll]="poll"></os-assignment-poll-vote>
|
||||||
|
<!-- <ng-container *ngIf="poll.state === pollStates.STATE_PUBLISHED" [ngTemplateOutlet]="resultsTemplate"></ng-container> -->
|
||||||
|
|
||||||
|
<div class="poll-main-content" *ngIf="false && poll.options">
|
||||||
|
<div *ngIf="canSee">
|
||||||
<div class="poll-grid">
|
<div class="poll-grid">
|
||||||
<div></div>
|
<div></div>
|
||||||
<div><span class="table-view-list-title" translate>Candidates</span></div>
|
<div><span class="table-view-list-title" translate>Candidates</span></div>
|
||||||
@ -78,7 +108,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
(click)="toggleElected(option)"
|
(click)="toggleElected(option)"
|
||||||
[disabled]="!canManage || assignment.isFinished"
|
[disabled]="!canManage || poll.assignment.isFinished"
|
||||||
disableRipple
|
disableRipple
|
||||||
>
|
>
|
||||||
<mat-icon
|
<mat-icon
|
||||||
@ -88,7 +118,7 @@
|
|||||||
>check_box</mat-icon
|
>check_box</mat-icon
|
||||||
>
|
>
|
||||||
<mat-icon
|
<mat-icon
|
||||||
*ngIf="!option.is_elected && canManage && !assignment.isFinished"
|
*ngIf="!option.is_elected && canManage && !poll.assignment.isFinished"
|
||||||
class="top-aligned primary"
|
class="top-aligned primary"
|
||||||
matTooltip="{{ 'Mark as elected' | translate }}"
|
matTooltip="{{ 'Mark as elected' | translate }}"
|
||||||
>
|
>
|
||||||
@ -176,13 +206,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Election Method -->
|
<!-- Election Method -->
|
||||||
<div *ngIf="canManage" class="spacer-bottom-10">
|
<!-- <div *ngIf="canManage" class="spacer-bottom-10">
|
||||||
<h4 translate>Election method</h4>
|
<h4 translate>Election method</h4>
|
||||||
<span>{{ pollMethodName | translate }}</span>
|
<span>{{ pollMethodName | translate }}</span>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<!-- Poll paper hint -->
|
<!-- Poll paper hint -->
|
||||||
<div *ngIf="canManage" class="hint-form" [formGroup]="descriptionForm">
|
<!-- <div *ngIf="canManage" class="hint-form" [formGroup]="descriptionForm">
|
||||||
<mat-form-field class="wide">
|
<mat-form-field class="wide">
|
||||||
<mat-label translate>Hint for ballot paper</mat-label>
|
<mat-label translate>Hint for ballot paper</mat-label>
|
||||||
<input matInput formControlName="description" />
|
<input matInput formControlName="description" />
|
||||||
@ -190,5 +220,21 @@
|
|||||||
<button mat-icon-button [disabled]="!dirtyDescription" (click)="onEditDescriptionButton()">
|
<button mat-icon-button [disabled]="!dirtyDescription" (click)="onEditDescriptionButton()">
|
||||||
<mat-icon inline>check</mat-icon>
|
<mat-icon inline>check</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ng-template #resultsTemplate>
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<mat-menu #triggerMenu="matMenu">
|
||||||
|
<ng-container *ngIf="poll">
|
||||||
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
(click)="changeState(state.value)"
|
||||||
|
*ngFor="let state of poll.nextStates | keyvalue"
|
||||||
|
>
|
||||||
|
<span translate>{{ state.key }}</span>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</mat-menu>
|
||||||
|
@ -17,6 +17,52 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.right-align {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-input {
|
||||||
|
.mat-form-field-wrapper {
|
||||||
|
// padding-bottom: 0;
|
||||||
|
|
||||||
|
.mat-form-field-infix {
|
||||||
|
width: 60px;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-properties {
|
||||||
|
margin: 4px 0;
|
||||||
|
|
||||||
|
.mat-chip {
|
||||||
|
margin: 0 4px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-state {
|
||||||
|
&.created {
|
||||||
|
background-color: #2196f3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
&.started {
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
&.finished {
|
||||||
|
background-color: #ff5252;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
&.published {
|
||||||
|
background-color: #ffd800;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.poll-menu {
|
.poll-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -2,6 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
|
|
||||||
import { E2EImportsModule } from 'e2e-imports.module';
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { AssignmentPollVoteComponent } from '../assignment-poll-vote/assignment-poll-vote.component';
|
||||||
import { AssignmentPollComponent } from './assignment-poll.component';
|
import { AssignmentPollComponent } from './assignment-poll.component';
|
||||||
|
|
||||||
describe('AssignmentPollComponent', () => {
|
describe('AssignmentPollComponent', () => {
|
||||||
@ -10,7 +11,7 @@ describe('AssignmentPollComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [AssignmentPollComponent],
|
declarations: [AssignmentPollComponent, AssignmentPollVoteComponent],
|
||||||
imports: [E2EImportsModule]
|
imports: [E2EImportsModule]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
|
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||||
import { FormGroup } from '@angular/forms';
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
@ -7,10 +7,10 @@ import { Title } from '@angular/platform-browser';
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
import { CalculablePollKey, MajorityMethod } from 'app/core/ui-services/poll.service';
|
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
|
||||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { AssignmentPollPdfService } from '../../services/assignment-poll-pdf.service';
|
import { BasePollComponent } from 'app/site/polls/components/base-poll.component';
|
||||||
import { ViewAssignment } from '../../models/view-assignment';
|
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||||
import { ViewAssignmentOption } from '../../models/view-assignment-option';
|
import { ViewAssignmentOption } from '../../models/view-assignment-option';
|
||||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||||
|
|
||||||
@ -23,30 +23,12 @@ import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
|||||||
styleUrls: ['./assignment-poll.component.scss'],
|
styleUrls: ['./assignment-poll.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class AssignmentPollComponent extends BaseViewComponent implements OnInit {
|
export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPoll> implements OnInit {
|
||||||
/**
|
|
||||||
* The related assignment (used for metainfos, e.g. related user names)
|
|
||||||
*/
|
|
||||||
@Input()
|
|
||||||
public assignment: ViewAssignment;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The poll represented in this component
|
|
||||||
*/
|
|
||||||
@Input()
|
|
||||||
public poll: ViewAssignmentPoll;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Form for updating the poll's description
|
* Form for updating the poll's description
|
||||||
*/
|
*/
|
||||||
public descriptionForm: FormGroup;
|
public descriptionForm: FormGroup;
|
||||||
|
|
||||||
/**
|
|
||||||
* The selected Majority method to display quorum calculations. Will be
|
|
||||||
* set/changed by the user
|
|
||||||
*/
|
|
||||||
public majorityChoice: MajorityMethod | null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* permission checks.
|
* permission checks.
|
||||||
* TODO stub
|
* TODO stub
|
||||||
@ -57,93 +39,38 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
return this.operator.hasPerms('assignments.can_manage');
|
return this.operator.hasPerms('assignments.can_manage');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public get canSee(): boolean {
|
||||||
* Gets the voting options
|
return this.operator.hasPerms('assignments.can_see');
|
||||||
*
|
|
||||||
* @returns all used (not undefined) option-independent values that are
|
|
||||||
* used in this poll (e.g.)
|
|
||||||
*/
|
|
||||||
public get pollValues(): CalculablePollKey[] {
|
|
||||||
// return this.pollService.getVoteOptionsByPoll(this.poll);
|
|
||||||
throw new Error('TODO');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns true if the description on the form differs from the poll's description
|
* @returns true if the description on the form differs from the poll's description
|
||||||
*/
|
*/
|
||||||
public get dirtyDescription(): boolean {
|
public get dirtyDescription(): boolean {
|
||||||
// return this.descriptionForm.get('description').value !== this.poll.description;
|
return this.descriptionForm.get('description').value !== this.poll.description;
|
||||||
throw new Error('TODO');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns true if vote results can be seen by the user
|
|
||||||
*/
|
|
||||||
public get pollData(): boolean {
|
|
||||||
/*if (!this.poll.has_votes) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.poll.published || this.canManage;*/
|
|
||||||
throw new Error('TODO');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the translated poll method name
|
|
||||||
*
|
|
||||||
* TODO: check/improve text here
|
|
||||||
*
|
|
||||||
* @returns a name for the poll method this poll is set to (which is determined
|
|
||||||
* by the number of candidates and config settings).
|
|
||||||
*/
|
|
||||||
public get pollMethodName(): string {
|
|
||||||
if (!this.poll) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
switch (this.poll.pollmethod) {
|
|
||||||
case 'votes':
|
|
||||||
return this.translate.instant('One vote per candidate');
|
|
||||||
case 'yna':
|
|
||||||
return this.translate.instant('Yes/No/Abstain per candidate');
|
|
||||||
case 'yn':
|
|
||||||
return this.translate.instant('Yes/No per candidate');
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
titleService: Title,
|
titleService: Title,
|
||||||
matSnackBar: MatSnackBar,
|
matSnackBar: MatSnackBar,
|
||||||
|
translate: TranslateService,
|
||||||
|
dialog: MatDialog,
|
||||||
|
promptService: PromptService,
|
||||||
|
repo: AssignmentPollRepositoryService,
|
||||||
|
pollDialog: AssignmentPollDialogService,
|
||||||
private operator: OperatorService,
|
private operator: OperatorService,
|
||||||
public translate: TranslateService,
|
private formBuilder: FormBuilder
|
||||||
public dialog: MatDialog,
|
|
||||||
private pdfService: AssignmentPollPdfService
|
|
||||||
) {
|
) {
|
||||||
super(titleService, translate, matSnackBar);
|
super(titleService, matSnackBar, translate, dialog, promptService, repo, pollDialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the currently selected majority choice option from the repo
|
|
||||||
*/
|
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
/*this.majorityChoice =
|
/*this.majorityChoice =
|
||||||
this.pollService.majorityMethods.find(method => method.value === this.pollService.defaultMajorityMethod) ||
|
this.pollService.majorityMethods.find(method => method.value === this.pollService.defaultMajorityMethod) ||
|
||||||
null;
|
null;*/
|
||||||
this.descriptionForm = this.formBuilder.group({
|
this.descriptionForm = this.formBuilder.group({
|
||||||
description: this.poll ? this.poll.description : ''
|
description: this.poll ? this.poll.description : ''
|
||||||
});*/
|
});
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for the 'delete poll' button
|
|
||||||
*
|
|
||||||
* TODO: Some confirmation (advanced logic (e.g. not deleting published?))
|
|
||||||
*/
|
|
||||||
public async onDeletePoll(): Promise<void> {
|
|
||||||
/*const title = this.translate.instant('Are you sure you want to delete this ballot?');
|
|
||||||
if (await this.promptService.open(title)) {
|
|
||||||
await this.assignmentRepo.deletePoll(this.poll).catch(this.raiseError);
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -151,7 +78,8 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public printBallot(): void {
|
public printBallot(): void {
|
||||||
this.pdfService.printBallots(this.poll);
|
throw new Error('TODO');
|
||||||
|
// this.pdfService.printBallots(this.poll);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -173,38 +101,6 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
throw new Error('TODO');
|
throw new Error('TODO');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the {@link AssignmentPollDialogComponent} dialog and then updates the votes, if the dialog
|
|
||||||
* closes successfully (validation is done there)
|
|
||||||
*/
|
|
||||||
public enterVotes(): void {
|
|
||||||
/*const dialogRef = this.dialog.open(AssignmentPollDialogComponent, {
|
|
||||||
data: this.poll.copy(),
|
|
||||||
...mediumDialogSettings
|
|
||||||
});
|
|
||||||
dialogRef.afterClosed().subscribe(result => {
|
|
||||||
if (result) {
|
|
||||||
this.assignmentRepo.updateVotes(result, this.poll).catch(this.raiseError);
|
|
||||||
}
|
|
||||||
});*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the majority method for this poll
|
|
||||||
*
|
|
||||||
* @param method the selected majority method
|
|
||||||
*/
|
|
||||||
public setMajority(method: MajorityMethod): void {
|
|
||||||
this.majorityChoice = method;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles the 'published' state
|
|
||||||
*/
|
|
||||||
public togglePublished(): void {
|
|
||||||
// this.assignmentRepo.updatePoll({ published: !this.poll.published }, this.poll);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark/unmark an option as elected
|
* Mark/unmark an option as elected
|
||||||
*
|
*
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { AssignmentOption } from 'app/shared/models/assignments/assignment-option';
|
import { AssignmentOption } from 'app/shared/models/assignments/assignment-option';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { BaseViewModel } from '../../base/base-view-model';
|
import { BaseViewModel } from '../../base/base-view-model';
|
||||||
|
import { ViewAssignmentPoll } from './view-assignment-poll';
|
||||||
import { ViewAssignmentVote } from './view-assignment-vote';
|
import { ViewAssignmentVote } from './view-assignment-vote';
|
||||||
|
|
||||||
export class ViewAssignmentOption extends BaseViewModel<AssignmentOption> {
|
export class ViewAssignmentOption extends BaseViewModel<AssignmentOption> {
|
||||||
@ -11,9 +12,10 @@ export class ViewAssignmentOption extends BaseViewModel<AssignmentOption> {
|
|||||||
protected _collectionString = AssignmentOption.COLLECTIONSTRING;
|
protected _collectionString = AssignmentOption.COLLECTIONSTRING;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TIMotionOptionRelations {
|
interface TIAssignmentOptionRelations {
|
||||||
votes: ViewAssignmentVote[];
|
votes: ViewAssignmentVote[];
|
||||||
user: ViewUser;
|
user: ViewUser;
|
||||||
|
poll: ViewAssignmentPoll;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewAssignmentOption extends AssignmentOption, TIMotionOptionRelations {}
|
export interface ViewAssignmentOption extends AssignmentOption, TIAssignmentOptionRelations {}
|
||||||
|
@ -1,25 +1,29 @@
|
|||||||
import { AssignmentPoll, AssignmentPollWithoutNestedModels } from 'app/shared/models/assignments/assignment-poll';
|
import { ChartData } from 'app/shared/components/charts/charts.component';
|
||||||
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
|
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||||
import { ViewGroup } from 'app/site/users/models/view-group';
|
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewAssignment } from './view-assignment';
|
||||||
import { ViewAssignmentOption } from './view-assignment-option';
|
import { ViewAssignmentOption } from './view-assignment-option';
|
||||||
|
|
||||||
export interface AssignmentPollTitleInformation {
|
export interface AssignmentPollTitleInformation {
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ViewAssignmentPoll extends BaseProjectableViewModel<AssignmentPoll>
|
export const AssignmentPollMethodsVerbose = {
|
||||||
implements AssignmentPollTitleInformation {
|
votes: 'Fixed Amount of votes for all candidates',
|
||||||
|
YN: 'Yes/No per candidate',
|
||||||
|
YNA: 'Yes/No/Abstain per candidate'
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll> implements AssignmentPollTitleInformation {
|
||||||
public static COLLECTIONSTRING = AssignmentPoll.COLLECTIONSTRING;
|
public static COLLECTIONSTRING = AssignmentPoll.COLLECTIONSTRING;
|
||||||
protected _collectionString = AssignmentPoll.COLLECTIONSTRING;
|
protected _collectionString = AssignmentPoll.COLLECTIONSTRING;
|
||||||
|
|
||||||
public get poll(): AssignmentPoll {
|
public readonly pollClassType: 'assignment' | 'motion' = 'assignment';
|
||||||
return this._model;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getSlide(): ProjectorElementBuildDeskriptor {
|
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||||
/*return {
|
// TODO: update to new voting system?
|
||||||
|
return {
|
||||||
getBasicProjectorElement: options => ({
|
getBasicProjectorElement: options => ({
|
||||||
name: 'assignments/assignment-poll',
|
name: 'assignments/assignment-poll',
|
||||||
assignment_id: this.assignment_id,
|
assignment_id: this.assignment_id,
|
||||||
@ -27,17 +31,22 @@ export class ViewAssignmentPoll extends BaseProjectableViewModel<AssignmentPoll>
|
|||||||
getIdentifiers: () => ['name', 'assignment_id', 'poll_id']
|
getIdentifiers: () => ['name', 'assignment_id', 'poll_id']
|
||||||
}),
|
}),
|
||||||
slideOptions: [],
|
slideOptions: [],
|
||||||
projectionDefaultName: 'assignments',
|
projectionDefaultName: 'assignment-poll',
|
||||||
getDialogTitle: () => 'TODO'
|
getDialogTitle: () => 'TODO'
|
||||||
};*/
|
};
|
||||||
throw new Error('TODO');
|
}
|
||||||
|
|
||||||
|
public get pollmethodVerbose(): string {
|
||||||
|
return AssignmentPollMethodsVerbose[this.pollmethod];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
public generateChartData(): ChartData {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TIAssignmentPollRelations {
|
export interface ViewAssignmentPoll extends AssignmentPoll {
|
||||||
options: ViewAssignmentOption[];
|
options: ViewAssignmentOption[];
|
||||||
voted: ViewUser[];
|
assignment: ViewAssignment;
|
||||||
groups: ViewGroup[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewAssignmentPoll extends AssignmentPollWithoutNestedModels, TIAssignmentPollRelations {}
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { AssignmentVote } from 'app/shared/models/assignments/assignment-vote';
|
import { AssignmentVote } from 'app/shared/models/assignments/assignment-vote';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { BaseViewModel } from '../../base/base-view-model';
|
import { BaseViewModel } from '../../base/base-view-model';
|
||||||
|
import { ViewAssignmentOption } from './view-assignment-option';
|
||||||
|
|
||||||
export class ViewAssignmentVote extends BaseViewModel<AssignmentVote> {
|
export class ViewAssignmentVote extends BaseViewModel<AssignmentVote> {
|
||||||
public get vote(): AssignmentVote {
|
public get vote(): AssignmentVote {
|
||||||
@ -11,7 +12,8 @@ export class ViewAssignmentVote extends BaseViewModel<AssignmentVote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface TIAssignmentVoteRelations {
|
interface TIAssignmentVoteRelations {
|
||||||
user?: ViewUser;
|
user: ViewUser;
|
||||||
|
option: ViewAssignmentOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewAssignmentVote extends AssignmentVote, TIAssignmentVoteRelations {}
|
export interface ViewAssignmentVote extends AssignmentVote, TIAssignmentVoteRelations {}
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { AssignmentPollDialogService } from './assignment-poll-dialog.service';
|
||||||
|
|
||||||
|
describe('AssignmentPollDialogService', () => {
|
||||||
|
beforeEach(() =>
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: AssignmentPollDialogService = TestBed.get(AssignmentPollDialogService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,22 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material';
|
||||||
|
|
||||||
|
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
|
||||||
|
import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service';
|
||||||
|
import { AssignmentPollDialogComponent } from 'app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component';
|
||||||
|
import { AssignmentPollService } from './assignment-poll.service';
|
||||||
|
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subclassed to provide the right `PollService` and `DialogComponent`
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AssignmentPollDialogService extends BasePollDialogService<ViewAssignmentPoll> {
|
||||||
|
protected dialogComponent = AssignmentPollDialogComponent;
|
||||||
|
|
||||||
|
public constructor(dialog: MatDialog, mapper: CollectionStringMapperService, service: AssignmentPollService) {
|
||||||
|
super(dialog, mapper, service);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { AssignmentPollService } from './assignment-poll.service';
|
||||||
|
|
||||||
|
describe('AssignmentPollService', () => {
|
||||||
|
beforeEach(() =>
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: AssignmentPollService = TestBed.get(AssignmentPollService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,56 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { ConstantsService } from 'app/core/core-services/constants.service';
|
||||||
|
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
|
||||||
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
|
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||||
|
import { Collection } from 'app/shared/models/base/collection';
|
||||||
|
import { MajorityMethod, PercentBase } from 'app/shared/models/poll/base-poll';
|
||||||
|
import { PollService } from 'app/site/polls/services/poll.service';
|
||||||
|
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AssignmentPollService extends PollService {
|
||||||
|
/**
|
||||||
|
* The default percentage base
|
||||||
|
*/
|
||||||
|
public defaultPercentBase: PercentBase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default majority method
|
||||||
|
*/
|
||||||
|
public defaultMajorityMethod: MajorityMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor. Subscribes to the configuration values needed
|
||||||
|
* @param config ConfigService
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
config: ConfigService,
|
||||||
|
constants: ConstantsService,
|
||||||
|
private translate: TranslateService,
|
||||||
|
private pollRepo: AssignmentPollRepositoryService
|
||||||
|
) {
|
||||||
|
super(constants);
|
||||||
|
config
|
||||||
|
.get<PercentBase>('motion_poll_default_100_percent_base')
|
||||||
|
.subscribe(base => (this.defaultPercentBase = base));
|
||||||
|
config
|
||||||
|
.get<MajorityMethod>('motion_poll_default_majority_method')
|
||||||
|
.subscribe(method => (this.defaultMajorityMethod = method));
|
||||||
|
}
|
||||||
|
|
||||||
|
public fillDefaultPollData(poll: Partial<ViewAssignmentPoll> & Collection): void {
|
||||||
|
super.fillDefaultPollData(poll);
|
||||||
|
const length = this.pollRepo.getViewModelList().filter(item => item.assignment_id === poll.assignment_id)
|
||||||
|
.length;
|
||||||
|
|
||||||
|
poll.title = !length ? this.translate.instant('Vote') : `${this.translate.instant('Vote')} (${length + 1})`;
|
||||||
|
poll.pollmethod = AssignmentPollMethods.YN;
|
||||||
|
poll.assignment_id = poll.assignment_id;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { MotionOption } from 'app/shared/models/motions/motion-option';
|
import { MotionOption } from 'app/shared/models/motions/motion-option';
|
||||||
import { BaseViewModel } from '../../base/base-view-model';
|
import { BaseViewModel } from '../../base/base-view-model';
|
||||||
|
import { ViewMotionPoll } from './view-motion-poll';
|
||||||
import { ViewMotionVote } from './view-motion-vote';
|
import { ViewMotionVote } from './view-motion-vote';
|
||||||
|
|
||||||
export class ViewMotionOption extends BaseViewModel<MotionOption> {
|
export class ViewMotionOption extends BaseViewModel<MotionOption> {
|
||||||
@ -12,6 +13,7 @@ export class ViewMotionOption extends BaseViewModel<MotionOption> {
|
|||||||
|
|
||||||
interface TIMotionOptionRelations {
|
interface TIMotionOptionRelations {
|
||||||
votes: ViewMotionVote[];
|
votes: ViewMotionVote[];
|
||||||
|
poll: ViewMotionPoll;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewMotionOption extends MotionOption, TIMotionOptionRelations {}
|
export interface ViewMotionOption extends MotionOption, TIMotionOptionRelations {}
|
||||||
|
@ -1,20 +1,45 @@
|
|||||||
import { MotionPoll, MotionPollWithoutNestedModels } from 'app/shared/models/motions/motion-poll';
|
import { ChartData } from 'app/shared/components/charts/charts.component';
|
||||||
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
|
import { MotionPoll, MotionPollMethods } from 'app/shared/models/motions/motion-poll';
|
||||||
|
import { PollColor } from 'app/shared/models/poll/base-poll';
|
||||||
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||||
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
||||||
import { ViewGroup } from 'app/site/users/models/view-group';
|
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
|
||||||
|
|
||||||
export interface MotionPollTitleInformation {
|
export interface MotionPollTitleInformation {
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ViewMotionPoll extends BaseProjectableViewModel<MotionPoll> implements MotionPollTitleInformation {
|
export const MotionPollMethodsVerbose = {
|
||||||
|
YN: 'Yes/No',
|
||||||
|
YNA: 'Yes/No/Abstain'
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPollTitleInformation {
|
||||||
public static COLLECTIONSTRING = MotionPoll.COLLECTIONSTRING;
|
public static COLLECTIONSTRING = MotionPoll.COLLECTIONSTRING;
|
||||||
protected _collectionString = MotionPoll.COLLECTIONSTRING;
|
protected _collectionString = MotionPoll.COLLECTIONSTRING;
|
||||||
|
|
||||||
public get poll(): MotionPoll {
|
public readonly pollClassType: 'assignment' | 'motion' = 'motion';
|
||||||
return this._model;
|
|
||||||
|
public generateChartData(): ChartData {
|
||||||
|
const fields = ['yes', 'no'];
|
||||||
|
if (this.pollmethod === MotionPollMethods.YNA) {
|
||||||
|
fields.push('abstain');
|
||||||
|
}
|
||||||
|
const data: ChartData = fields.map(key => ({
|
||||||
|
label: key.toUpperCase(),
|
||||||
|
data: [this.options[0][key]],
|
||||||
|
backgroundColor: PollColor[key],
|
||||||
|
hoverBackgroundColor: PollColor[key]
|
||||||
|
}));
|
||||||
|
|
||||||
|
data.push({
|
||||||
|
label: 'Votes invalid',
|
||||||
|
data: [this.votesinvalid],
|
||||||
|
backgroundColor: PollColor.votesinvalid,
|
||||||
|
hoverBackgroundColor: PollColor.votesinvalid
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSlide(): ProjectorElementBuildDeskriptor {
|
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||||
@ -29,12 +54,12 @@ export class ViewMotionPoll extends BaseProjectableViewModel<MotionPoll> impleme
|
|||||||
getDialogTitle: this.getTitle
|
getDialogTitle: this.getTitle
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get pollmethodVerbose(): string {
|
||||||
|
return MotionPollMethodsVerbose[this.pollmethod];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TIMotionPollRelations {
|
export interface ViewMotionPoll extends MotionPoll {
|
||||||
options: ViewMotionOption[];
|
options: ViewMotionOption[];
|
||||||
voted: ViewUser[];
|
|
||||||
groups: ViewGroup[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewMotionPoll extends MotionPollWithoutNestedModels, TIMotionPollRelations {}
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { MotionVote } from 'app/shared/models/motions/motion-vote';
|
import { MotionVote } from 'app/shared/models/motions/motion-vote';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { BaseViewModel } from '../../base/base-view-model';
|
import { BaseViewModel } from '../../base/base-view-model';
|
||||||
|
import { ViewMotionOption } from './view-motion-option';
|
||||||
|
|
||||||
export class ViewMotionVote extends BaseViewModel<MotionVote> {
|
export class ViewMotionVote extends BaseViewModel<MotionVote> {
|
||||||
public get vote(): MotionVote {
|
public get vote(): MotionVote {
|
||||||
@ -12,6 +13,7 @@ export class ViewMotionVote extends BaseViewModel<MotionVote> {
|
|||||||
|
|
||||||
interface TIMotionVoteRelations {
|
interface TIMotionVoteRelations {
|
||||||
user?: ViewUser;
|
user?: ViewUser;
|
||||||
|
option: ViewMotionOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewMotionVote extends MotionVote, TIMotionVoteRelations {}
|
export interface ViewMotionVote extends MotionVote, TIMotionVoteRelations {}
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
<os-list-view-table
|
<os-list-view-table
|
||||||
[repo]="motionRepo"
|
[listObservableProvider]="motionRepo"
|
||||||
[sortService]="amendmentSortService"
|
[sortService]="amendmentSortService"
|
||||||
[filterService]="amendmentFilterService"
|
[filterService]="amendmentFilterService"
|
||||||
[columns]="tableColumnDefinition"
|
[columns]="tableColumnDefinition"
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
<os-list-view-table
|
<os-list-view-table
|
||||||
[repo]="repo"
|
|
||||||
[vScrollFixed]="64"
|
[vScrollFixed]="64"
|
||||||
|
[listObservableProvider]="repo"
|
||||||
[allowProjector]="false"
|
[allowProjector]="false"
|
||||||
[showListOfSpeakers]="false"
|
[showListOfSpeakers]="false"
|
||||||
[columns]="tableColumnDefinition"
|
[columns]="tableColumnDefinition"
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
<os-list-view-table
|
<os-list-view-table
|
||||||
[repo]="motionRepo"
|
[listObservableProvider]="motionRepo"
|
||||||
[filterService]="filterService"
|
[filterService]="filterService"
|
||||||
[columns]="tableColumnDefinition"
|
[columns]="tableColumnDefinition"
|
||||||
[restricted]="restrictedColumns"
|
[restricted]="restrictedColumns"
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
|
|
||||||
<os-list-view-table
|
<os-list-view-table
|
||||||
class="block-list"
|
class="block-list"
|
||||||
[repo]="repo"
|
|
||||||
[vScrollFixed]="64"
|
[vScrollFixed]="64"
|
||||||
|
[listObservableProvider]="repo"
|
||||||
[showFilterBar]="true"
|
[showFilterBar]="true"
|
||||||
[columns]="tableColumnDefinition"
|
[columns]="tableColumnDefinition"
|
||||||
[sortService]="sortService"
|
[sortService]="sortService"
|
||||||
|
@ -459,15 +459,17 @@
|
|||||||
|
|
||||||
<!-- motion polls -->
|
<!-- motion polls -->
|
||||||
<div *ngIf="!editMotion" class="spacer-top-20 spacer-bottom-20">
|
<div *ngIf="!editMotion" class="spacer-top-20 spacer-bottom-20">
|
||||||
<div class="create-poll-button" *ngIf="perms.isAllowed('createpoll', motion)">
|
<div class="mat-card create-poll-button" *ngIf="perms.isAllowed('createpoll', motion)">
|
||||||
<button mat-button (click)="createPoll()">
|
<button mat-button (click)="openDialog()">
|
||||||
<mat-icon class="main-nav-color">poll</mat-icon>
|
<mat-icon class="main-nav-color">poll</mat-icon>
|
||||||
<span translate>New vote</span>
|
<span translate>New poll</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<mat-accordion>
|
<os-motion-poll
|
||||||
<os-motion-poll-preview *ngFor="let poll of motion.polls" [poll]="poll"></os-motion-poll-preview>
|
*ngFor="let poll of motion.polls; trackBy: trackByIndex"
|
||||||
</mat-accordion>
|
[poll]="poll"
|
||||||
|
></os-motion-poll>
|
||||||
|
<!-- <os-motion-poll-manager [motion]="motion"></os-motion-poll-manager> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -4,6 +4,15 @@ span {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.create-poll-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 0px !important;
|
||||||
|
button {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.extra-controls-slot {
|
.extra-controls-slot {
|
||||||
div {
|
div {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
@ -207,13 +216,6 @@ span {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-poll-button {
|
|
||||||
margin-top: 10px;
|
|
||||||
button {
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-chip-list-stacked {
|
.mat-chip-list-stacked {
|
||||||
.mat-chip {
|
.mat-chip {
|
||||||
margin: 4px 4px 4px 4px;
|
margin: 4px 4px 4px 4px;
|
||||||
|
@ -7,8 +7,8 @@ import { MotionCommentsComponent } from '../motion-comments/motion-comments.comp
|
|||||||
import { MotionDetailDiffComponent } from '../motion-detail-diff/motion-detail-diff.component';
|
import { MotionDetailDiffComponent } from '../motion-detail-diff/motion-detail-diff.component';
|
||||||
import { MotionDetailOriginalChangeRecommendationsComponent } from '../motion-detail-original-change-recommendations/motion-detail-original-change-recommendations.component';
|
import { MotionDetailOriginalChangeRecommendationsComponent } from '../motion-detail-original-change-recommendations/motion-detail-original-change-recommendations.component';
|
||||||
import { MotionDetailComponent } from './motion-detail.component';
|
import { MotionDetailComponent } from './motion-detail.component';
|
||||||
import { MotionPollPreviewComponent } from '../motion-poll/motion-poll-preview/motion-poll-preview.component';
|
import { MotionPollVoteComponent } from '../../../motion-poll/motion-poll-vote/motion-poll-vote.component';
|
||||||
import { MotionPollComponent } from '../motion-poll/motion-poll.component';
|
import { MotionPollComponent } from '../../../motion-poll/motion-poll/motion-poll.component';
|
||||||
import { PersonalNoteComponent } from '../personal-note/personal-note.component';
|
import { PersonalNoteComponent } from '../personal-note/personal-note.component';
|
||||||
|
|
||||||
describe('MotionDetailComponent', () => {
|
describe('MotionDetailComponent', () => {
|
||||||
@ -26,7 +26,7 @@ describe('MotionDetailComponent', () => {
|
|||||||
MotionPollComponent,
|
MotionPollComponent,
|
||||||
MotionDetailOriginalChangeRecommendationsComponent,
|
MotionDetailOriginalChangeRecommendationsComponent,
|
||||||
MotionDetailDiffComponent,
|
MotionDetailDiffComponent,
|
||||||
MotionPollPreviewComponent
|
MotionPollVoteComponent
|
||||||
]
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
@ -47,6 +47,7 @@ import { ViewCreateMotion } from 'app/site/motions/models/view-create-motion';
|
|||||||
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
||||||
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
|
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
|
||||||
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation';
|
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation';
|
||||||
|
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||||
import { ViewStatuteParagraph } from 'app/site/motions/models/view-statute-paragraph';
|
import { ViewStatuteParagraph } from 'app/site/motions/models/view-statute-paragraph';
|
||||||
import { ViewWorkflow } from 'app/site/motions/models/view-workflow';
|
import { ViewWorkflow } from 'app/site/motions/models/view-workflow';
|
||||||
import { MotionEditNotification } from 'app/site/motions/motion-edit-notification';
|
import { MotionEditNotification } from 'app/site/motions/motion-edit-notification';
|
||||||
@ -62,6 +63,7 @@ import { AmendmentSortListService } from 'app/site/motions/services/amendment-so
|
|||||||
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
|
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
|
||||||
import { MotionFilterListService } from 'app/site/motions/services/motion-filter-list.service';
|
import { MotionFilterListService } from 'app/site/motions/services/motion-filter-list.service';
|
||||||
import { MotionPdfExportService } from 'app/site/motions/services/motion-pdf-export.service';
|
import { MotionPdfExportService } from 'app/site/motions/services/motion-pdf-export.service';
|
||||||
|
import { MotionPollDialogService } from 'app/site/motions/services/motion-poll-dialog.service';
|
||||||
import { MotionSortListService } from 'app/site/motions/services/motion-sort-list.service';
|
import { MotionSortListService } from 'app/site/motions/services/motion-sort-list.service';
|
||||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
@ -464,7 +466,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
private amendmentSortService: AmendmentSortListService,
|
private amendmentSortService: AmendmentSortListService,
|
||||||
private motionFilterService: MotionFilterListService,
|
private motionFilterService: MotionFilterListService,
|
||||||
private amendmentFilterService: AmendmentFilterListService,
|
private amendmentFilterService: AmendmentFilterListService,
|
||||||
private cd: ChangeDetectorRef
|
private cd: ChangeDetectorRef,
|
||||||
|
private pollDialog: MotionPollDialogService
|
||||||
) {
|
) {
|
||||||
super(title, translate, matSnackBar);
|
super(title, translate, matSnackBar);
|
||||||
}
|
}
|
||||||
@ -1378,13 +1381,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
window.open(attachment.url);
|
window.open(attachment.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for creating a poll
|
|
||||||
*/
|
|
||||||
public createPoll(): void {
|
|
||||||
this.router.navigate(['motions', 'polls', 'new'], { queryParams: { parent: this.motion.id || null } });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a recommendation can be followed. Checks for permissions and additionally if a recommentadion is present
|
* Check if a recommendation can be followed. Checks for permissions and additionally if a recommentadion is present
|
||||||
*/
|
*/
|
||||||
@ -1568,12 +1564,12 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
* Function to prevent automatically closing the window/tab,
|
* Function to prevent automatically closing the window/tab,
|
||||||
* if the user is editing a motion.
|
* if the user is editing a motion.
|
||||||
*
|
*
|
||||||
* @param $event The event object from 'onUnbeforeUnload'.
|
* @param event The event object from 'onUnbeforeUnload'.
|
||||||
*/
|
*/
|
||||||
@HostListener('window:beforeunload', ['$event'])
|
@HostListener('window:beforeunload', ['$event'])
|
||||||
public stopClosing($event: Event): void {
|
public stopClosing(event: Event): void {
|
||||||
if (this.editMotion) {
|
if (this.editMotion) {
|
||||||
$event.returnValue = null;
|
event.returnValue = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1628,4 +1624,10 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
public detectChanges(): void {
|
public detectChanges(): void {
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public openDialog(poll?: ViewMotionPoll): void {
|
||||||
|
this.pollDialog.openDialog(
|
||||||
|
poll ? poll : { collectionString: ViewMotionPoll.COLLECTIONSTRING, motion_id: this.motion.id }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
<h2 translate>Voting result</h2>
|
|
||||||
<div class="meta-text">
|
|
||||||
<span translate>Special values</span>:<br />
|
|
||||||
<mat-chip>-1</mat-chip> =
|
|
||||||
<span translate>majority</span><br />
|
|
||||||
<mat-chip color="accent">-2</mat-chip> =
|
|
||||||
<span translate>undocumented</span>
|
|
||||||
</div>
|
|
||||||
<div *ngFor="let key of pollKeys">
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>{{ getLabel(key) | translate }}</mat-label>
|
|
||||||
<input type="number" matInput [(ngModel)]="data[key]" />
|
|
||||||
<!-- TODO mark required fields -->
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
<div class="submit-buttons">
|
|
||||||
<button mat-button (click)="submit()">{{ 'Save' | translate }}</button>
|
|
||||||
<button mat-button (click)="cancel()">{{ 'Cancel' | translate }}</button>
|
|
||||||
</div>
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user