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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "~8.2.4",
|
||||
"@angular/animations": "^8.2.14",
|
||||
"@angular/cdk": "~8.1.4",
|
||||
"@angular/cdk-experimental": "~8.1.4",
|
||||
"@angular/common": "~8.2.4",
|
||||
"@angular/compiler": "~8.2.4",
|
||||
"@angular/core": "~8.2.4",
|
||||
"@angular/forms": "~8.2.4",
|
||||
"@angular/common": "^8.2.14",
|
||||
"@angular/compiler": "^8.2.14",
|
||||
"@angular/core": "^8.2.14",
|
||||
"@angular/forms": "^8.2.14",
|
||||
"@angular/material": "~8.1.4",
|
||||
"@angular/material-moment-adapter": "~8.1.4",
|
||||
"@angular/platform-browser": "~8.2.4",
|
||||
"@angular/platform-browser-dynamic": "~8.2.4",
|
||||
"@angular/pwa": "^0.803.1",
|
||||
"@angular/router": "~8.2.4",
|
||||
"@angular/service-worker": "~8.2.4",
|
||||
"@ngx-pwa/local-storage": "~8.2.1",
|
||||
"@angular/platform-browser": "^8.2.14",
|
||||
"@angular/platform-browser-dynamic": "^8.2.14",
|
||||
"@angular/pwa": "^0.803.23",
|
||||
"@angular/router": "^8.2.14",
|
||||
"@angular/service-worker": "^8.2.14",
|
||||
"@ngx-pwa/local-storage": "^8.2.4",
|
||||
"@ngx-translate/core": "~11.0.1",
|
||||
"@ngx-translate/http-loader": "^4.0.0",
|
||||
"@pebula/ngrid": "1.0.0-rc.16",
|
||||
"@pebula/ngrid-material": "1.0.0-rc.16",
|
||||
"@pebula/utils": "1.0.2",
|
||||
"@tinymce/tinymce-angular": "^3.2.0",
|
||||
"acorn": "^7.0.0",
|
||||
"core-js": "^3.2.1",
|
||||
"css-element-queries": "^1.2.1",
|
||||
"@tinymce/tinymce-angular": "^3.3.1",
|
||||
"acorn": "^7.1.0",
|
||||
"chart.js": "^2.9.2",
|
||||
"core-js": "^3.6.4",
|
||||
"css-element-queries": "^1.2.3",
|
||||
"exceljs": "1.15.0",
|
||||
"file-saver": "^2.0.2",
|
||||
"hammerjs": "^2.0.8",
|
||||
"lz4js": "^0.2.0",
|
||||
"material-icon-font": "git+https://github.com/petergng/materialIconFont.git",
|
||||
"moment": "^2.24.0",
|
||||
"ng2-charts": "^2.3.0",
|
||||
"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-material-timepicker": "^4.0.2",
|
||||
"ngx-papaparse": "^4.0.2",
|
||||
"pdfmake": "^0.1.58",
|
||||
"po2json": "^1.0.0-alpha",
|
||||
"rxjs": "^6.5.2",
|
||||
"tinymce": "^5.0.14",
|
||||
"pdfmake": "^0.1.63",
|
||||
"po2json": "^1.0.0-beta-2",
|
||||
"rxjs": "^6.5.4",
|
||||
"tinymce": "^5.1.5",
|
||||
"tslib": "^1.10.0",
|
||||
"uuid": "^3.3.2",
|
||||
"uuid": "^3.3.3",
|
||||
"zone.js": "~0.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.803.2",
|
||||
"@angular/cli": "~8.3.2",
|
||||
"@angular/compiler-cli": "~8.2.4",
|
||||
"@angular/language-service": "~8.2.4",
|
||||
"@angular-devkit/build-angular": "^0.803.23",
|
||||
"@angular/cli": "^8.3.23",
|
||||
"@angular/compiler-cli": "^8.2.14",
|
||||
"@angular/language-service": "^8.2.14",
|
||||
"@biesbjerg/ngx-translate-extract": "^3.0.5",
|
||||
"@compodoc/compodoc": "^1.1.8",
|
||||
"@types/jasmine": "^3.3.9",
|
||||
"@types/jasminewd2": "^2.0.6",
|
||||
"@types/node": "~12.7.2",
|
||||
"@types/yargs": "^13.0.0",
|
||||
"codelyzer": "^5.0.1",
|
||||
"husky": "^3.0.4",
|
||||
"@compodoc/compodoc": "^1.1.11",
|
||||
"@types/jasmine": "^3.5.0",
|
||||
"@types/jasminewd2": "^2.0.8",
|
||||
"@types/node": "^12.7.12",
|
||||
"@types/yargs": "^13.0.5",
|
||||
"codelyzer": "^5.2.1",
|
||||
"husky": "^3.1.0",
|
||||
"jasmine-core": "~3.4.0",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "^4.1.0",
|
||||
"karma": "^4.4.1",
|
||||
"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-html-reporter": "^1.4.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.1",
|
||||
"npm-license-crawler": "^0.2.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^1.19.1",
|
||||
"protractor": "^5.4.2",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"source-map-explorer": "^2.0.1",
|
||||
"source-map-explorer": "^2.2.2",
|
||||
"ts-node": "~8.3.0",
|
||||
"tslint": "~5.19.0",
|
||||
"tsutils": "3.17.1",
|
||||
"typescript": "~3.5.3",
|
||||
"webpack-bundle-analyzer": "^3.3.2"
|
||||
"webpack-bundle-analyzer": "^3.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
.content {
|
||||
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 { ServertimeService } from './core/core-services/servertime.service';
|
||||
import { ThemeService } from './core/ui-services/theme.service';
|
||||
import { VotingBannerService } from './core/ui-services/voting-banner.service';
|
||||
|
||||
declare global {
|
||||
/**
|
||||
@ -25,6 +26,8 @@ declare global {
|
||||
*/
|
||||
interface Array<T> {
|
||||
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.
|
||||
prioritizeService: PrioritizeService,
|
||||
pingService: PingService,
|
||||
routingState: RoutingStateService
|
||||
routingState: RoutingStateService,
|
||||
votingBannerService: VotingBannerService // needed for initialisation
|
||||
) {
|
||||
// manually add the supported languages
|
||||
translate.addLangs(['en', 'de', 'cs', 'ru']);
|
||||
@ -92,7 +96,7 @@ export class AppComponent {
|
||||
|
||||
// change default JS functions
|
||||
this.overloadArrayToString();
|
||||
this.overloadFlatMap();
|
||||
this.overloadArrayFunctions();
|
||||
this.overloadModulo();
|
||||
|
||||
// Wait until the App reaches a stable state.
|
||||
@ -138,8 +142,7 @@ export class AppComponent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an implementation of flatMap.
|
||||
* TODO: Remove once flatMap made its way into official JS/TS (ES 2019?)
|
||||
* Adds an implementation of flatMap and intersect.
|
||||
*/
|
||||
private overloadFlatMap(): void {
|
||||
Object.defineProperty(Array.prototype, 'flatMap', {
|
||||
@ -150,6 +153,34 @@ export class AppComponent {
|
||||
},
|
||||
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 { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
import { BannerDefinition, BannerService } from '../ui-services/banner.service';
|
||||
|
||||
/**
|
||||
* This service handles everything connected with being offline.
|
||||
*
|
||||
@ -16,6 +19,16 @@ export class OfflineService {
|
||||
* BehaviorSubject to receive further status values.
|
||||
*/
|
||||
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
|
||||
@ -33,7 +46,7 @@ export class OfflineService {
|
||||
if (!this.offline.getValue()) {
|
||||
console.log('offline because whoami failed.');
|
||||
}
|
||||
this.offline.next(true);
|
||||
this.goOffline();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,7 +56,15 @@ export class OfflineService {
|
||||
if (!this.offline.getValue()) {
|
||||
console.log('offline because connection lost.');
|
||||
}
|
||||
this.goOffline();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to set offline status
|
||||
*/
|
||||
private goOffline(): void {
|
||||
this.offline.next(true);
|
||||
this.banner.addBanner(this.bannerDefinition);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -51,5 +72,6 @@ export class OfflineService {
|
||||
*/
|
||||
public goOnline(): void {
|
||||
this.offline.next(false);
|
||||
this.banner.removeBanner(this.bannerDefinition);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
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
|
||||
@ -14,6 +15,9 @@ export class OpenSlidesStatusService {
|
||||
* in History mode, saves the history point.
|
||||
*/
|
||||
private history: History = null;
|
||||
private bannerDefinition: BannerDefinition = {
|
||||
type: 'history'
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns, if OpenSlides is in the history mode.
|
||||
@ -27,7 +31,7 @@ export class OpenSlidesStatusService {
|
||||
/**
|
||||
* Ctor, does nothing.
|
||||
*/
|
||||
public constructor() {}
|
||||
public constructor(private banner: BannerService) {}
|
||||
|
||||
/**
|
||||
* Calls the getLocaleString function of the history object, if present.
|
||||
@ -44,6 +48,7 @@ export class OpenSlidesStatusService {
|
||||
*/
|
||||
public enterHistoryMode(history: History): void {
|
||||
this.history = history;
|
||||
this.banner.addBanner(this.bannerDefinition);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -51,5 +56,6 @@ export class OpenSlidesStatusService {
|
||||
*/
|
||||
public leaveHistoryMode(): void {
|
||||
this.history = null;
|
||||
this.banner.removeBanner(this.bannerDefinition);
|
||||
}
|
||||
}
|
||||
|
@ -187,12 +187,24 @@ export class RelationManagerService {
|
||||
const _model: M = target.getModel();
|
||||
const relation = typeof property === 'string' ? relationsByKey[property] : null;
|
||||
|
||||
// try to find a getter for property
|
||||
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 getter was found in prototype chain, bind it with this proxy for right `this` access
|
||||
result = descriptor.get.bind(viewModel)();
|
||||
} else {
|
||||
result = target[property];
|
||||
// console.log(property, target);
|
||||
}
|
||||
} else if (property in _model) {
|
||||
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 { 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 { 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 { VotingService } from 'app/core/ui-services/voting.service';
|
||||
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 { 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 { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { BaseRepository, NestedModelDescriptors } from '../base-repository';
|
||||
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||
import { DataStoreService } from '../../core-services/data-store.service';
|
||||
|
||||
@ -29,36 +30,35 @@ const AssignmentPollRelations: RelationDefinition[] = [
|
||||
ownIdKey: 'voted_id',
|
||||
ownKey: 'voted',
|
||||
foreignViewModel: ViewUser
|
||||
},
|
||||
{
|
||||
type: 'O2M',
|
||||
ownIdKey: 'options_id',
|
||||
ownKey: 'options',
|
||||
foreignViewModel: ViewAssignmentOption
|
||||
},
|
||||
{
|
||||
type: 'M2O',
|
||||
ownIdKey: 'assignment_id',
|
||||
ownKey: 'assignment',
|
||||
foreignViewModel: ViewAssignment
|
||||
}
|
||||
];
|
||||
|
||||
const AssignmentPollNestedModelDescriptors: NestedModelDescriptors = {
|
||||
'assignments/assignment-poll': [
|
||||
{
|
||||
ownKey: 'options',
|
||||
foreignViewModel: ViewAssignmentOption,
|
||||
foreignModel: AssignmentOption,
|
||||
order: 'weight',
|
||||
relationDefinitionsByKey: {
|
||||
user: {
|
||||
type: 'M2O',
|
||||
ownIdKey: 'user_id',
|
||||
ownKey: 'user',
|
||||
foreignViewModel: ViewUser
|
||||
},
|
||||
votes: {
|
||||
type: 'O2M',
|
||||
foreignIdKey: 'option_id',
|
||||
ownKey: 'votes',
|
||||
foreignViewModel: ViewAssignmentVote
|
||||
}
|
||||
},
|
||||
titles: {
|
||||
getTitle: (viewOption: ViewAssignmentOption) => (viewOption.user ? viewOption.user.getTitle() : '')
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
export interface AssignmentAnalogVoteData {
|
||||
options: {
|
||||
[key: number]: {
|
||||
Y: number;
|
||||
N?: number;
|
||||
A?: number;
|
||||
};
|
||||
};
|
||||
votesvalid?: number;
|
||||
votesinvalid?: number;
|
||||
votescast?: number;
|
||||
global_no?: number;
|
||||
global_abstain?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository Service for Assignments.
|
||||
@ -68,7 +68,7 @@ const AssignmentPollNestedModelDescriptors: NestedModelDescriptors = {
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AssignmentPollRepositoryService extends BaseRepository<
|
||||
export class AssignmentPollRepositoryService extends BasePollRepositoryService<
|
||||
ViewAssignmentPoll,
|
||||
AssignmentPoll,
|
||||
AssignmentPollTitleInformation
|
||||
@ -89,7 +89,9 @@ export class AssignmentPollRepositoryService extends BaseRepository<
|
||||
mapperService: CollectionStringMapperService,
|
||||
viewModelStoreService: ViewModelStoreService,
|
||||
translate: TranslateService,
|
||||
relationManager: RelationManagerService
|
||||
relationManager: RelationManagerService,
|
||||
votingService: VotingService,
|
||||
http: HttpService
|
||||
) {
|
||||
super(
|
||||
DS,
|
||||
@ -100,7 +102,9 @@ export class AssignmentPollRepositoryService extends BaseRepository<
|
||||
relationManager,
|
||||
AssignmentPoll,
|
||||
AssignmentPollRelations,
|
||||
AssignmentPollNestedModelDescriptors
|
||||
{},
|
||||
votingService,
|
||||
http
|
||||
);
|
||||
}
|
||||
|
||||
@ -111,4 +115,8 @@ export class AssignmentPollRepositoryService extends BaseRepository<
|
||||
public getVerboseName = (plural: boolean = false) => {
|
||||
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 { RelationDefinition } from 'app/core/definitions/relations';
|
||||
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 { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { BaseRepository } from '../base-repository';
|
||||
@ -19,6 +20,12 @@ const AssignmentVoteRelations: RelationDefinition[] = [
|
||||
ownIdKey: 'user_id',
|
||||
ownKey: 'user',
|
||||
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) => {
|
||||
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 { DataSendService } from '../core-services/data-send.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 { OnAfterAppsLoaded } from '../definitions/on-after-apps-loaded';
|
||||
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>
|
||||
implements OnAfterAppsLoaded, Collection {
|
||||
implements OnAfterAppsLoaded, Collection, HasViewModelListObservable<V> {
|
||||
/**
|
||||
* 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> } = {};
|
||||
|
||||
/**
|
||||
* Observable subject for the whole list. These entries are unsorted an not piped through
|
||||
* autodTime. Just use this internally.
|
||||
* Observable subject for the whole list. These entries are unsorted and not piped through
|
||||
* auditTime. Just use this internally.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
protected createViewModelWithTitles(model: M): V {
|
||||
@ -269,7 +270,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
||||
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;
|
||||
|
||||
|
@ -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 { 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 { 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 { VotingService } from 'app/core/ui-services/voting.service';
|
||||
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
|
||||
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
||||
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 { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { BaseRepository, NestedModelDescriptors } from '../base-repository';
|
||||
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||
import { DataStoreService } from '../../core-services/data-store.service';
|
||||
|
||||
@ -29,30 +29,15 @@ const MotionPollRelations: RelationDefinition[] = [
|
||||
ownIdKey: 'voted_id',
|
||||
ownKey: 'voted',
|
||||
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.
|
||||
*
|
||||
@ -61,7 +46,7 @@ const MotionPollNestedModelDescriptors: NestedModelDescriptors = {
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MotionPollRepositoryService extends BaseRepository<
|
||||
export class MotionPollRepositoryService extends BasePollRepositoryService<
|
||||
ViewMotionPoll,
|
||||
MotionPoll,
|
||||
MotionPollTitleInformation
|
||||
@ -72,7 +57,9 @@ export class MotionPollRepositoryService extends BaseRepository<
|
||||
mapperService: CollectionStringMapperService,
|
||||
viewModelStoreService: ViewModelStoreService,
|
||||
translate: TranslateService,
|
||||
relationManager: RelationManagerService
|
||||
relationManager: RelationManagerService,
|
||||
votingService: VotingService,
|
||||
http: HttpService
|
||||
) {
|
||||
super(
|
||||
DS,
|
||||
@ -83,7 +70,9 @@ export class MotionPollRepositoryService extends BaseRepository<
|
||||
relationManager,
|
||||
MotionPoll,
|
||||
MotionPollRelations,
|
||||
MotionPollNestedModelDescriptors
|
||||
{},
|
||||
votingService,
|
||||
http
|
||||
);
|
||||
}
|
||||
|
||||
@ -94,4 +83,8 @@ export class MotionPollRepositoryService extends BaseRepository<
|
||||
public getVerboseName = (plural: boolean = false) => {
|
||||
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 { RelationDefinition } from 'app/core/definitions/relations';
|
||||
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 { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { BaseRepository } from '../base-repository';
|
||||
@ -19,6 +20,12 @@ const MotionVoteRelations: RelationDefinition[] = [
|
||||
ownIdKey: 'user_id',
|
||||
ownKey: 'user',
|
||||
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');
|
||||
};
|
||||
|
||||
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.
|
||||
*
|
||||
|
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) {
|
||||
return true;
|
||||
}
|
||||
} else if (typeof item[filter.property] === 'function') {
|
||||
return item[filter.property]() === option.condition;
|
||||
} else if (item[filter.property] === option.condition) {
|
||||
return true;
|
||||
} 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 {
|
||||
this.contentForm = this.fb.control([]);
|
||||
}
|
||||
protected updateForm(value: ViewMediafile[]): void {
|
||||
this.contentForm.setValue(value);
|
||||
protected updateForm(value: ViewMediafile[] | null): void {
|
||||
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">
|
||||
<os-search-value-selector
|
||||
formControlName="list"
|
||||
[fullWidth]="true"
|
||||
[inputListValues]="searchList"
|
||||
[placeholder]="searchListLabel"
|
||||
></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 { 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 { BaseSortListService } from 'app/core/ui-services/base-sort-list.service';
|
||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||
@ -63,7 +63,7 @@ export interface ColumnRestriction {
|
||||
* @example
|
||||
* ```html
|
||||
* <os-list-view-table
|
||||
* [repo]="motionRepo"
|
||||
* [listObservableProvider]="motionRepo"
|
||||
* [filterService]="filterService"
|
||||
* [sortService]="sortService"
|
||||
* [columns]="motionColumnDefinition"
|
||||
@ -99,7 +99,7 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
|
||||
* The required repository
|
||||
*/
|
||||
@Input()
|
||||
public repo: BaseRepository<V, M, any>;
|
||||
public listObservableProvider: HasViewModelListObservable<V>;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* be a FilterListService extendingFilterListService.
|
||||
* be a FilterListService extending FilterListService.
|
||||
*/
|
||||
@Input()
|
||||
public filterService: BaseFilterListService<V>;
|
||||
@ -322,15 +322,6 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
|
||||
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
|
||||
* "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
|
||||
*/
|
||||
private getListObservable(): void {
|
||||
if (this.repo && this.viewModelListObservable) {
|
||||
if (this.listObservableProvider) {
|
||||
const listObservable = this.listObservableProvider.getViewModelListObservable();
|
||||
if (this.filterService && this.sortService) {
|
||||
// filtering and sorting
|
||||
this.filterService.initFilters(this.viewModelListObservable);
|
||||
this.filterService.initFilters(listObservable);
|
||||
this.sortService.initSorting(this.filterService.outputObservable);
|
||||
this.dataListObservable = this.sortService.outputObservable;
|
||||
} else if (this.filterService) {
|
||||
// only filter service
|
||||
this.filterService.initFilters(this.viewModelListObservable);
|
||||
this.filterService.initFilters(listObservable);
|
||||
this.dataListObservable = this.filterService.outputObservable;
|
||||
} else if (this.sortService) {
|
||||
// only sorting
|
||||
this.sortService.initSorting(this.viewModelListObservable);
|
||||
this.sortService.initSorting(listObservable);
|
||||
this.dataListObservable = this.sortService.outputObservable;
|
||||
} else {
|
||||
// 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>
|
||||
<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">
|
||||
<mat-option [value]="null">
|
||||
<mat-option>
|
||||
{{ noneTitle | translate }}
|
||||
</mat-option>
|
||||
<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 {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
Input,
|
||||
Optional,
|
||||
Self
|
||||
Self,
|
||||
ViewChild,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { FormBuilder, FormControl, NgControl } from '@angular/forms';
|
||||
import { MatFormFieldControl } from '@angular/material';
|
||||
@ -43,9 +44,13 @@ import { Selectable } from '../selectable';
|
||||
templateUrl: './search-value-selector.component.html',
|
||||
styleUrls: ['./search-value-selector.component.scss'],
|
||||
providers: [{ provide: MatFormFieldControl, useExisting: SearchValueSelectorComponent }],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
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
|
||||
*/
|
||||
@ -59,13 +64,10 @@ export class SearchValueSelectorComponent extends BaseFormControlComponent<Selec
|
||||
public includeNone = false;
|
||||
|
||||
@Input()
|
||||
public noneTitle = '–';
|
||||
public showChips = true;
|
||||
|
||||
/**
|
||||
* Boolean, whether the component should be rendered with full width.
|
||||
*/
|
||||
@Input()
|
||||
public fullWidth = false;
|
||||
public noneTitle = '–';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
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 get width(): string {
|
||||
return this.chipPlaceholder ? `${this.chipPlaceholder.nativeElement.clientWidth - 16}px` : '100%';
|
||||
}
|
||||
|
||||
/**
|
||||
* All items
|
||||
*/
|
||||
@ -104,7 +116,6 @@ export class SearchValueSelectorComponent extends BaseFormControlComponent<Selec
|
||||
*/
|
||||
public constructor(
|
||||
protected translate: TranslateService,
|
||||
cd: ChangeDetectorRef,
|
||||
fb: FormBuilder,
|
||||
@Optional() @Self() public ngControl: NgControl,
|
||||
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 {
|
||||
if ((event.target as Element).tagName.toLowerCase() !== 'select') {
|
||||
// this.element.nativeElement.querySelector('select').focus();
|
||||
@ -155,7 +175,6 @@ export class SearchValueSelectorComponent extends BaseFormControlComponent<Selec
|
||||
}
|
||||
|
||||
protected updateForm(value: Selectable[] | null): void {
|
||||
const nextValue = value;
|
||||
this.contentForm.setValue(nextValue);
|
||||
this.contentForm.setValue(value);
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,10 @@
|
||||
import { AssignmentOption } from './assignment-option';
|
||||
import { BasePoll, BasePollWithoutNestedModels } from '../poll/base-poll';
|
||||
import { BasePoll } from '../poll/base-poll';
|
||||
|
||||
export enum AssignmentPollmethods {
|
||||
'yn' = 'yn',
|
||||
'yna' = 'yna',
|
||||
'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;
|
||||
export enum AssignmentPollMethods {
|
||||
YN = 'YN',
|
||||
YNA = 'YNA',
|
||||
Votes = 'votes'
|
||||
}
|
||||
|
||||
/**
|
||||
@ -24,9 +15,15 @@ export class AssignmentPoll extends BasePoll<AssignmentPoll, AssignmentOption> {
|
||||
public static COLLECTIONSTRING = 'assignments/assignment-poll';
|
||||
|
||||
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) {
|
||||
super(AssignmentPoll.COLLECTIONSTRING, input);
|
||||
}
|
||||
}
|
||||
export interface AssignmentPoll extends AssignmentPollWithoutNestedModels {}
|
||||
|
@ -109,7 +109,7 @@ export abstract class BaseFormControlComponent<T> extends MatFormFieldControl<T>
|
||||
|
||||
this.subscriptions.push(
|
||||
fm.monitor(element.nativeElement, true).subscribe(origin => {
|
||||
this.focused = !!origin;
|
||||
this.focused = origin === 'mouse' || origin === 'touch';
|
||||
this.stateChanges.next();
|
||||
}),
|
||||
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';
|
||||
|
||||
export enum MotionPollMethods {
|
||||
YN = 'YN',
|
||||
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.
|
||||
@ -22,13 +13,10 @@ export class MotionPoll extends BasePoll<MotionPoll, MotionOption> {
|
||||
public static COLLECTIONSTRING = 'motions/motion-poll';
|
||||
|
||||
public id: number;
|
||||
public motion_id: number;
|
||||
public pollmethod: MotionPollMethods;
|
||||
|
||||
public constructor(input?: any) {
|
||||
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 no: number;
|
||||
public abstain: number;
|
||||
public poll_id: number;
|
||||
|
||||
protected getDecimalFields(): (keyof BaseOption<T>)[] {
|
||||
return ['yes', 'no', 'abstain'];
|
||||
|
@ -1,6 +1,15 @@
|
||||
import { BaseDecimalModel } from '../base/base-decimal-model';
|
||||
import { BaseOption } from './base-option';
|
||||
|
||||
export enum PollColor {
|
||||
yes = '#9fd773',
|
||||
no = '#cc6c5b',
|
||||
abstain = '#a6a6a6',
|
||||
votesvalid = '#e2e2e2',
|
||||
votesinvalid = '#e2e2e2',
|
||||
votescast = '#e2e2e2'
|
||||
}
|
||||
|
||||
export enum PollState {
|
||||
Created = 1,
|
||||
Started,
|
||||
@ -8,41 +17,21 @@ export enum PollState {
|
||||
Published
|
||||
}
|
||||
|
||||
export const PollStateVerbose = {
|
||||
1: 'Created',
|
||||
2: 'Started',
|
||||
3: 'Finished',
|
||||
4: 'Published'
|
||||
};
|
||||
|
||||
export enum PollType {
|
||||
Analog = 'analog',
|
||||
Named = 'named',
|
||||
Pseudoanonymous = 'pseudoanonymous'
|
||||
}
|
||||
|
||||
export const PollTypeVerbose = {
|
||||
analog: 'Analog',
|
||||
named: 'Named',
|
||||
pseudoanonymous: 'Pseudoanonymous'
|
||||
};
|
||||
|
||||
export enum PercentBase {
|
||||
YN = 'YN',
|
||||
YNA = 'YNA',
|
||||
Valid = 'valid',
|
||||
Votes = 'votes',
|
||||
Cast = 'cast',
|
||||
Disabled = 'disabled'
|
||||
}
|
||||
|
||||
export const PercentBaseVerbose = {
|
||||
YN: 'Yes/No',
|
||||
YNA: 'Yes/No/Abstain',
|
||||
valid: 'Valid votes',
|
||||
cast: 'Casted votes',
|
||||
disabled: 'Disabled'
|
||||
};
|
||||
|
||||
export enum MajorityMethod {
|
||||
Simple = 'simple',
|
||||
TwoThirds = 'two_thirds',
|
||||
@ -50,47 +39,20 @@ export enum MajorityMethod {
|
||||
Disabled = 'disabled'
|
||||
}
|
||||
|
||||
export const MajorityMethodVerbose = {
|
||||
simple: 'Simple',
|
||||
two_thirds: 'Two Thirds',
|
||||
three_quarters: 'Three Quarters',
|
||||
disabled: 'Disabled'
|
||||
};
|
||||
|
||||
export interface BasePollWithoutNestedModels {
|
||||
state: PollState;
|
||||
type: PollType;
|
||||
title: string;
|
||||
votesvalid: number;
|
||||
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[];
|
||||
export abstract class BasePoll<T = any, O extends BaseOption<any> = any> extends BaseDecimalModel<T> {
|
||||
public state: PollState;
|
||||
public type: PollType;
|
||||
public title: string;
|
||||
public votesvalid: number;
|
||||
public votesinvalid: number;
|
||||
public votescast: number;
|
||||
public groups_id: number[];
|
||||
public voted_id: number[];
|
||||
public majority_method: MajorityMethod;
|
||||
public onehundred_percent_base: PercentBase;
|
||||
public user_has_voted: boolean;
|
||||
|
||||
protected getDecimalFields(): (keyof BasePoll<T, O>)[] {
|
||||
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';
|
||||
|
||||
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> {
|
||||
public weight: number;
|
||||
public value: 'Y' | 'N' | 'A';
|
||||
public value: VoteValue;
|
||||
public option_id: number;
|
||||
public user_id?: number;
|
||||
|
||||
public get valueVerbose(): string {
|
||||
return VoteValueVerbose[this.value];
|
||||
}
|
||||
|
||||
protected getDecimalFields(): (keyof BaseVote<T>)[] {
|
||||
return ['weight'];
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatSliderModule } from '@angular/material/slider';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
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!!
|
||||
import { NgxMaterialTimepickerModule } from 'ngx-material-timepicker';
|
||||
import { ChartsModule } from 'ng2-charts';
|
||||
|
||||
// components
|
||||
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 { TrustPipe } from './pipes/trust.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.
|
||||
@ -158,6 +168,7 @@ import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
||||
MatStepperModule,
|
||||
MatTabsModule,
|
||||
MatSliderModule,
|
||||
MatSlideToggleModule,
|
||||
MatDividerModule,
|
||||
DragDropModule,
|
||||
OpenSlidesTranslateModule.forChild(),
|
||||
@ -171,7 +182,8 @@ import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
||||
PblNgridMaterialModule,
|
||||
PblNgridTargetEventsModule,
|
||||
PdfViewerModule,
|
||||
NgxMaterialTimepickerModule
|
||||
NgxMaterialTimepickerModule,
|
||||
ChartsModule
|
||||
],
|
||||
exports: [
|
||||
FormsModule,
|
||||
@ -205,6 +217,7 @@ import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
||||
MatButtonToggleModule,
|
||||
MatStepperModule,
|
||||
MatSliderModule,
|
||||
MatSlideToggleModule,
|
||||
MatDividerModule,
|
||||
DragDropModule,
|
||||
NgxMatSelectSearchModule,
|
||||
@ -257,8 +270,16 @@ import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
||||
OverlayComponent,
|
||||
PreviewComponent,
|
||||
NgxMaterialTimepickerModule,
|
||||
ChartsModule,
|
||||
TrustPipe,
|
||||
LocalizedDatePipe
|
||||
LocalizedDatePipe,
|
||||
BreadcrumbComponent,
|
||||
ChartsComponent,
|
||||
CheckInputComponent,
|
||||
BannerComponent,
|
||||
PollFormComponent,
|
||||
MotionPollDialogComponent,
|
||||
AssignmentPollDialogComponent
|
||||
],
|
||||
declarations: [
|
||||
PermsDirective,
|
||||
@ -305,7 +326,14 @@ import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
||||
PreviewComponent,
|
||||
HeightResizingDirective,
|
||||
TrustPipe,
|
||||
LocalizedDatePipe
|
||||
LocalizedDatePipe,
|
||||
BreadcrumbComponent,
|
||||
ChartsComponent,
|
||||
CheckInputComponent,
|
||||
BannerComponent,
|
||||
PollFormComponent,
|
||||
MotionPollDialogComponent,
|
||||
AssignmentPollDialogComponent
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@ -330,7 +358,9 @@ import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
||||
ChoiceDialogComponent,
|
||||
ProjectionDialogComponent,
|
||||
ProgressSnackBarComponent,
|
||||
SuperSearchComponent
|
||||
SuperSearchComponent,
|
||||
MotionPollDialogComponent,
|
||||
AssignmentPollDialogComponent
|
||||
]
|
||||
})
|
||||
export class SharedModule {}
|
||||
|
@ -14,7 +14,7 @@
|
||||
</os-head-bar>
|
||||
|
||||
<os-list-view-table
|
||||
[repo]="repo"
|
||||
[listObservableProvider]="repo"
|
||||
[vScrollFixed]="64"
|
||||
[filterService]="filterService"
|
||||
[columns]="tableColumnDefinition"
|
||||
|
@ -3,11 +3,13 @@ import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { AssignmentDetailComponent } from './components/assignment-detail/assignment-detail.component';
|
||||
import { AssignmentListComponent } from './components/assignment-list/assignment-list.component';
|
||||
import { AssignmentPollDetailComponent } from './components/assignment-poll-detail/assignment-poll-detail.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: AssignmentListComponent, pathMatch: 'full' },
|
||||
{ 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({
|
||||
|
@ -1,11 +1,14 @@
|
||||
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 { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-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 { AssignmentVote } from 'app/shared/models/assignments/assignment-vote';
|
||||
import { Assignment } from '../../shared/models/assignments/assignment';
|
||||
import { ViewAssignment } from './models/view-assignment';
|
||||
import { ViewAssignmentOption } from './models/view-assignment-option';
|
||||
import { ViewAssignmentPoll } from './models/view-assignment-poll';
|
||||
import { ViewAssignmentVote } from './models/view-assignment-vote';
|
||||
|
||||
@ -27,6 +30,11 @@ export const AssignmentsAppConfig: AppConfig = {
|
||||
model: AssignmentVote,
|
||||
viewModel: ViewAssignmentVote,
|
||||
repository: AssignmentVoteRepositoryService
|
||||
},
|
||||
{
|
||||
model: AssignmentOption,
|
||||
viewModel: ViewAssignmentOption,
|
||||
repository: AssignmentOptionRepositoryService
|
||||
}
|
||||
],
|
||||
mainMenuEntries: [
|
||||
|
@ -3,7 +3,8 @@ import { NgModule } from '@angular/core';
|
||||
|
||||
import { AssignmentDetailComponent } from './components/assignment-detail/assignment-detail.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 { AssignmentsRoutingModule } from './assignments-routing.module';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
@ -11,11 +12,11 @@ import { SharedModule } from '../../shared/shared.module';
|
||||
@NgModule({
|
||||
imports: [CommonModule, AssignmentsRoutingModule, SharedModule],
|
||||
declarations: [
|
||||
AssignmentListComponent,
|
||||
AssignmentDetailComponent,
|
||||
AssignmentListComponent,
|
||||
AssignmentPollComponent,
|
||||
AssignmentPollDialogComponent
|
||||
],
|
||||
entryComponents: [AssignmentPollDialogComponent]
|
||||
AssignmentPollDetailComponent,
|
||||
AssignmentPollVoteComponent
|
||||
]
|
||||
})
|
||||
export class AssignmentsModule {}
|
||||
|
@ -63,8 +63,26 @@
|
||||
<ng-container [ngTemplateOutlet]="assignmentFormTemplate"></ng-container>
|
||||
</div>
|
||||
<div *ngIf="!editAssignment">
|
||||
<!-- assignment meta infos-->
|
||||
<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>
|
||||
|
||||
@ -116,46 +134,10 @@
|
||||
</mat-card>
|
||||
</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>
|
||||
<!-- Candidates -->
|
||||
<mat-card class="os-card">
|
||||
<ng-container *ngIf="assignment && !assignment.isFinished">
|
||||
<h3 translate>Candidates</h3>
|
||||
|
||||
<!-- Candidate List -->
|
||||
<div>
|
||||
<div
|
||||
class="candidates-list"
|
||||
@ -165,7 +147,7 @@
|
||||
[input]="assignment.assignment_related_users"
|
||||
[live]="true"
|
||||
[count]="true"
|
||||
[enable]="hasPerms('manage')"
|
||||
[enable]="hasPerms('addOthers')"
|
||||
(sortEvent)="onSortingChange($event)"
|
||||
>
|
||||
<!-- implicit item references into the component using ng-template slot -->
|
||||
@ -174,7 +156,6 @@
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'Remove candidate' | translate }}"
|
||||
*osPerms="'assignments.can_manage'"
|
||||
(click)="removeUser(item)"
|
||||
>
|
||||
<mat-icon>clear</mat-icon>
|
||||
@ -218,10 +199,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider
|
||||
*ngIf="assignment && assignment.polls && assignment.polls.length"
|
||||
class="candidate-list-separator"
|
||||
></mat-divider>
|
||||
<div class="ballot-button" *ngIf="assignment && hasPerms('createPoll')">
|
||||
<button mat-button (click)="openDialog()">
|
||||
<mat-icon color="primary">poll</mat-icon>
|
||||
<span translate>New ballot</span>
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</mat-card>
|
||||
</ng-template>
|
||||
|
||||
<!-- Form -->
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
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 { E2EImportsModule } from '../../../../../e2e-imports.module';
|
||||
|
||||
@ -11,7 +12,7 @@ describe('AssignmentDetailComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
declarations: [AssignmentDetailComponent, AssignmentPollComponent]
|
||||
declarations: [AssignmentDetailComponent, AssignmentPollComponent, AssignmentPollVoteComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
|
@ -22,7 +22,9 @@ import { LocalPermissionsService } from 'app/site/motions/services/local-permiss
|
||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { AssignmentPdfExportService } from '../../services/assignment-pdf-export.service';
|
||||
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||
import { AssignmentPhases, ViewAssignment } from '../../models/view-assignment';
|
||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
import { ViewAssignmentRelatedUser } from '../../models/view-assignment-related-user';
|
||||
|
||||
/**
|
||||
@ -173,7 +175,8 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
private tagRepo: TagRepositoryService,
|
||||
private promptService: PromptService,
|
||||
private pdfService: AssignmentPdfExportService,
|
||||
private mediafileRepo: MediafileRepositoryService
|
||||
private mediafileRepo: MediafileRepositoryService,
|
||||
private pollDialog: AssignmentPollDialogService
|
||||
) {
|
||||
super(title, translate, matSnackBar);
|
||||
this.subscriptions.push(
|
||||
@ -302,8 +305,12 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
/**
|
||||
* Creates a new Poll
|
||||
*/
|
||||
public async createPoll(): Promise<void> {
|
||||
// await this.repo.createPoll(this.assignment).catch(this.raiseError);
|
||||
public openDialog(): void {
|
||||
this.pollDialog.openDialog({
|
||||
collectionString: ViewAssignmentPoll.COLLECTIONSTRING,
|
||||
assignment_id: this.assignment.id,
|
||||
assignment: this.assignment
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -20,7 +20,7 @@
|
||||
</os-head-bar>
|
||||
|
||||
<os-list-view-table
|
||||
[repo]="repo"
|
||||
[listObservableProvider]="repo"
|
||||
[filterService]="filterService"
|
||||
[sortService]="sortService"
|
||||
[columns]="tableColumnDefinition"
|
||||
|
@ -12,7 +12,7 @@ import { AssignmentRepositoryService } from 'app/core/repositories/assignments/a
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||
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 { AssignmentSortListService } from '../../services/assignment-sort-list.service';
|
||||
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>
|
||||
<div class="meta-text">
|
||||
<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">
|
||||
<os-poll-form [data]="pollData" [pollMethods]="assignmentPollMethods" #pollForm></os-poll-form>
|
||||
<ng-container *ngIf="pollForm.contentForm.get('type').value === 'analog'">
|
||||
<!-- Candidate values -->
|
||||
<div [ngClass]="getGridClass()" *ngFor="let candidate of data.options">
|
||||
<div class="candidate-name">
|
||||
{{ candidate.user.full_name }}
|
||||
<form [formGroup]="dialogVoteForm">
|
||||
<div formGroupName="options">
|
||||
<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 *ngFor="let key of optionPollKeys" class="votes">
|
||||
<mat-form-field>
|
||||
<input type="number"
|
||||
matInput
|
||||
[value]="getValue(key, candidate)"
|
||||
(change)="setValue(key, candidate, $event.target.value)"
|
||||
/>
|
||||
<mat-label> {{ key | translate }}</mat-label>
|
||||
</mat-form-field>
|
||||
|
||||
<div>
|
||||
<div *ngFor="let value of analogPollValues" [formGroupName]="option.user_id">
|
||||
<os-check-input
|
||||
[placeholder]="voteValueVerbose[value] | translate"
|
||||
[checkboxValue]="-1"
|
||||
inputType="number"
|
||||
[checkboxLabel]="'Majority' | translate"
|
||||
[formControlName]="value"
|
||||
></os-check-input>
|
||||
</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 -->
|
||||
<div *ngFor="let sumValue of sumValues" class="sum-value">
|
||||
<!-- <div *ngFor="let sumValue of sumValues" class="sum-value">
|
||||
<mat-form-field>
|
||||
<input
|
||||
type="number"
|
||||
@ -34,9 +56,14 @@
|
||||
/>
|
||||
<mat-label>{{ pollService.getLabel(sumValue) | translate }}</mat-label>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="submit-buttons">
|
||||
<button mat-button (click)="submit()">{{ 'Save' | translate }}</button>
|
||||
<button mat-button (click)="cancel()">{{ 'Cancel' | translate }}</button>
|
||||
</div> -->
|
||||
</ng-container>
|
||||
<mat-divider></mat-divider>
|
||||
<div mat-dialog-actions>
|
||||
<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>
|
||||
|
@ -19,36 +19,12 @@
|
||||
border-bottom: 1px solid grey;
|
||||
}
|
||||
|
||||
.votes-grid-1 {
|
||||
.votes-grid {
|
||||
display: grid;
|
||||
grid-gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
grid-template-columns: auto 60px;
|
||||
align-items: center;
|
||||
.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;
|
||||
align-items: baseline;
|
||||
grid-template-columns: auto max-content;
|
||||
.mat-form-field {
|
||||
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 { 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 { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
|
||||
/**
|
||||
* Vote entries included once for summary (e.g. total votes cast)
|
||||
*/
|
||||
type summaryPollKey = 'votescast' | 'votesvalid' | 'votesinvalid' | 'votesno' | 'votesabstain';
|
||||
type OptionsObject = { user_id: number; user: ViewUser }[];
|
||||
|
||||
/**
|
||||
* 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',
|
||||
styleUrls: ['./assignment-poll-dialog.component.scss']
|
||||
})
|
||||
export class AssignmentPollDialogComponent {
|
||||
export class AssignmentPollDialogComponent extends BasePollDialogComponent implements OnInit {
|
||||
/**
|
||||
* The actual poll data to work on
|
||||
*/
|
||||
@ -27,13 +35,8 @@ export class AssignmentPollDialogComponent {
|
||||
/**
|
||||
* The summary values that will have fields in the dialog
|
||||
*/
|
||||
public get sumValues(): summaryPollKey[] {
|
||||
const generalValues: summaryPollKey[] = ['votesvalid', 'votesinvalid', 'votescast'];
|
||||
if (this.data.pollmethod === 'votes') {
|
||||
return ['votesno', 'votesabstain', ...generalValues];
|
||||
} else {
|
||||
return generalValues;
|
||||
}
|
||||
public get sumValues(): string[] {
|
||||
return ['votesvalid', 'votesinvalid', 'votescast'];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,39 +45,118 @@ export class AssignmentPollDialogComponent {
|
||||
*/
|
||||
public specialValues: [number, string][];
|
||||
|
||||
@ViewChild('pollForm', { static: true })
|
||||
protected pollForm: PollFormComponent;
|
||||
|
||||
/**
|
||||
* vote entries for each option in this component. Is empty if method
|
||||
* 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,
|
||||
* injects the poll itself
|
||||
*/
|
||||
public constructor(
|
||||
public dialogRef: MatDialogRef<AssignmentPollDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: ViewAssignmentPoll
|
||||
private fb: FormBuilder,
|
||||
title: Title,
|
||||
protected translate: TranslateService,
|
||||
matSnackbar: MatSnackBar,
|
||||
public dialogRef: MatDialogRef<BasePollDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public pollData: Partial<ViewAssignmentPoll>
|
||||
) {
|
||||
switch (this.data.pollmethod) {
|
||||
case 'votes':
|
||||
this.optionPollKeys = ['Votes'];
|
||||
break;
|
||||
case 'yn':
|
||||
this.optionPollKeys = ['Yes', 'No'];
|
||||
break;
|
||||
case 'yna':
|
||||
this.optionPollKeys = ['Yes', 'No', 'Abstain'];
|
||||
break;
|
||||
super(title, translate, matSnackbar, dialogRef);
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
// on new poll creation, poll.options does not exist, so we have to build a substitute from the assignment candidates
|
||||
this.options = this.pollData.options
|
||||
? this.pollData.options
|
||||
: this.pollData.assignment.candidates.map(
|
||||
user => ({
|
||||
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
|
||||
* default angular cancelling behavior
|
||||
* Pre-executed method to initialize the dialog-form depending on the poll-method.
|
||||
*/
|
||||
public cancel(): void {
|
||||
this.dialogRef.close();
|
||||
private createDialog(): void {
|
||||
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
|
||||
*/
|
||||
public setSumValue(value: any /*SummaryPollKey*/, weight: string): void {
|
||||
this.data[value] = parseFloat(weight);
|
||||
}
|
||||
|
||||
public getGridClass(): string {
|
||||
return `votes-grid-${this.optionPollKeys.length}`;
|
||||
this.pollData[value] = parseFloat(weight);
|
||||
}
|
||||
}
|
||||
|
@ -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 -->
|
||||
<button
|
||||
mat-icon-button
|
||||
*osPerms="'assignments.can_manage'; "core.can_manage_projector""
|
||||
*osPerms="'assignments.can_manage'; 'core.can_manage_projector'"
|
||||
[matMenuTriggerFor]="pollItemMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
@ -11,21 +11,21 @@
|
||||
</button>
|
||||
<mat-menu #pollItemMenu="matMenu">
|
||||
<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>
|
||||
<span translate>Print ballot paper</span>
|
||||
</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()">
|
||||
<mat-icon>
|
||||
{{ poll.published ? 'visibility_off' : 'visibility' }}
|
||||
</mat-icon>
|
||||
<span *ngIf="!poll.published" translate>Publish</span>
|
||||
<span *ngIf="poll.published" translate>Unpublish</span>
|
||||
</button>
|
||||
</button> -->
|
||||
</div>
|
||||
<div *osPerms="'core.can_manage_projector'">
|
||||
<os-projector-button menuItem="true" [object]="poll"></os-projector-button>
|
||||
@ -40,8 +40,38 @@
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
||||
<div class="poll-main-content" *ngIf="poll.options">
|
||||
<div *ngIf="pollData">
|
||||
<div class="poll-properties">
|
||||
<!-- <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></div>
|
||||
<div><span class="table-view-list-title" translate>Candidates</span></div>
|
||||
@ -78,7 +108,7 @@
|
||||
type="button"
|
||||
mat-icon-button
|
||||
(click)="toggleElected(option)"
|
||||
[disabled]="!canManage || assignment.isFinished"
|
||||
[disabled]="!canManage || poll.assignment.isFinished"
|
||||
disableRipple
|
||||
>
|
||||
<mat-icon
|
||||
@ -88,7 +118,7 @@
|
||||
>check_box</mat-icon
|
||||
>
|
||||
<mat-icon
|
||||
*ngIf="!option.is_elected && canManage && !assignment.isFinished"
|
||||
*ngIf="!option.is_elected && canManage && !poll.assignment.isFinished"
|
||||
class="top-aligned primary"
|
||||
matTooltip="{{ 'Mark as elected' | translate }}"
|
||||
>
|
||||
@ -176,13 +206,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Election Method -->
|
||||
<div *ngIf="canManage" class="spacer-bottom-10">
|
||||
<!-- <div *ngIf="canManage" class="spacer-bottom-10">
|
||||
<h4 translate>Election method</h4>
|
||||
<span>{{ pollMethodName | translate }}</span>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 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-label translate>Hint for ballot paper</mat-label>
|
||||
<input matInput formControlName="description" />
|
||||
@ -190,5 +220,21 @@
|
||||
<button mat-icon-button [disabled]="!dirtyDescription" (click)="onEditDescriptionButton()">
|
||||
<mat-icon inline>check</mat-icon>
|
||||
</button>
|
||||
</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 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -2,6 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { AssignmentPollVoteComponent } from '../assignment-poll-vote/assignment-poll-vote.component';
|
||||
import { AssignmentPollComponent } from './assignment-poll.component';
|
||||
|
||||
describe('AssignmentPollComponent', () => {
|
||||
@ -10,7 +11,7 @@ describe('AssignmentPollComponent', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AssignmentPollComponent],
|
||||
declarations: [AssignmentPollComponent, AssignmentPollVoteComponent],
|
||||
imports: [E2EImportsModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
@ -7,10 +7,10 @@ import { Title } from '@angular/platform-browser';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { CalculablePollKey, MajorityMethod } from 'app/core/ui-services/poll.service';
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { AssignmentPollPdfService } from '../../services/assignment-poll-pdf.service';
|
||||
import { ViewAssignment } from '../../models/view-assignment';
|
||||
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { BasePollComponent } from 'app/site/polls/components/base-poll.component';
|
||||
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||
import { ViewAssignmentOption } from '../../models/view-assignment-option';
|
||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
|
||||
@ -23,30 +23,12 @@ import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
styleUrls: ['./assignment-poll.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class AssignmentPollComponent extends BaseViewComponent 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;
|
||||
|
||||
export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPoll> implements OnInit {
|
||||
/**
|
||||
* Form for updating the poll's description
|
||||
*/
|
||||
public descriptionForm: FormGroup;
|
||||
|
||||
/**
|
||||
* The selected Majority method to display quorum calculations. Will be
|
||||
* set/changed by the user
|
||||
*/
|
||||
public majorityChoice: MajorityMethod | null;
|
||||
|
||||
/**
|
||||
* permission checks.
|
||||
* TODO stub
|
||||
@ -57,93 +39,38 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
||||
return this.operator.hasPerms('assignments.can_manage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the voting options
|
||||
*
|
||||
* @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');
|
||||
public get canSee(): boolean {
|
||||
return this.operator.hasPerms('assignments.can_see');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if the description on the form differs from the poll's description
|
||||
*/
|
||||
public get dirtyDescription(): boolean {
|
||||
// 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 '';
|
||||
}
|
||||
return this.descriptionForm.get('description').value !== this.poll.description;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
matSnackBar: MatSnackBar,
|
||||
translate: TranslateService,
|
||||
dialog: MatDialog,
|
||||
promptService: PromptService,
|
||||
repo: AssignmentPollRepositoryService,
|
||||
pollDialog: AssignmentPollDialogService,
|
||||
private operator: OperatorService,
|
||||
public translate: TranslateService,
|
||||
public dialog: MatDialog,
|
||||
private pdfService: AssignmentPollPdfService
|
||||
private formBuilder: FormBuilder
|
||||
) {
|
||||
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 {
|
||||
/*this.majorityChoice =
|
||||
this.pollService.majorityMethods.find(method => method.value === this.pollService.defaultMajorityMethod) ||
|
||||
null;
|
||||
null;*/
|
||||
this.descriptionForm = this.formBuilder.group({
|
||||
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 {
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { AssignmentOption } from 'app/shared/models/assignments/assignment-option';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { BaseViewModel } from '../../base/base-view-model';
|
||||
import { ViewAssignmentPoll } from './view-assignment-poll';
|
||||
import { ViewAssignmentVote } from './view-assignment-vote';
|
||||
|
||||
export class ViewAssignmentOption extends BaseViewModel<AssignmentOption> {
|
||||
@ -11,9 +12,10 @@ export class ViewAssignmentOption extends BaseViewModel<AssignmentOption> {
|
||||
protected _collectionString = AssignmentOption.COLLECTIONSTRING;
|
||||
}
|
||||
|
||||
interface TIMotionOptionRelations {
|
||||
interface TIAssignmentOptionRelations {
|
||||
votes: ViewAssignmentVote[];
|
||||
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 { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
|
||||
import { ChartData } from 'app/shared/components/charts/charts.component';
|
||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||
import { ViewAssignment } from './view-assignment';
|
||||
import { ViewAssignmentOption } from './view-assignment-option';
|
||||
|
||||
export interface AssignmentPollTitleInformation {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export class ViewAssignmentPoll extends BaseProjectableViewModel<AssignmentPoll>
|
||||
implements AssignmentPollTitleInformation {
|
||||
export const AssignmentPollMethodsVerbose = {
|
||||
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;
|
||||
protected _collectionString = AssignmentPoll.COLLECTIONSTRING;
|
||||
|
||||
public get poll(): AssignmentPoll {
|
||||
return this._model;
|
||||
}
|
||||
public readonly pollClassType: 'assignment' | 'motion' = 'assignment';
|
||||
|
||||
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||
/*return {
|
||||
// TODO: update to new voting system?
|
||||
return {
|
||||
getBasicProjectorElement: options => ({
|
||||
name: 'assignments/assignment-poll',
|
||||
assignment_id: this.assignment_id,
|
||||
@ -27,17 +31,22 @@ export class ViewAssignmentPoll extends BaseProjectableViewModel<AssignmentPoll>
|
||||
getIdentifiers: () => ['name', 'assignment_id', 'poll_id']
|
||||
}),
|
||||
slideOptions: [],
|
||||
projectionDefaultName: 'assignments',
|
||||
projectionDefaultName: 'assignment-poll',
|
||||
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[];
|
||||
voted: ViewUser[];
|
||||
groups: ViewGroup[];
|
||||
assignment: ViewAssignment;
|
||||
}
|
||||
|
||||
export interface ViewAssignmentPoll extends AssignmentPollWithoutNestedModels, TIAssignmentPollRelations {}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { AssignmentVote } from 'app/shared/models/assignments/assignment-vote';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { BaseViewModel } from '../../base/base-view-model';
|
||||
import { ViewAssignmentOption } from './view-assignment-option';
|
||||
|
||||
export class ViewAssignmentVote extends BaseViewModel<AssignmentVote> {
|
||||
public get vote(): AssignmentVote {
|
||||
@ -11,7 +12,8 @@ export class ViewAssignmentVote extends BaseViewModel<AssignmentVote> {
|
||||
}
|
||||
|
||||
interface TIAssignmentVoteRelations {
|
||||
user?: ViewUser;
|
||||
user: ViewUser;
|
||||
option: ViewAssignmentOption;
|
||||
}
|
||||
|
||||
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 { BaseViewModel } from '../../base/base-view-model';
|
||||
import { ViewMotionPoll } from './view-motion-poll';
|
||||
import { ViewMotionVote } from './view-motion-vote';
|
||||
|
||||
export class ViewMotionOption extends BaseViewModel<MotionOption> {
|
||||
@ -12,6 +13,7 @@ export class ViewMotionOption extends BaseViewModel<MotionOption> {
|
||||
|
||||
interface TIMotionOptionRelations {
|
||||
votes: ViewMotionVote[];
|
||||
poll: ViewMotionPoll;
|
||||
}
|
||||
|
||||
export interface ViewMotionOption extends MotionOption, TIMotionOptionRelations {}
|
||||
|
@ -1,20 +1,45 @@
|
||||
import { MotionPoll, MotionPollWithoutNestedModels } from 'app/shared/models/motions/motion-poll';
|
||||
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
|
||||
import { ChartData } from 'app/shared/components/charts/charts.component';
|
||||
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 { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
||||
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||
|
||||
export interface MotionPollTitleInformation {
|
||||
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;
|
||||
protected _collectionString = MotionPoll.COLLECTIONSTRING;
|
||||
|
||||
public get poll(): MotionPoll {
|
||||
return this._model;
|
||||
public readonly pollClassType: 'assignment' | 'motion' = 'motion';
|
||||
|
||||
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 {
|
||||
@ -29,12 +54,12 @@ export class ViewMotionPoll extends BaseProjectableViewModel<MotionPoll> impleme
|
||||
getDialogTitle: this.getTitle
|
||||
};
|
||||
}
|
||||
|
||||
public get pollmethodVerbose(): string {
|
||||
return MotionPollMethodsVerbose[this.pollmethod];
|
||||
}
|
||||
}
|
||||
|
||||
interface TIMotionPollRelations {
|
||||
export interface ViewMotionPoll extends MotionPoll {
|
||||
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 { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { BaseViewModel } from '../../base/base-view-model';
|
||||
import { ViewMotionOption } from './view-motion-option';
|
||||
|
||||
export class ViewMotionVote extends BaseViewModel<MotionVote> {
|
||||
public get vote(): MotionVote {
|
||||
@ -12,6 +13,7 @@ export class ViewMotionVote extends BaseViewModel<MotionVote> {
|
||||
|
||||
interface TIMotionVoteRelations {
|
||||
user?: ViewUser;
|
||||
option: ViewMotionOption;
|
||||
}
|
||||
|
||||
export interface ViewMotionVote extends MotionVote, TIMotionVoteRelations {}
|
||||
|
@ -25,7 +25,7 @@
|
||||
</os-head-bar>
|
||||
|
||||
<os-list-view-table
|
||||
[repo]="motionRepo"
|
||||
[listObservableProvider]="motionRepo"
|
||||
[sortService]="amendmentSortService"
|
||||
[filterService]="amendmentFilterService"
|
||||
[columns]="tableColumnDefinition"
|
||||
|
@ -13,8 +13,8 @@
|
||||
</os-head-bar>
|
||||
|
||||
<os-list-view-table
|
||||
[repo]="repo"
|
||||
[vScrollFixed]="64"
|
||||
[listObservableProvider]="repo"
|
||||
[allowProjector]="false"
|
||||
[showListOfSpeakers]="false"
|
||||
[columns]="tableColumnDefinition"
|
||||
|
@ -44,7 +44,7 @@
|
||||
</os-head-bar>
|
||||
|
||||
<os-list-view-table
|
||||
[repo]="motionRepo"
|
||||
[listObservableProvider]="motionRepo"
|
||||
[filterService]="filterService"
|
||||
[columns]="tableColumnDefinition"
|
||||
[restricted]="restrictedColumns"
|
||||
|
@ -5,8 +5,8 @@
|
||||
|
||||
<os-list-view-table
|
||||
class="block-list"
|
||||
[repo]="repo"
|
||||
[vScrollFixed]="64"
|
||||
[listObservableProvider]="repo"
|
||||
[showFilterBar]="true"
|
||||
[columns]="tableColumnDefinition"
|
||||
[sortService]="sortService"
|
||||
|
@ -459,15 +459,17 @@
|
||||
|
||||
<!-- motion polls -->
|
||||
<div *ngIf="!editMotion" class="spacer-top-20 spacer-bottom-20">
|
||||
<div class="create-poll-button" *ngIf="perms.isAllowed('createpoll', motion)">
|
||||
<button mat-button (click)="createPoll()">
|
||||
<div class="mat-card create-poll-button" *ngIf="perms.isAllowed('createpoll', motion)">
|
||||
<button mat-button (click)="openDialog()">
|
||||
<mat-icon class="main-nav-color">poll</mat-icon>
|
||||
<span translate>New vote</span>
|
||||
<span translate>New poll</span>
|
||||
</button>
|
||||
</div>
|
||||
<mat-accordion>
|
||||
<os-motion-poll-preview *ngFor="let poll of motion.polls" [poll]="poll"></os-motion-poll-preview>
|
||||
</mat-accordion>
|
||||
<os-motion-poll
|
||||
*ngFor="let poll of motion.polls; trackBy: trackByIndex"
|
||||
[poll]="poll"
|
||||
></os-motion-poll>
|
||||
<!-- <os-motion-poll-manager [motion]="motion"></os-motion-poll-manager> -->
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@ -4,6 +4,15 @@ span {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.create-poll-button {
|
||||
margin-top: 10px;
|
||||
padding: 0px !important;
|
||||
button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.extra-controls-slot {
|
||||
div {
|
||||
padding: 0px;
|
||||
@ -207,13 +216,6 @@ span {
|
||||
}
|
||||
}
|
||||
|
||||
.create-poll-button {
|
||||
margin-top: 10px;
|
||||
button {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-chip-list-stacked {
|
||||
.mat-chip {
|
||||
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 { MotionDetailOriginalChangeRecommendationsComponent } from '../motion-detail-original-change-recommendations/motion-detail-original-change-recommendations.component';
|
||||
import { MotionDetailComponent } from './motion-detail.component';
|
||||
import { MotionPollPreviewComponent } from '../motion-poll/motion-poll-preview/motion-poll-preview.component';
|
||||
import { MotionPollComponent } from '../motion-poll/motion-poll.component';
|
||||
import { MotionPollVoteComponent } from '../../../motion-poll/motion-poll-vote/motion-poll-vote.component';
|
||||
import { MotionPollComponent } from '../../../motion-poll/motion-poll/motion-poll.component';
|
||||
import { PersonalNoteComponent } from '../personal-note/personal-note.component';
|
||||
|
||||
describe('MotionDetailComponent', () => {
|
||||
@ -26,7 +26,7 @@ describe('MotionDetailComponent', () => {
|
||||
MotionPollComponent,
|
||||
MotionDetailOriginalChangeRecommendationsComponent,
|
||||
MotionDetailDiffComponent,
|
||||
MotionPollPreviewComponent
|
||||
MotionPollVoteComponent
|
||||
]
|
||||
}).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 { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
|
||||
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 { ViewWorkflow } from 'app/site/motions/models/view-workflow';
|
||||
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 { MotionFilterListService } from 'app/site/motions/services/motion-filter-list.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 { ViewTag } from 'app/site/tags/models/view-tag';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
@ -464,7 +466,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
||||
private amendmentSortService: AmendmentSortListService,
|
||||
private motionFilterService: MotionFilterListService,
|
||||
private amendmentFilterService: AmendmentFilterListService,
|
||||
private cd: ChangeDetectorRef
|
||||
private cd: ChangeDetectorRef,
|
||||
private pollDialog: MotionPollDialogService
|
||||
) {
|
||||
super(title, translate, matSnackBar);
|
||||
}
|
||||
@ -1378,13 +1381,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
||||
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
|
||||
*/
|
||||
@ -1568,12 +1564,12 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
||||
* Function to prevent automatically closing the window/tab,
|
||||
* 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'])
|
||||
public stopClosing($event: Event): void {
|
||||
public stopClosing(event: Event): void {
|
||||
if (this.editMotion) {
|
||||
$event.returnValue = null;
|
||||
event.returnValue = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1628,4 +1624,10 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
||||
public detectChanges(): void {
|
||||
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