Adds the chart and dialog for analog voting

This commit is contained in:
GabrielMeyer 2019-11-12 18:30:26 +01:00 committed by FinnStutzenstein
parent 72ff1b1f09
commit 96aa3b0084
185 changed files with 6060 additions and 2430 deletions

View File

@ -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"
}
}

View File

@ -1,3 +1,4 @@
.content {
flex: 1;
height: 100vh;
}

View File

@ -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
});
}
/**

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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];

View File

@ -0,0 +1,5 @@
import { Observable } from 'rxjs';
export interface HasViewModelListObservable<V> {
getViewModelListObservable(): Observable<V[]>;
}

View File

@ -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();
});
});

View File

@ -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');
};
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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();
});
});

View File

@ -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');
};
}

View File

@ -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));
}
}

View File

@ -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
}
];

View File

@ -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.
*

View 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();
});
});

View 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);
}
}
}

View File

@ -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) {

View 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);
}
}
}
}

View File

@ -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();
}));
});

View File

@ -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 '';
}
}
}

View File

@ -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();
});
});

View 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;
}
}
}

View 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();
});
});

View 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];
}
}
}

View File

@ -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 || []);
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
});
});

View 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));
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
});
});

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -0,0 +1,4 @@
.charts-wrapper {
position: relative;
display: block;
}

View File

@ -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();
});
});

View 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 }]
});
}
}

View File

@ -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>

View File

@ -0,0 +1,10 @@
.check-input--container {
display: flex;
align-items: center;
justify-content: space-between;
& > * {
flex: 1;
padding: 0 5px;
}
}

View File

@ -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();
});
});

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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))

View File

@ -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 {}

View File

@ -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'];

View File

@ -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 {}

View File

@ -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'];
}

View File

@ -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 {}

View File

@ -14,7 +14,7 @@
</os-head-bar>
<os-list-view-table
[repo]="repo"
[listObservableProvider]="repo"
[vScrollFixed]="64"
[filterService]="filterService"
[columns]="tableColumnDefinition"

View File

@ -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({

View File

@ -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: [

View File

@ -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 {}

View File

@ -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,112 +134,79 @@
</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 -->
<h3 translate>Candidates</h3>
<!-- Candidate List -->
<div>
<div
class="candidates-list"
*ngIf="assignment && assignment.assignment_related_users && assignment.assignment_related_users.length > 0"
>
<os-sorting-list
[input]="assignment.assignment_related_users"
[live]="true"
[count]="true"
[enable]="hasPerms('manage')"
(sortEvent)="onSortingChange($event)"
>
<!-- implicit item references into the component using ng-template slot -->
<ng-template let-item>
<span *ngIf="hasPerms('addOthers')">
<button
mat-icon-button
matTooltip="{{ 'Remove candidate' | translate }}"
*osPerms="'assignments.can_manage'"
(click)="removeUser(item)"
>
<mat-icon>clear</mat-icon>
</button>
</span>
</ng-template>
</os-sorting-list>
</div>
<div class="add-candidates">
<!-- Search for candidates -->
<div class="search-field" *ngIf="hasPerms('addOthers')">
<form
*ngIf="hasPerms('addOthers') && filteredCandidates && filteredCandidates.value.length > 0"
[formGroup]="candidatesForm"
<mat-card class="os-card">
<ng-container *ngIf="assignment && !assignment.isFinished">
<h3 translate>Candidates</h3>
<div>
<div
class="candidates-list"
*ngIf="assignment && assignment.assignment_related_users && assignment.assignment_related_users.length > 0"
>
<mat-form-field>
<os-search-value-selector
class="search-bar"
formControlName="userId"
[multiple]="false"
placeholder="{{ 'Select a new candidate' | translate }}"
[inputListValues]="filteredCandidates"
></os-search-value-selector>
</mat-form-field>
</form>
</div>
<os-sorting-list
[input]="assignment.assignment_related_users"
[live]="true"
[count]="true"
[enable]="hasPerms('addOthers')"
(sortEvent)="onSortingChange($event)"
>
<!-- implicit item references into the component using ng-template slot -->
<ng-template let-item>
<span *ngIf="hasPerms('addOthers')">
<button
mat-icon-button
matTooltip="{{ 'Remove candidate' | translate }}"
(click)="removeUser(item)"
>
<mat-icon>clear</mat-icon>
</button>
</span>
</ng-template>
</os-sorting-list>
</div>
<!-- Add me and remove me if OP has correct permission -->
<div *ngIf="assignment && hasPerms('addSelf') && assignment.candidates">
<div>
<button mat-button color="accent" (click)="addSelf()" *ngIf="!isSelfCandidate">
<mat-icon>add</mat-icon>
<span translate>Add me</span>
</button>
<button mat-button color="accent" (click)="removeSelf()" *ngIf="isSelfCandidate">
<mat-icon>remove</mat-icon>
<span translate>Remove me</span>
</button>
<div class="add-candidates">
<!-- Search for candidates -->
<div class="search-field" *ngIf="hasPerms('addOthers')">
<form
*ngIf="hasPerms('addOthers') && filteredCandidates && filteredCandidates.value.length > 0"
[formGroup]="candidatesForm"
>
<mat-form-field>
<os-search-value-selector
class="search-bar"
formControlName="userId"
[multiple]="false"
placeholder="{{ 'Select a new candidate' | translate }}"
[inputListValues]="filteredCandidates"
></os-search-value-selector>
</mat-form-field>
</form>
</div>
<!-- Add me and remove me if OP has correct permission -->
<div *ngIf="assignment && hasPerms('addSelf') && assignment.candidates">
<div>
<button mat-button color="accent" (click)="addSelf()" *ngIf="!isSelfCandidate">
<mat-icon>add</mat-icon>
<span translate>Add me</span>
</button>
<button mat-button color="accent" (click)="removeSelf()" *ngIf="isSelfCandidate">
<mat-icon>remove</mat-icon>
<span translate>Remove me</span>
</button>
</div>
</div>
</div>
</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 -->

View File

@ -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();
}));

View File

@ -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
});
}
/**

View File

@ -20,7 +20,7 @@
</os-head-bar>
<os-list-view-table
[repo]="repo"
[listObservableProvider]="repo"
[filterService]="filterService"
[sortService]="sortService"
[columns]="tableColumnDefinition"

View File

@ -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';

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -1,30 +1,52 @@
<h2 translate>Voting result</h2>
<div class="meta-text">
<span translate>Special values</span>:<br />
<mat-chip>-1</mat-chip>&nbsp;=&nbsp; <span translate>majority</span>&nbsp;
<mat-chip color="accent">-2</mat-chip>&nbsp;=&nbsp;
<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>
<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 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 *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>

View File

@ -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%;
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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();
});
});

View File

@ -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];
}
}

View File

@ -3,7 +3,7 @@
<!-- Buttons -->
<button
mat-icon-button
*osPerms="'assignments.can_manage'; &quot;core.can_manage_projector&quot;"
*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>

View File

@ -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;

View File

@ -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();
}));

View File

@ -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
*

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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();
});
});

View File

@ -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;
}
}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -25,7 +25,7 @@
</os-head-bar>
<os-list-view-table
[repo]="motionRepo"
[listObservableProvider]="motionRepo"
[sortService]="amendmentSortService"
[filterService]="amendmentFilterService"
[columns]="tableColumnDefinition"

View File

@ -13,8 +13,8 @@
</os-head-bar>
<os-list-view-table
[repo]="repo"
[vScrollFixed]="64"
[listObservableProvider]="repo"
[allowProjector]="false"
[showListOfSpeakers]="false"
[columns]="tableColumnDefinition"

View File

@ -44,7 +44,7 @@
</os-head-bar>
<os-list-view-table
[repo]="motionRepo"
[listObservableProvider]="motionRepo"
[filterService]="filterService"
[columns]="tableColumnDefinition"
[restricted]="restrictedColumns"

View File

@ -5,8 +5,8 @@
<os-list-view-table
class="block-list"
[repo]="repo"
[vScrollFixed]="64"
[listObservableProvider]="repo"
[showFilterBar]="true"
[columns]="tableColumnDefinition"
[sortService]="sortService"

View File

@ -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>

View File

@ -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;

View File

@ -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();
}));

View File

@ -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 }
);
}
}

View File

@ -1,20 +0,0 @@
<h2 translate>Voting result</h2>
<div class="meta-text">
<span translate>Special values</span>:<br />
<mat-chip>-1</mat-chip>&nbsp;=&nbsp;
<span translate>majority</span><br />
<mat-chip color="accent">-2</mat-chip>&nbsp;=&nbsp;
<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