Reworked config
- Move config meta data into REST - seperate views for config groups
This commit is contained in:
parent
bcba878e18
commit
cf7a5ce714
@ -2,9 +2,9 @@ import { Injectable } from '@angular/core';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
|
||||
import { ConstantsService } from 'app/core/core-services/constants.service';
|
||||
import { DataSendService } from 'app/core/core-services/data-send.service';
|
||||
import { DataStoreService } from 'app/core/core-services/data-store.service';
|
||||
import { HttpService } from 'app/core/core-services/http.service';
|
||||
@ -13,29 +13,9 @@ import { ViewModelStoreService } from 'app/core/core-services/view-model-store.s
|
||||
import { BaseRepository } from 'app/core/repositories/base-repository';
|
||||
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||
import { Config } from 'app/shared/models/core/config';
|
||||
import { ConfigItem } from 'app/site/config/components/config-list/config-list.component';
|
||||
import { ConfigTitleInformation, ViewConfig } from 'app/site/config/models/view-config';
|
||||
|
||||
/**
|
||||
* Holds a single config item.
|
||||
*/
|
||||
interface ConfigItem {
|
||||
/**
|
||||
* The key of this config variable.
|
||||
*/
|
||||
key: string;
|
||||
|
||||
/**
|
||||
* The actual view config for this variable.
|
||||
*/
|
||||
config: ViewConfig;
|
||||
|
||||
/**
|
||||
* The config variable data given in the constants. This is hold here, so the view
|
||||
* config can be updated with this data.
|
||||
*/
|
||||
data: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a config subgroup. It can only holds items and no further groups.
|
||||
*/
|
||||
@ -48,7 +28,7 @@ interface ConfigSubgroup {
|
||||
/**
|
||||
* All items in this sub group.
|
||||
*/
|
||||
items: ConfigItem[];
|
||||
configs: ViewConfig[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,11 +44,6 @@ export interface ConfigGroup {
|
||||
* A list of subgroups.
|
||||
*/
|
||||
subgroups: ConfigSubgroup[];
|
||||
|
||||
/**
|
||||
* A list of config items that are not in any subgroup.
|
||||
*/
|
||||
items: ConfigItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -82,22 +57,27 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config,
|
||||
/**
|
||||
* Own store for config groups.
|
||||
*/
|
||||
private configs: ConfigGroup[] = null;
|
||||
private configs: ConfigGroup[] | null = null;
|
||||
|
||||
/**
|
||||
* Own subject for config groups.
|
||||
*/
|
||||
protected configListSubject: BehaviorSubject<ConfigGroup[]> = new BehaviorSubject<ConfigGroup[]>(null);
|
||||
private readonly configsSubject: BehaviorSubject<ConfigGroup[]> = new BehaviorSubject<ConfigGroup[]>(null);
|
||||
|
||||
/**
|
||||
* Saves, if we got config variables (the structure) from the server.
|
||||
* Custom observer for the config
|
||||
*/
|
||||
protected gotConfigsVariables = false;
|
||||
public get configsObservable(): Observable<ConfigGroup[]> {
|
||||
return this.configsSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves, if we got first configs via autoupdate or cache.
|
||||
* Gets an observalble for all existing (main) config groups. Just the group names
|
||||
* are given with this observable.
|
||||
*/
|
||||
protected gotFirstUpdate = false;
|
||||
public get availableGroupsOberservable(): Observable<string[]> {
|
||||
return this.configsSubject.pipe(map((groups: ConfigGroup[]) => groups.map(group => group.name)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for ConfigRepositoryService. Requests the constants from the server and creates the config group structure.
|
||||
@ -114,18 +94,15 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config,
|
||||
viewModelStoreService: ViewModelStoreService,
|
||||
translate: TranslateService,
|
||||
relationManager: RelationManagerService,
|
||||
private constantsService: ConstantsService,
|
||||
private http: HttpService
|
||||
) {
|
||||
super(DS, dataSend, mapperService, viewModelStoreService, translate, relationManager, Config);
|
||||
|
||||
this.constantsService.get('ConfigVariables').subscribe(constant => {
|
||||
this.createConfigStructure(constant);
|
||||
this.updateConfigStructure(false, ...Object.values(this.viewModelStore));
|
||||
this.gotConfigsVariables = true;
|
||||
this.checkConfigStructure();
|
||||
this.updateConfigListObservable();
|
||||
});
|
||||
this.setSortFunction((a, b) => a.weight - b.weight);
|
||||
|
||||
this.getViewModelListObservable().subscribe(configs =>
|
||||
this.updateConfigStructure(configs.filter(config => !config.hidden))
|
||||
);
|
||||
}
|
||||
|
||||
public getVerboseName = (plural: boolean = false) => {
|
||||
@ -154,96 +131,50 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config,
|
||||
throw new Error('Config variables cannot be created');
|
||||
}
|
||||
|
||||
public changedModels(ids: number[]): void {
|
||||
super.changedModels(ids);
|
||||
|
||||
ids.forEach(id => {
|
||||
this.updateConfigStructure(false, this.viewModelStore[id]);
|
||||
});
|
||||
this.gotFirstUpdate = true;
|
||||
this.checkConfigStructure();
|
||||
this.updateConfigListObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom observer for the config
|
||||
*/
|
||||
public getConfigListObservable(): Observable<ConfigGroup[]> {
|
||||
return this.configListSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom notification for the observers.
|
||||
*/
|
||||
protected updateConfigListObservable(): void {
|
||||
if (this.configs) {
|
||||
this.configListSubject.next(this.configs);
|
||||
this.configsSubject.next(this.configs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the config structure
|
||||
*/
|
||||
public getConfigStructure(): ConfigGroup[] {
|
||||
return this.configs;
|
||||
public getConfigGroupOberservable(name: string): Observable<ConfigGroup> {
|
||||
return this.configsSubject.pipe(
|
||||
map((groups: ConfigGroup[]) => groups.find(group => group.name.toLowerCase() === name))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the config structure, if we got configs (first data) and the
|
||||
* structure (config variables)
|
||||
*/
|
||||
protected checkConfigStructure(): void {
|
||||
if (this.gotConfigsVariables && this.gotFirstUpdate) {
|
||||
this.updateConfigStructure(true, ...Object.values(this.viewModelStore));
|
||||
}
|
||||
}
|
||||
protected updateConfigStructure(configs: ViewConfig[]): void {
|
||||
const groups: ConfigGroup[] = [];
|
||||
|
||||
/**
|
||||
* With a given (and maybe partially filled) config structure, all given view configs are put into it.
|
||||
* @param check Whether to check, if all given configs are there (according to the config structure).
|
||||
* If check is true and one viewConfig is missing, the user will get an error message.
|
||||
* @param viewConfigs All view configs to put into the structure
|
||||
*/
|
||||
protected updateConfigStructure(check: boolean, ...viewConfigs: ViewConfig[]): void {
|
||||
if (!this.configs) {
|
||||
return;
|
||||
}
|
||||
configs.forEach(config => {
|
||||
if (groups.length === 0 || groups[groups.length - 1].name !== config.group) {
|
||||
groups.push({
|
||||
name: config.group,
|
||||
subgroups: []
|
||||
});
|
||||
}
|
||||
|
||||
// Map the viewConfigs to their keys.
|
||||
const keyConfigMap: { [key: string]: ViewConfig } = {};
|
||||
viewConfigs.forEach(viewConfig => {
|
||||
keyConfigMap[viewConfig.key] = viewConfig;
|
||||
const subgroupsLength = groups[groups.length - 1].subgroups.length;
|
||||
if (
|
||||
subgroupsLength === 0 ||
|
||||
groups[groups.length - 1].subgroups[subgroupsLength - 1].name !== config.subgroup
|
||||
) {
|
||||
groups[groups.length - 1].subgroups.push({
|
||||
name: config.subgroup,
|
||||
configs: []
|
||||
});
|
||||
}
|
||||
groups[groups.length - 1].subgroups[groups[groups.length - 1].subgroups.length - 1].configs.push(config);
|
||||
});
|
||||
|
||||
// traverse through configs structure and replace all given viewConfigs
|
||||
for (const group of this.configs) {
|
||||
for (const subgroup of group.subgroups) {
|
||||
for (const item of subgroup.items) {
|
||||
if (keyConfigMap[item.key]) {
|
||||
keyConfigMap[item.key].setConstantsInfo(item.data);
|
||||
item.config = keyConfigMap[item.key];
|
||||
} else if (check) {
|
||||
throw new Error(
|
||||
`No config variable found for "${item.key}". Please migrate the database or rebuild the servercache.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const item of group.items) {
|
||||
if (keyConfigMap[item.key]) {
|
||||
keyConfigMap[item.key].setConstantsInfo(item.data);
|
||||
item.config = keyConfigMap[item.key];
|
||||
} else if (check) {
|
||||
throw new Error(
|
||||
`No config variable found for "${item.key}". Please migrate the database or rebuild the servercache.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.configsSubject.next(groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a config value.
|
||||
* Saves a config value. The server needs the key instead of the id to fetch the config variable.
|
||||
*/
|
||||
public async update(config: Partial<Config>, viewConfig: ViewConfig): Promise<void> {
|
||||
const updatedConfig = viewConfig.getUpdatedModel(config);
|
||||
@ -251,43 +182,24 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config,
|
||||
}
|
||||
|
||||
/**
|
||||
* initially create the config structure from the given constant.
|
||||
* @param constant
|
||||
* Function to update multiple settings.
|
||||
*
|
||||
* @param configItems An array of `ConfigItem` with the key of the changed setting and the value for that setting.
|
||||
*
|
||||
* @returns Either a promise containing errors or null, if there are no errors.
|
||||
*/
|
||||
private createConfigStructure(constant: any): void {
|
||||
this.configs = [];
|
||||
for (const group of constant) {
|
||||
const _group: ConfigGroup = {
|
||||
name: group.name,
|
||||
subgroups: [],
|
||||
items: []
|
||||
};
|
||||
// The server always sends subgroups. But if it has an empty name, there is no subgroup..
|
||||
for (const subgroup of group.subgroups) {
|
||||
if (subgroup.name) {
|
||||
const _subgroup: ConfigSubgroup = {
|
||||
name: subgroup.name,
|
||||
items: []
|
||||
};
|
||||
for (const item of subgroup.items) {
|
||||
_subgroup.items.push({
|
||||
key: item.key,
|
||||
config: null,
|
||||
data: item
|
||||
});
|
||||
}
|
||||
_group.subgroups.push(_subgroup);
|
||||
} else {
|
||||
for (const item of subgroup.items) {
|
||||
_group.items.push({
|
||||
key: item.key,
|
||||
config: null,
|
||||
data: item
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
this.configs.push(_group);
|
||||
}
|
||||
public async bulkUpdate(configItems: ConfigItem[]): Promise<{ errors: { [key: string]: string } } | null> {
|
||||
return await this.http.post(`/rest/core/config/bulk_update/`, configItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to send a `reset`-poll for every group to the server.
|
||||
*
|
||||
* @param groups The names of the groups, that should be updated.
|
||||
*
|
||||
* @returns The answer of the server.
|
||||
*/
|
||||
public async resetGroups(groups: string[]): Promise<void> {
|
||||
return await this.http.post(`/rest/core/config/reset_groups/`, groups);
|
||||
}
|
||||
}
|
||||
|
@ -1,57 +1,65 @@
|
||||
<mat-card
|
||||
class="block-tile"
|
||||
[style.display]="orientation === 'horizontal' ? 'flex' : 'block'"
|
||||
[ngClass]="{ 'is-square': isSquare, vertical: orientation === 'vertical' }"
|
||||
(click)="onClick($event)"
|
||||
>
|
||||
<div
|
||||
[ngSwitch]="blockType"
|
||||
class="block-node-container"
|
||||
[ngClass]="{ 'no-padding': noPaddingBlock }"
|
||||
*ngIf="showBlockNode"
|
||||
>
|
||||
<div [ngSwitch]="blockType" class="block-node-container">
|
||||
<div *ngSwitchCase="'text'" class="tile-text stretch-to-fill-parent tile-color" [style.border-radius]="orientation === 'horizontal' ? '4px 0 0 4px' : '4px 4px 0 0'">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
{{ block }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div *ngSwitchCase="'image'">
|
||||
<img mat-card-image [src]="block" alt="" />
|
||||
</div>
|
||||
<div *ngSwitchCase="'node'" class="tile-text stretch-to-fill-parent tile-color" [style.border-radius]="orientation === 'horizontal' ? '4px 0 0 4px' : '4px 4px 0 0'">
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="blockNode"
|
||||
[ngTemplateOutletContext]="data"></ng-container>
|
||||
<div
|
||||
#blockNode
|
||||
class="tile-text stretch-to-fill-parent tile-color"
|
||||
[style.border-radius]="orientation === 'horizontal' ? '4px 0 0 4px' : '4px 4px 0 0'"
|
||||
>
|
||||
<ng-container *ngSwitchCase="'text'">
|
||||
{{ block }}
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'node'">
|
||||
<ng-container [ngTemplateOutlet]="blockNodeTemplate" [ngTemplateOutletContext]="data"></ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile-content-node-container">
|
||||
<div class="tile-content-node-container" *ngIf="showContentNode || showActions">
|
||||
<mat-card-content class="tile-content">
|
||||
<mat-card-title class="tile-content-title stretch-to-fill-parent" *ngIf="!only || only === 'title'">
|
||||
{{ title }}
|
||||
</mat-card-title>
|
||||
<mat-card-subtitle class="tile-content-subtitle" *ngIf="subtitle">
|
||||
{{ subtitle }}
|
||||
</mat-card-subtitle>
|
||||
<mat-divider *ngIf="!only"></mat-divider>
|
||||
<div *ngIf="!only || only === 'content'" class="tile-content-extra">
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="contentNode"
|
||||
[ngTemplateOutletContext]="data"></ng-container>
|
||||
</div>
|
||||
<mat-card-actions *ngIf="showActions">
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="actionNode"></ng-container>
|
||||
<ng-container *ngIf="showContentNode">
|
||||
<ng-container *ngIf="!only || only === 'title'">
|
||||
<mat-card-title class="tile-content-title">
|
||||
{{ title }}
|
||||
</mat-card-title>
|
||||
<mat-card-subtitle class="tile-content-subtitle" *ngIf="subtitle">
|
||||
{{ subtitle }}
|
||||
</mat-card-subtitle>
|
||||
</ng-container>
|
||||
<mat-divider *ngIf="!only"></mat-divider>
|
||||
<div
|
||||
*ngIf="!only || only === 'content'"
|
||||
class="tile-content-extra"
|
||||
[ngClass]="{ 'only-content': only === 'content' }"
|
||||
#contentNode
|
||||
>
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="contentNodeTemplate"
|
||||
[ngTemplateOutletContext]="data"
|
||||
></ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<mat-card-actions *ngIf="showActions" #actionNode>
|
||||
<ng-container [ngTemplateOutlet]="actionNodeTemplate"></ng-container>
|
||||
</mat-card-actions>
|
||||
</mat-card-content>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
<ng-template #blockNode>
|
||||
<ng-template #blockNodeTemplate>
|
||||
<ng-content select=".block-node"></ng-content>
|
||||
</ng-template>
|
||||
<ng-template #contentNode>
|
||||
<ng-template #contentNodeTemplate>
|
||||
<ng-content select=".block-content-node"></ng-content>
|
||||
</ng-template>
|
||||
<ng-template #actionNode>
|
||||
<ng-template #actionNodeTemplate>
|
||||
<ng-content select=".block-action-node"></ng-content>
|
||||
</ng-template>
|
||||
|
@ -3,11 +3,34 @@
|
||||
.block-tile {
|
||||
padding: 0;
|
||||
|
||||
&.is-square:not(.vertical) {
|
||||
.block-node-container {
|
||||
padding-bottom: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
&.is-square {
|
||||
padding-bottom: 100%;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.block-node-container {
|
||||
padding-bottom: 50%;
|
||||
}
|
||||
.tile-content-node-container {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.block-node-container {
|
||||
position: relative;
|
||||
padding-bottom: 50%;
|
||||
min-width: 30%;
|
||||
|
||||
&:not(.no-padding) {
|
||||
padding-bottom: 50%;
|
||||
}
|
||||
|
||||
.tile-text {
|
||||
table {
|
||||
height: 100%;
|
||||
@ -20,9 +43,8 @@
|
||||
}
|
||||
|
||||
.tile-content-node-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 8px 16px !important;
|
||||
margin: 8px 16px;
|
||||
|
||||
.tile-content {
|
||||
margin-bottom: 0;
|
||||
@ -47,7 +69,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tile-content-extra {
|
||||
.tile-content-extra:not(.only-content) {
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
|
@ -1,39 +1,24 @@
|
||||
import { Component, ContentChild, Input, TemplateRef } from '@angular/core';
|
||||
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, ViewChild } from '@angular/core';
|
||||
|
||||
import { TileComponent } from '../tile/tile.component';
|
||||
|
||||
/**
|
||||
* Enumeration to define if the content is only text or a node.
|
||||
*/
|
||||
export enum ContentType {
|
||||
text = 'text',
|
||||
node = 'node'
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumeration to define of which the big block is.
|
||||
*/
|
||||
export enum BlockType {
|
||||
text = 'text',
|
||||
node = 'node',
|
||||
picture = 'picture'
|
||||
node = 'node'
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells, whether to align the block and content next to each other or one below the other.
|
||||
*/
|
||||
export enum Orientation {
|
||||
horizontal = 'horizontal',
|
||||
vertical = 'vertical'
|
||||
}
|
||||
export type Orientation = 'horizontal' | 'vertical';
|
||||
|
||||
/**
|
||||
* Tells, if the tile should only display the content or the title in the content part.
|
||||
*/
|
||||
export enum ShowOnly {
|
||||
title = 'title',
|
||||
content = 'content'
|
||||
}
|
||||
export type ShowOnly = 'title' | 'content' | null;
|
||||
|
||||
/**
|
||||
* Class, that extends the `tile.component`.
|
||||
@ -45,30 +30,42 @@ export enum ShowOnly {
|
||||
templateUrl: './block-tile.component.html',
|
||||
styleUrls: ['./block-tile.component.scss']
|
||||
})
|
||||
export class BlockTileComponent extends TileComponent {
|
||||
export class BlockTileComponent extends TileComponent implements AfterViewInit {
|
||||
/**
|
||||
* Reference to the content of the content part.
|
||||
*/
|
||||
@ContentChild(TemplateRef, { static: true })
|
||||
public contentNode: TemplateRef<any>;
|
||||
@ViewChild('contentNode', { static: false })
|
||||
public contentNode: ElementRef<HTMLElement>;
|
||||
|
||||
/**
|
||||
* Reference to the block part, if it is a node.
|
||||
*/
|
||||
@ContentChild(TemplateRef, { static: true })
|
||||
public blockNode: TemplateRef<any>;
|
||||
@ViewChild('blockNode', { static: false })
|
||||
public blockNode: ElementRef<HTMLElement>;
|
||||
|
||||
/**
|
||||
* Reference to the action buttons in the content part, if used.
|
||||
*/
|
||||
@ContentChild(TemplateRef, { static: true })
|
||||
public actionNode: TemplateRef<any>;
|
||||
@ViewChild('actionNode', { static: false })
|
||||
public actionNode: ElementRef<HTMLElement>;
|
||||
|
||||
/**
|
||||
* Defines the type of the primary block.
|
||||
*/
|
||||
@Input()
|
||||
public blockType: BlockType;
|
||||
public blockType: BlockType = BlockType.node;
|
||||
|
||||
/**
|
||||
* Manually remove the padding, the block is surrounded by.
|
||||
*/
|
||||
@Input()
|
||||
public noPaddingBlock = false;
|
||||
|
||||
/**
|
||||
* Renders the block-tile as a square.
|
||||
*/
|
||||
@Input()
|
||||
public isSquare = false;
|
||||
|
||||
/**
|
||||
* Input for the primary block content.
|
||||
@ -77,12 +74,6 @@ export class BlockTileComponent extends TileComponent {
|
||||
@Input()
|
||||
public block: string;
|
||||
|
||||
/**
|
||||
* Defines the type of the content.
|
||||
*/
|
||||
@Input()
|
||||
public contentType: ContentType;
|
||||
|
||||
/**
|
||||
* The title in the content part.
|
||||
*/
|
||||
@ -100,17 +91,130 @@ export class BlockTileComponent extends TileComponent {
|
||||
* whether the block part should be displayed above the content or next to it.
|
||||
*/
|
||||
@Input()
|
||||
public orientation: Orientation;
|
||||
public orientation: Orientation = 'horizontal';
|
||||
|
||||
/**
|
||||
* Tells, whether the tile should display only one of `Title` or `Content` in the content part.
|
||||
*/
|
||||
@Input()
|
||||
public only: ShowOnly;
|
||||
public only: ShowOnly = null;
|
||||
|
||||
/**
|
||||
* Boolean, if the block-part of the tile is shown or not.
|
||||
*/
|
||||
private _showBlockNode: boolean;
|
||||
|
||||
/**
|
||||
* To decide, whether the block-node should always be shown.
|
||||
* Otherwise this will decide automatically.
|
||||
*
|
||||
* @param show Whether the block-part should be shown or not.
|
||||
*/
|
||||
@Input()
|
||||
public set showBlockNode(show: boolean) {
|
||||
this._showBlockNode = show;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns A boolean whether the block-part of the tile should be shown.
|
||||
* If this is not set manually, it will return `true` for the first time to check,
|
||||
* if this part contains any nodes.
|
||||
*/
|
||||
public get showBlockNode(): boolean {
|
||||
return typeof this._showBlockNode === 'undefined' ? true : this._showBlockNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Boolean, if the content-part of the tile is shown or not.
|
||||
*/
|
||||
private _showContentNode: boolean;
|
||||
|
||||
/**
|
||||
* To decide, whether the content-node should always be shown.
|
||||
* Otherwise this will decide automatically.
|
||||
*
|
||||
* @param show Whether the content-part should be shown or not.
|
||||
*/
|
||||
@Input()
|
||||
public set showContentNode(show: boolean) {
|
||||
this._showContentNode = show;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns A boolean whether the content-part of the tile should be shown.
|
||||
* If this is not set manually, it will return `true` for the first time to check,
|
||||
* if this part contains any nodes.
|
||||
*/
|
||||
public get showContentNode(): boolean {
|
||||
return typeof this._showContentNode === 'undefined'
|
||||
? true
|
||||
: this._showContentNode || !!this.only || !!this.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Boolean, if the part with actions of the tile is shown or not.
|
||||
*/
|
||||
private _showActionNode: boolean;
|
||||
|
||||
/**
|
||||
* Boolean, whether to show action buttons in the content part.
|
||||
*
|
||||
* @param show Whether the action-part should be shown or not.
|
||||
*/
|
||||
@Input()
|
||||
public showActions: boolean;
|
||||
public set showActions(show: boolean) {
|
||||
this._showActionNode = show;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns A boolean whether the action-part of the tile should be shown.
|
||||
* If this is not set manually, it will return `true` for the first time to check,
|
||||
* if this part contains any nodes.
|
||||
*/
|
||||
public get showActions(): boolean {
|
||||
return typeof this._showActionNode === 'undefined' ? true : this._showActionNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default constructor.
|
||||
*
|
||||
* @param cd ChangeDetectorRef
|
||||
*/
|
||||
public constructor(private cd: ChangeDetectorRef) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* AfterViewInit.
|
||||
*
|
||||
* Here it will check, if the visibility of the three parts of the tile is set manually.
|
||||
* If not, it will check, if the parts contain nodes to display or not.
|
||||
*/
|
||||
public ngAfterViewInit(): void {
|
||||
if (typeof this._showBlockNode === 'undefined') {
|
||||
this.showBlockNode = this.checkForContent(this.blockNode);
|
||||
}
|
||||
if (typeof this._showContentNode === 'undefined') {
|
||||
this.showContentNode = this.checkForContent(this.contentNode);
|
||||
}
|
||||
if (typeof this._showActionNode === 'undefined') {
|
||||
this.showActions = this.checkForContent(this.actionNode);
|
||||
}
|
||||
this.cd.detectChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to test, whether the child-nodes of the given parent-element
|
||||
* are a comment or not. If not, then the parent-element contains content to display.
|
||||
*
|
||||
* @param parentElement The element whose child-nodes are tested.
|
||||
*
|
||||
* @returns `True`, if there is at least one node other than a comment.
|
||||
*/
|
||||
private checkForContent(parentElement: ElementRef<HTMLElement>): boolean {
|
||||
if (!parentElement) {
|
||||
return false;
|
||||
}
|
||||
return Array.from(parentElement.nativeElement.childNodes).some(item => item.nodeType !== 8);
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@
|
||||
<div class="spacer"></div>
|
||||
|
||||
<!-- Button to open the global search -->
|
||||
<button *ngIf="!editMode" mat-icon-button (click)="openSearch()">
|
||||
<button *ngIf="!editMode && isSearchEnabled" mat-icon-button (click)="openSearch()">
|
||||
<mat-icon>search</mat-icon>
|
||||
</button>
|
||||
|
||||
@ -40,7 +40,8 @@
|
||||
<!-- Main action button - desktop -->
|
||||
<button
|
||||
mat-icon-button
|
||||
*ngIf="mainButton && !editMode && !vp.isMobile && !multiSelectMode"
|
||||
*ngIf="hasMainButton && !editMode && !vp.isMobile && !multiSelectMode"
|
||||
[disabled]="!isMainButtonEnabled"
|
||||
(click)="sendMainEvent()"
|
||||
matTooltip="{{ mainActionTooltip | translate }}"
|
||||
>
|
||||
@ -62,7 +63,8 @@
|
||||
<button
|
||||
mat-fab
|
||||
class="head-button"
|
||||
*ngIf="mainButton && !editMode && vp.isMobile && !multiSelectMode"
|
||||
*ngIf="hasMainButton && !editMode && vp.isMobile && !multiSelectMode"
|
||||
[disabled]="!isMainButtonEnabled"
|
||||
(click)="sendMainEvent()"
|
||||
matTooltip="{{ mainActionTooltip | translate }}"
|
||||
>
|
||||
|
@ -21,7 +21,7 @@ import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||
* saveText="Create"
|
||||
* [nav]="false"
|
||||
* [goBack]="true"
|
||||
* [mainButton]="opCanEdit()"
|
||||
* [hasMainButton]="opCanEdit()"
|
||||
* [mainButtonIcon]="edit"
|
||||
* [backButtonIcon]="arrow_back"
|
||||
* [editMode]="editMotion"
|
||||
@ -82,6 +82,12 @@ export class HeadBarComponent implements OnInit {
|
||||
@Input()
|
||||
public editMode = false;
|
||||
|
||||
/**
|
||||
* Determine, if the search should not be available.
|
||||
*/
|
||||
@Input()
|
||||
public isSearchEnabled = true;
|
||||
|
||||
/**
|
||||
* The save button can manually be disabled.
|
||||
*/
|
||||
@ -98,7 +104,13 @@ export class HeadBarComponent implements OnInit {
|
||||
* Determine if there should be the main action button
|
||||
*/
|
||||
@Input()
|
||||
public mainButton = false;
|
||||
public hasMainButton = false;
|
||||
|
||||
/**
|
||||
* Determine if the main action button should be enabled or not.
|
||||
*/
|
||||
@Input()
|
||||
public isMainButtonEnabled = true;
|
||||
|
||||
/**
|
||||
* Set to true if the component should use location.back instead
|
||||
|
@ -1,4 +1,5 @@
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="tileContext"
|
||||
[ngTemplateOutletContext]="data">
|
||||
</ng-container>
|
||||
<ng-container [ngTemplateOutlet]="tileContext" [ngTemplateOutletContext]="data"> </ng-container>
|
||||
|
||||
<ng-template #tileContext>
|
||||
<ng-content></ng-content>
|
||||
</ng-template>
|
||||
|
@ -149,7 +149,7 @@ export class TileComponent implements OnInit {
|
||||
*
|
||||
* @param size how great the tile should be
|
||||
*/
|
||||
private setTabletSize(size: number): void {
|
||||
private setTabletSize(size: number = 4): void {
|
||||
if (size <= 8 && size >= 0) {
|
||||
this.tabletSize = size;
|
||||
} else {
|
||||
@ -162,7 +162,7 @@ export class TileComponent implements OnInit {
|
||||
*
|
||||
* @param size how great the tile should be
|
||||
*/
|
||||
private setMediumSize(size: number): void {
|
||||
private setMediumSize(size: number = 4): void {
|
||||
if (size <= 12 && size >= 0) {
|
||||
this.mediumSize = size;
|
||||
} else {
|
||||
@ -175,7 +175,7 @@ export class TileComponent implements OnInit {
|
||||
*
|
||||
* @param size how great the tile should be
|
||||
*/
|
||||
private setLargeSize(size: number): void {
|
||||
private setLargeSize(size: number = 4): void {
|
||||
if (size <= 16 && size >= 0) {
|
||||
this.largeSize = size;
|
||||
} else {
|
||||
|
@ -1,5 +1,35 @@
|
||||
import { BaseModel } from '../base/base-model';
|
||||
|
||||
export interface ConfigChoice {
|
||||
value: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* All valid input types for config variables.
|
||||
*/
|
||||
export type ConfigInputType =
|
||||
| 'text'
|
||||
| 'string'
|
||||
| 'boolean'
|
||||
| 'markupText'
|
||||
| 'integer'
|
||||
| 'choice'
|
||||
| 'datetimepicker'
|
||||
| 'colorpicker'
|
||||
| 'translations';
|
||||
|
||||
export interface ConfigData {
|
||||
defaultValue: any;
|
||||
inputType: ConfigInputType;
|
||||
label: string;
|
||||
helpText?: string;
|
||||
choices?: ConfigChoice[];
|
||||
weight: number;
|
||||
group: string;
|
||||
subgroup?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Representation of a config variable
|
||||
* @ignore
|
||||
@ -8,7 +38,8 @@ export class Config extends BaseModel {
|
||||
public static COLLECTIONSTRING = 'core/config';
|
||||
public id: number;
|
||||
public key: string;
|
||||
public value: Object;
|
||||
public value: any;
|
||||
public data?: ConfigData;
|
||||
|
||||
public constructor(input?: any) {
|
||||
super(Config.COLLECTIONSTRING, input);
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { WatchSortingTreeGuard } from './watch-sorting-tree.guard';
|
||||
import { WatchForChangesGuard } from './watch-for-changes.guard';
|
||||
|
||||
describe('WatchSortingTreeGuard', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [WatchSortingTreeGuard]
|
||||
providers: [WatchForChangesGuard]
|
||||
});
|
||||
});
|
||||
|
||||
it('should ...', inject([WatchSortingTreeGuard], (guard: WatchSortingTreeGuard) => {
|
||||
it('should ...', inject([WatchForChangesGuard], (guard: WatchForChangesGuard) => {
|
||||
expect(guard).toBeTruthy();
|
||||
}));
|
||||
});
|
@ -11,7 +11,7 @@ export interface CanComponentDeactivate {
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class WatchSortingTreeGuard implements CanDeactivate<CanComponentDeactivate> {
|
||||
export class WatchForChangesGuard implements CanDeactivate<CanComponentDeactivate> {
|
||||
/**
|
||||
* Function to determine whether the route will change or not.
|
||||
*
|
@ -4,7 +4,7 @@ import { RouterModule, Routes } from '@angular/router';
|
||||
import { AgendaImportListComponent } from './components/agenda-import-list/agenda-import-list.component';
|
||||
import { AgendaListComponent } from './components/agenda-list/agenda-list.component';
|
||||
import { AgendaSortComponent } from './components/agenda-sort/agenda-sort.component';
|
||||
import { WatchSortingTreeGuard } from 'app/shared/utils/watch-sorting-tree.guard';
|
||||
import { WatchForChangesGuard } from 'app/shared/utils/watch-for-changes.guard';
|
||||
import { ListOfSpeakersComponent } from './components/list-of-speakers/list-of-speakers.component';
|
||||
|
||||
const routes: Routes = [
|
||||
@ -13,7 +13,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'sort-agenda',
|
||||
component: AgendaSortComponent,
|
||||
canDeactivate: [WatchSortingTreeGuard],
|
||||
canDeactivate: [WatchForChangesGuard],
|
||||
data: { basePerm: 'agenda.can_manage' }
|
||||
},
|
||||
{ path: 'speakers', component: ListOfSpeakersComponent, data: { basePerm: 'agenda.can_see_list_of_speakers' } },
|
||||
|
@ -1,4 +1,4 @@
|
||||
<os-head-bar [mainButton]="canManage" (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect">
|
||||
<os-head-bar [hasMainButton]="canManage" (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect">
|
||||
<!-- Title -->
|
||||
<div class="title-slot"><h2 translate>Agenda</h2></div>
|
||||
<!-- Menu -->
|
||||
@ -119,7 +119,13 @@
|
||||
<!-- Import -->
|
||||
<button mat-menu-item *osPerms="'agenda.can_manage'" routerLink="import">
|
||||
<mat-icon>cloud_upload</mat-icon>
|
||||
<span translate>Import</span><span> ...</span>
|
||||
<span translate>Import</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<!-- Settings -->
|
||||
<button mat-menu-item *osPerms="'core.can_manage_config'" routerLink="/settings/agenda">
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span translate>Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<os-head-bar
|
||||
[mainButton]="hasPerms('manage')"
|
||||
[hasMainButton]="hasPerms('manage')"
|
||||
mainButtonIcon="edit"
|
||||
[nav]="false"
|
||||
[editMode]="editAssignment"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<os-head-bar
|
||||
[mainButton]="operator.hasPerms('assignments.can_manage')"
|
||||
[hasMainButton]="operator.hasPerms('assignments.can_manage')"
|
||||
(mainEvent)="onPlusButton()"
|
||||
[multiSelectMode]="isMultiSelect"
|
||||
>
|
||||
@ -91,6 +91,12 @@
|
||||
<mat-icon>archive</mat-icon>
|
||||
<span translate>Export ...</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<!-- Settings -->
|
||||
<button mat-menu-item *osPerms="'core.can_manage_config'" routerLink="/settings/elections">
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span translate>Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isMultiSelect">
|
||||
|
@ -8,7 +8,7 @@ import { SortDefinition } from 'app/core/ui-services/base-sort.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { SortingTreeComponent } from 'app/shared/components/sorting-tree/sorting-tree.component';
|
||||
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||
import { CanComponentDeactivate } from 'app/shared/utils/watch-sorting-tree.guard';
|
||||
import { CanComponentDeactivate } from 'app/shared/utils/watch-for-changes.guard';
|
||||
import { BaseViewComponent } from './base-view';
|
||||
import { BaseViewModel } from './base-view-model';
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
<!-- required for all kinds of input -->
|
||||
<mat-label>{{ configItem.label | translate }}</mat-label>
|
||||
<span matSuffix>
|
||||
<mat-icon pull="right" class="text-success" *ngIf="updateSuccessIcon">check_circle</mat-icon>
|
||||
<mat-icon pull="right" class="red-warning-text" *ngIf="error">error</mat-icon>
|
||||
</span>
|
||||
<mat-error *ngIf="error"> {{ error }} </mat-error>
|
||||
|
||||
@ -57,7 +57,7 @@
|
||||
[value]="translatedValue"
|
||||
></textarea>
|
||||
<span matSuffix>
|
||||
<mat-icon pull="right" class="text-success" *ngIf="updateSuccessIcon">
|
||||
<mat-icon pull="right" class="red-warning-text" *ngIf="updateSuccessIcon">
|
||||
check_circle
|
||||
</mat-icon>
|
||||
</span>
|
||||
@ -77,7 +77,7 @@
|
||||
/>
|
||||
<mat-hint *ngIf="configItem.helpText">{{ configItem.helpText | translate }}</mat-hint>
|
||||
<div class="suffix-wrapper" matSuffix>
|
||||
<mat-icon class="text-success" *ngIf="updateSuccessIcon">check_circle</mat-icon>
|
||||
<mat-icon class="red-warning-text" *ngIf="updateSuccessIcon">check_circle</mat-icon>
|
||||
<mat-datepicker-toggle
|
||||
[for]="datepicker"
|
||||
(click)="$event.preventDefault()"
|
||||
@ -89,7 +89,7 @@
|
||||
<mat-form-field>
|
||||
<input matInput [format]="24" formControlName="time" [ngxTimepicker]="timepicker" />
|
||||
<div class="suffix-wrapper" matSuffix>
|
||||
<mat-icon class="text-success" *ngIf="updateSuccessIcon">check_circle</mat-icon>
|
||||
<mat-icon class="red-warning-text" *ngIf="updateSuccessIcon">error</mat-icon>
|
||||
<ngx-material-timepicker-toggle [for]="timepicker"></ngx-material-timepicker-toggle>
|
||||
</div>
|
||||
<mat-error *ngIf="error"> {{ error }} </mat-error>
|
||||
@ -103,16 +103,13 @@
|
||||
<h4>{{ configItem.label | translate }}</h4>
|
||||
<editor formControlName="value" [init]="getTinyMceSettings()"></editor>
|
||||
<span matSuffix>
|
||||
<mat-icon pull="right" class="text-success" *ngIf="updateSuccessIcon">check_circle</mat-icon>
|
||||
<mat-icon pull="right" class="red-warning-text" *ngIf="updateSuccessIcon">error</mat-icon>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Custom Translations -->
|
||||
<div *ngIf="configItem.inputType === 'translations'">
|
||||
<os-custom-translation formControlName="value"></os-custom-translation>
|
||||
<span matSuffix>
|
||||
<mat-icon pull="right" class="text-success" *ngIf="updateSuccessIcon">check_circle</mat-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@ -122,9 +119,4 @@
|
||||
<mat-icon>help_outline</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="reset-button">
|
||||
<button mat-icon-button *ngIf="hasDefault()" matTooltip="{{ 'Reset' | translate }}" (click)="onResetButton()">
|
||||
<mat-icon>replay</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,9 +2,11 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
@ -18,6 +20,7 @@ import { distinctUntilChanged } from 'rxjs/operators';
|
||||
import { BaseComponent } from 'app/base.component';
|
||||
import { ConfigRepositoryService } from 'app/core/repositories/config/config-repository.service';
|
||||
import { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher';
|
||||
import { ConfigItem } from '../config-list/config-list.component';
|
||||
import { ViewConfig } from '../../models/view-config';
|
||||
|
||||
/**
|
||||
@ -26,7 +29,7 @@ import { ViewConfig } from '../../models/view-config';
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* <os-config-field [item]="item.config"></os-config-field>
|
||||
* <os-config-field [config]="<ViewConfig>"></os-config-field>
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
@ -44,16 +47,6 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
||||
*/
|
||||
public updateSuccessIcon = false;
|
||||
|
||||
/**
|
||||
* The timeout for the success icon to hide.
|
||||
*/
|
||||
private updateSuccessIconTimeout: number | null = null;
|
||||
|
||||
/**
|
||||
* The debounce timeout for inputs request delay.
|
||||
*/
|
||||
private debounceTimeout: number | null = null;
|
||||
|
||||
/**
|
||||
* A possible error send by the server.
|
||||
*/
|
||||
@ -69,8 +62,8 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
||||
* populated constants-info.
|
||||
*/
|
||||
@Input()
|
||||
public set item(value: ViewConfig) {
|
||||
if (value && value.hasConstantsInfo) {
|
||||
public set config(value: ViewConfig) {
|
||||
if (value) {
|
||||
this.configItem = value;
|
||||
|
||||
if (this.form) {
|
||||
@ -90,6 +83,25 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Passes the list of errors as object.
|
||||
*
|
||||
* The function looks, if the key of this config-item is contained in the list.
|
||||
*
|
||||
* @param errorList The object containing all errors.
|
||||
*/
|
||||
@Input()
|
||||
public set errorList(errorList: { [key: string]: any }) {
|
||||
const hasError = Object.keys(errorList).find(errorKey => errorKey === this.configItem.key);
|
||||
if (hasError) {
|
||||
this.error = errorList[hasError];
|
||||
this.updateError(true);
|
||||
} else {
|
||||
this.error = null;
|
||||
this.updateError(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The form for this configItem.
|
||||
*/
|
||||
@ -100,6 +112,9 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
||||
*/
|
||||
public matcher = new ParentErrorStateMatcher();
|
||||
|
||||
@Output()
|
||||
public update = new EventEmitter<ConfigItem>();
|
||||
|
||||
/**
|
||||
* The usual component constructor. datetime pickers will set their locale
|
||||
* to the current language chosen
|
||||
@ -172,7 +187,7 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
||||
private unixToDateAndTime(unix: number): { date: Moment; time: string } {
|
||||
const date = moment.unix(unix);
|
||||
const time = date.hours() + ':' + date.minutes();
|
||||
return { date: date, time: time };
|
||||
return { date, time };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -211,12 +226,7 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
||||
const time = this.form.get('time').value;
|
||||
value = this.dateAndTimeToUnix(date, time);
|
||||
}
|
||||
if (this.debounceTimeout !== null) {
|
||||
clearTimeout(<any>this.debounceTimeout);
|
||||
}
|
||||
this.debounceTimeout = <any>setTimeout(() => {
|
||||
this.update(value);
|
||||
}, this.configItem.getDebouncingTimeout());
|
||||
this.sendUpdate(value);
|
||||
this.cd.detectChanges();
|
||||
}
|
||||
|
||||
@ -233,53 +243,22 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
||||
* Sends an update request for the config item to the server.
|
||||
* @param value The new value to set.
|
||||
*/
|
||||
private update(value: any): void {
|
||||
this.debounceTimeout = null;
|
||||
this.repo.update({ value: value }, this.configItem).then(() => {
|
||||
this.error = null;
|
||||
this.showSuccessIcon();
|
||||
}, this.setError.bind(this));
|
||||
private sendUpdate(value: any): void {
|
||||
this.update.emit({ key: this.configItem.key, value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the green success icon on the component. The icon gets automatically cleared.
|
||||
* Function to update the form-control to display or hide an error.
|
||||
*
|
||||
* @param error `true | false`, if an error should be shown. `null`, if there is no error.
|
||||
*/
|
||||
private showSuccessIcon(): void {
|
||||
if (this.updateSuccessIconTimeout !== null) {
|
||||
clearTimeout(<any>this.updateSuccessIconTimeout);
|
||||
}
|
||||
this.updateSuccessIconTimeout = <any>setTimeout(() => {
|
||||
this.updateSuccessIcon = false;
|
||||
if (!this.wasViewDestroyed()) {
|
||||
this.cd.detectChanges();
|
||||
}
|
||||
}, 2000);
|
||||
this.updateSuccessIcon = true;
|
||||
if (!this.wasViewDestroyed()) {
|
||||
private updateError(error: boolean | null): void {
|
||||
if (this.form) {
|
||||
this.form.setErrors(error ? { error } : null);
|
||||
this.cd.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true, if the view was destroyed. Note: This
|
||||
* needs to access internal attributes from the change detection
|
||||
* reference.
|
||||
*/
|
||||
private wasViewDestroyed(): boolean {
|
||||
return (<any>this.cd).destroyed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the error on this field.
|
||||
*
|
||||
* @param error The error as string.
|
||||
*/
|
||||
private setError(error: string): void {
|
||||
this.error = error;
|
||||
this.form.setErrors({ error: true });
|
||||
this.cd.detectChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the configItem to determine the kind of interation:
|
||||
* input, textarea, choice or date
|
||||
@ -311,16 +290,6 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
||||
return excluded.includes(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a reset buton should be offered.
|
||||
* TODO: is 'null' a valid default in some cases?
|
||||
*
|
||||
* @returns true if any default exists
|
||||
*/
|
||||
public hasDefault(): boolean {
|
||||
return this.configItem.defaultValue !== undefined && this.configItem.defaultValue !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Amends the application-wide tinyMCE settings with update triggers that
|
||||
* send updated values only after leaving focus (Blur) or closing the editor (Remove)
|
||||
@ -333,7 +302,7 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
||||
setup: editor => {
|
||||
editor.on('Blur', ev => {
|
||||
if (ev.target.getContent() !== this.translatedValue) {
|
||||
this.update(ev.target.getContent());
|
||||
this.sendUpdate(ev.target.getContent());
|
||||
}
|
||||
});
|
||||
editor.on('Remove', ev => {
|
||||
@ -341,7 +310,7 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
||||
// fast navigation, when the editor is not fully loaded. Then the content is empty
|
||||
// and would trigger an update with empty data.
|
||||
if (ev.target.getContent() && ev.target.getContent() !== this.translatedValue) {
|
||||
this.update(ev.target.getContent());
|
||||
this.sendUpdate(ev.target.getContent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,28 +1,45 @@
|
||||
<os-head-bar>
|
||||
<os-head-bar [nav]="false" [hasMainButton]="false" [isSearchEnabled]="false">
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 translate>Settings</h2>
|
||||
<h2 *ngIf="configGroup">
|
||||
{{ configGroup.name | translate }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Menu -->
|
||||
<div class="menu-slot">
|
||||
<button
|
||||
mat-button
|
||||
[disabled]="!hasChanges()"
|
||||
(click)="saveAll()"
|
||||
matTooltip="{{ 'Save all changes' | translate }}"
|
||||
>
|
||||
<strong class="upper">{{ 'Save' | translate }}</strong>
|
||||
</button>
|
||||
<button type="button" mat-icon-button [matMenuTriggerFor]="settingsMenu"><mat-icon>more_vert</mat-icon></button>
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
<div class="spacer-top-20"></div>
|
||||
<mat-accordion>
|
||||
<ng-container *ngFor="let group of this.configs">
|
||||
<mat-expansion-panel displayMode="flat">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
{{ group.name | translate }}
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngFor="let subgroup of group.subgroups">
|
||||
<h3 class="accent">{{ subgroup.name | translate }}</h3>
|
||||
<div *ngFor="let item of subgroup.items">
|
||||
<os-config-field [item]="item.config"></os-config-field>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngFor="let item of group.items">
|
||||
<os-config-field [item]="item.config"></os-config-field>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</ng-container>
|
||||
</mat-accordion>
|
||||
|
||||
<mat-card class="os-card" *ngIf="configGroup">
|
||||
<div id="wrapper">
|
||||
<ng-container *ngFor="let subgroup of configGroup.subgroups; trackBy: trackByIndex">
|
||||
<h3 class="accent" *ngIf="configGroup.subgroups.length > 1">{{ subgroup.name | translate }}</h3>
|
||||
<ng-container *ngFor="let config of subgroup.configs">
|
||||
<os-config-field
|
||||
(update)="updateConfigGroup($event)"
|
||||
[config]="config"
|
||||
[errorList]="errors"
|
||||
></os-config-field>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
<mat-menu #settingsMenu="matMenu">
|
||||
<button mat-menu-item (click)="resetAll()">
|
||||
<mat-icon>undo</mat-icon>
|
||||
<span translate>Reset all to default</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
@ -1,8 +1,12 @@
|
||||
mat-expansion-panel {
|
||||
max-width: 770px;
|
||||
margin: auto;
|
||||
#wrapper {
|
||||
font-size: 14px;
|
||||
}
|
||||
h3.accent {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 20px;
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
&:not(:first-child) {
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,29 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { BaseComponent } from 'app/base.component';
|
||||
import { ConfigGroup, ConfigRepositoryService } from 'app/core/repositories/config/config-repository.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { CanComponentDeactivate } from 'app/shared/utils/watch-for-changes.guard';
|
||||
|
||||
/**
|
||||
* Key-value-pair to set a setting with its associated value.
|
||||
*/
|
||||
export interface ConfigItem {
|
||||
/**
|
||||
* The key has to be a string.
|
||||
*/
|
||||
key: string;
|
||||
|
||||
/**
|
||||
* The value can be any.
|
||||
*/
|
||||
value: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* List view for the global settings
|
||||
@ -14,13 +33,27 @@ import { ConfigGroup, ConfigRepositoryService } from 'app/core/repositories/conf
|
||||
templateUrl: './config-list.component.html',
|
||||
styleUrls: ['./config-list.component.scss']
|
||||
})
|
||||
export class ConfigListComponent extends BaseComponent implements OnInit {
|
||||
public configs: ConfigGroup[];
|
||||
export class ConfigListComponent extends BaseComponent implements CanComponentDeactivate, OnInit, OnDestroy {
|
||||
public configGroup: ConfigGroup;
|
||||
|
||||
public configGroupSubscription: Subscription | null = null;
|
||||
|
||||
/**
|
||||
* Object containing all errors.
|
||||
*/
|
||||
public errors = {};
|
||||
|
||||
/**
|
||||
* Array of all changed settings.
|
||||
*/
|
||||
private configItems: ConfigItem[] = [];
|
||||
|
||||
public constructor(
|
||||
protected titleService: Title,
|
||||
protected translate: TranslateService,
|
||||
private repo: ConfigRepositoryService
|
||||
private repo: ConfigRepositoryService,
|
||||
private route: ActivatedRoute,
|
||||
private promptDialog: PromptService
|
||||
) {
|
||||
super(titleService, translate);
|
||||
}
|
||||
@ -29,10 +62,85 @@ export class ConfigListComponent extends BaseComponent implements OnInit {
|
||||
* Sets the title, inits the table and calls the repo
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
super.setTitle('Settings');
|
||||
|
||||
this.repo.getConfigListObservable().subscribe(configs => {
|
||||
this.configs = configs;
|
||||
const settings = this.translate.instant('Settings');
|
||||
this.route.params.subscribe(params => {
|
||||
this.clearSubscription();
|
||||
this.configGroupSubscription = this.repo.getConfigGroupOberservable(params.group).subscribe(configGroup => {
|
||||
if (configGroup) {
|
||||
const groupName = this.translate.instant(configGroup.name);
|
||||
super.setTitle(`${settings} - ${groupName}`);
|
||||
this.configGroup = configGroup;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the specified config-item indicated by the given key.
|
||||
*
|
||||
* @param key The key of the config-item.
|
||||
* @param value The next value the config-item has.
|
||||
*/
|
||||
public updateConfigGroup(update: ConfigItem): void {
|
||||
const { key, value }: ConfigItem = update;
|
||||
const index = this.configItems.findIndex(item => item.key === key);
|
||||
if (index === -1) {
|
||||
this.configItems.push({ key, value });
|
||||
} else {
|
||||
this.configItems[index] = { key, value };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves every field in this config-group.
|
||||
*/
|
||||
public saveAll(): void {
|
||||
this.repo.bulkUpdate(this.configItems).then(result => {
|
||||
this.errors = result.errors;
|
||||
if (Object.keys(result.errors).length === 0) {
|
||||
this.configItems = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This resets all values to their defaults.
|
||||
*/
|
||||
public resetAll(): void {
|
||||
this.repo.resetGroups([this.configGroup.name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns, if there are changes depending on the `configMap`.
|
||||
*
|
||||
* @returns True, if the array `configMap` has at least one member.
|
||||
*/
|
||||
public hasChanges(): boolean {
|
||||
return !!this.configItems.length;
|
||||
}
|
||||
|
||||
private clearSubscription(): void {
|
||||
if (this.configGroupSubscription) {
|
||||
this.configGroupSubscription.unsubscribe();
|
||||
this.configGroupSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.clearSubscription();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle-hook to hook into, before the route changes.
|
||||
*
|
||||
* @returns The answer of the user, if he made changes, `true` otherwise.
|
||||
*/
|
||||
public async canDeactivate(): Promise<boolean> {
|
||||
if (this.hasChanges()) {
|
||||
const title = this.translate.instant('Do you really want to exit this page?');
|
||||
const content = this.translate.instant('You made changes.');
|
||||
return await this.promptDialog.open(title, content);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,50 @@
|
||||
<os-head-bar [isSearchEnabled]="false">
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 translate>Settings</h2>
|
||||
</div>
|
||||
<!-- Menu -->
|
||||
<div class="menu-slot">
|
||||
<button type="button" mat-icon-button [matMenuTriggerFor]="settingsMenu"><mat-icon>more_vert</mat-icon></button>
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
<os-grid-layout>
|
||||
<os-block-tile
|
||||
*ngFor="let group of this.groups"
|
||||
orientation="vertical"
|
||||
[only]="'content'"
|
||||
[preferredSize]="2"
|
||||
[isSquare]="true"
|
||||
>
|
||||
<ng-container class="block-node">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td [ngSwitch]="group">
|
||||
<mat-icon *ngSwitchCase="'General'">home</mat-icon>
|
||||
<mat-icon *ngSwitchCase="'Agenda'">today</mat-icon>
|
||||
<mat-icon *ngSwitchCase="'Motions'">assignment</mat-icon>
|
||||
<mat-icon *ngSwitchCase="'Elections'">how_to_vote</mat-icon>
|
||||
<mat-icon *ngSwitchCase="'Participants'">groups</mat-icon>
|
||||
<mat-icon *ngSwitchCase="'Custom translations'">language</mat-icon>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
<ng-container class="block-content-node">
|
||||
<a class="stretch-to-fill-parent" [routerLink]="group.toLowerCase()"></a>
|
||||
<div class="link-wrapper">
|
||||
{{ group | translate }}
|
||||
</div>
|
||||
</ng-container>
|
||||
</os-block-tile>
|
||||
</os-grid-layout>
|
||||
|
||||
<mat-menu #settingsMenu="matMenu">
|
||||
<button mat-menu-item (click)="resetAll()">
|
||||
<mat-icon>undo</mat-icon>
|
||||
<span translate>Reset every group to default</span>
|
||||
</button>
|
||||
</mat-menu>
|
@ -0,0 +1,14 @@
|
||||
.parent-wrapper {
|
||||
position: relative;
|
||||
padding: 50%;
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.link-wrapper {
|
||||
text-decoration: none;
|
||||
padding: 10% 0;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ConfigOverviewComponent } from './config-overview.component';
|
||||
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
||||
|
||||
describe('ConfigOverviewComponent', () => {
|
||||
let component: ConfigOverviewComponent;
|
||||
let fixture: ComponentFixture<ConfigOverviewComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
declarations: [ConfigOverviewComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ConfigOverviewComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,50 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { BaseComponent } from 'app/base.component';
|
||||
import { ConfigRepositoryService } from 'app/core/repositories/config/config-repository.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
|
||||
/**
|
||||
* List view for the global settings
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-config-overview',
|
||||
templateUrl: './config-overview.component.html',
|
||||
styleUrls: ['./config-overview.component.scss']
|
||||
})
|
||||
export class ConfigOverviewComponent extends BaseComponent implements OnInit {
|
||||
public groups: string[] = [];
|
||||
|
||||
public constructor(
|
||||
protected titleService: Title,
|
||||
protected translate: TranslateService,
|
||||
public repo: ConfigRepositoryService,
|
||||
private promptDialog: PromptService
|
||||
) {
|
||||
super(titleService, translate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the title, inits the table and calls the repo
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
super.setTitle('Settings');
|
||||
|
||||
this.repo.availableGroupsOberservable.subscribe(groups => {
|
||||
this.groups = groups;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets every config for all registered group.
|
||||
*/
|
||||
public async resetAll(): Promise<void> {
|
||||
const title = this.translate.instant('Are you sure, you want to reset all groups?');
|
||||
if (await this.promptDialog.open(title)) {
|
||||
await this.repo.resetGroups(this.groups);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +1,32 @@
|
||||
<!-- Add new translation button -->
|
||||
<div *ngFor="let translation of translations; let i = index">
|
||||
<form>
|
||||
<mat-form-field>
|
||||
<input matInput [value]="translation.original" (input)="onChangeOriginal($event.target.value, i)" />
|
||||
</mat-form-field>
|
||||
<mat-icon>arrow_forward</mat-icon>
|
||||
<mat-form-field>
|
||||
<input matInput [value]="translation.translation" (input)="onChangeTranslation($event.target.value, i)" />
|
||||
</mat-form-field>
|
||||
<button mat-icon-button>
|
||||
<mat-icon (click)="onRemoveTranslation(i)">close</mat-icon>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<form [formGroup]="translationForm">
|
||||
<div
|
||||
class="inline-form"
|
||||
formArrayName="translationBoxes"
|
||||
*ngFor="let translation of translationBoxes.controls; let i = index"
|
||||
>
|
||||
<ng-container [formGroupName]="i">
|
||||
<mat-form-field>
|
||||
<input formControlName="original" matInput placeholder="{{ 'Original' | translate }}" />
|
||||
<mat-error translate>You have to fill this field.</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-icon>arrow_forward</mat-icon>
|
||||
<mat-form-field>
|
||||
<input formControlName="translation" matInput placeholder="{{ 'Translation' | translate }}" />
|
||||
<mat-error translate>You have to fill this field.</mat-error>
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
(click)="onRemoveTranslation(i)"
|
||||
matTooltip="{{ 'Cancel' | translate }}"
|
||||
>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Add new translation button -->
|
||||
<button mat-button (click)="onAddNewTranslation()">{{ 'Add new custom translation' | translate }}</button>
|
||||
<button mat-button (click)="addNewTranslation()">
|
||||
<mat-icon>add</mat-icon>{{ 'Add new custom translation' | translate }}
|
||||
</button>
|
||||
|
@ -0,0 +1,8 @@
|
||||
.inline-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
mat-icon {
|
||||
margin: 0 8px;
|
||||
}
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
import { Component, forwardRef } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
import { CustomTranslation, CustomTranslations } from 'app/core/translate/translation-parser';
|
||||
import { Component, forwardRef, OnInit } from '@angular/core';
|
||||
import { ControlValueAccessor, FormArray, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
|
||||
|
||||
/**
|
||||
* Custom translations as custom form component
|
||||
@ -23,16 +21,39 @@ import { CustomTranslation, CustomTranslations } from 'app/core/translate/transl
|
||||
}
|
||||
]
|
||||
})
|
||||
export class CustomTranslationComponent implements ControlValueAccessor {
|
||||
export class CustomTranslationComponent implements ControlValueAccessor, OnInit {
|
||||
/**
|
||||
* Holds the custom translations in a list
|
||||
* The parent form-group.
|
||||
*/
|
||||
public translations: CustomTranslations = [];
|
||||
public translationForm: FormGroup;
|
||||
|
||||
/**
|
||||
* Empty constructor
|
||||
* Reference to the form-control within the `translationForm`.
|
||||
*/
|
||||
public constructor() {}
|
||||
public translationBoxes: FormArray;
|
||||
|
||||
/**
|
||||
* Default constructor.
|
||||
*
|
||||
* @param fb FormBuilder
|
||||
*/
|
||||
public constructor(private fb: FormBuilder) {}
|
||||
|
||||
/**
|
||||
* Initializes the form-controls.
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
this.translationForm = this.fb.group({
|
||||
translationBoxes: this.fb.array([])
|
||||
});
|
||||
|
||||
this.translationBoxes = this.translationForm.get('translationBoxes') as FormArray;
|
||||
this.translationBoxes.valueChanges.subscribe(value => {
|
||||
if (this.translationBoxes.valid) {
|
||||
this.propagateChange(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to determine which information to give to the parent form
|
||||
@ -46,7 +67,9 @@ export class CustomTranslationComponent implements ControlValueAccessor {
|
||||
*/
|
||||
public writeValue(obj: any): void {
|
||||
if (obj) {
|
||||
this.translations = obj;
|
||||
for (const item of obj) {
|
||||
this.addNewTranslation(item.original, item.translation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,46 +96,28 @@ export class CustomTranslationComponent implements ControlValueAccessor {
|
||||
*/
|
||||
public setDisabledState?(isDisabled: boolean): void {}
|
||||
|
||||
/**
|
||||
* Detects changes to the "original" word
|
||||
*
|
||||
* @param value the value that was typed
|
||||
* @param index the index of the change
|
||||
*/
|
||||
public onChangeOriginal(value: string, index: number): void {
|
||||
this.translations[index].original = value;
|
||||
this.propagateChange(this.translations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects changes to the translation
|
||||
* @param value the value that was typed
|
||||
* @param index the index of the change
|
||||
*/
|
||||
public onChangeTranslation(value: string, index: number): void {
|
||||
this.translations[index].translation = value;
|
||||
this.propagateChange(this.translations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a custom translation
|
||||
*
|
||||
* @param index the translation to remove
|
||||
*/
|
||||
public onRemoveTranslation(index: number): void {
|
||||
this.translations.splice(index, 1);
|
||||
this.propagateChange(this.translations);
|
||||
this.translationBoxes.removeAt(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new custom translation to the list and to the server
|
||||
* Function to add a new translation-field to the form-array.
|
||||
* If strings are passed, they are passed as the fields' value.
|
||||
*
|
||||
* @param original The original string to translate.
|
||||
* @param translation The translation for the given string.
|
||||
*/
|
||||
public onAddNewTranslation(): void {
|
||||
const newCustomTranslation: CustomTranslation = {
|
||||
original: 'New',
|
||||
translation: 'New'
|
||||
};
|
||||
|
||||
this.translations.push(newCustomTranslation);
|
||||
this.propagateChange(this.translations);
|
||||
public addNewTranslation(original: string = '', translation: string = ''): void {
|
||||
this.translationBoxes.push(
|
||||
this.fb.group({
|
||||
original: [original, Validators.required],
|
||||
translation: [translation, Validators.required]
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { WatchForChangesGuard } from 'app/shared/utils/watch-for-changes.guard';
|
||||
import { ConfigListComponent } from './components/config-list/config-list.component';
|
||||
import { ConfigOverviewComponent } from './components/config-overview/config-overview.component';
|
||||
|
||||
const routes: Routes = [{ path: '', component: ConfigListComponent, pathMatch: 'full' }];
|
||||
const routes: Routes = [
|
||||
{ path: '', component: ConfigOverviewComponent, pathMatch: 'full' },
|
||||
{ path: ':group', component: ConfigListComponent, canDeactivate: [WatchForChangesGuard] }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
|
@ -3,13 +3,14 @@ import { NgModule } from '@angular/core';
|
||||
|
||||
import { ConfigFieldComponent } from './components/config-field/config-field.component';
|
||||
import { ConfigListComponent } from './components/config-list/config-list.component';
|
||||
import { ConfigOverviewComponent } from './components/config-overview/config-overview.component';
|
||||
import { ConfigRoutingModule } from './config-routing.module';
|
||||
import { CustomTranslationComponent } from './components/custom-translation/custom-translation.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, ConfigRoutingModule, SharedModule],
|
||||
declarations: [ConfigListComponent, ConfigFieldComponent, CustomTranslationComponent],
|
||||
declarations: [ConfigOverviewComponent, ConfigListComponent, ConfigFieldComponent, CustomTranslationComponent],
|
||||
entryComponents: [CustomTranslationComponent]
|
||||
})
|
||||
export class ConfigModule {}
|
||||
|
@ -1,37 +1,6 @@
|
||||
import { Config } from 'app/shared/models/core/config';
|
||||
import { Config, ConfigChoice, ConfigData, ConfigInputType } from 'app/shared/models/core/config';
|
||||
import { BaseViewModel } from '../../base/base-view-model';
|
||||
|
||||
interface ConfigChoice {
|
||||
value: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* All valid input types for config variables.
|
||||
*/
|
||||
type ConfigInputType =
|
||||
| 'text'
|
||||
| 'string'
|
||||
| 'boolean'
|
||||
| 'markupText'
|
||||
| 'integer'
|
||||
| 'choice'
|
||||
| 'datetimepicker'
|
||||
| 'colorpicker'
|
||||
| 'translations';
|
||||
|
||||
/**
|
||||
* Represents all information that is given in the constant.
|
||||
*/
|
||||
interface ConfigConstant {
|
||||
default_value?: string;
|
||||
help_text?: string;
|
||||
input_type: ConfigInputType;
|
||||
key: string;
|
||||
label: string;
|
||||
choices?: ConfigChoice[];
|
||||
}
|
||||
|
||||
export interface ConfigTitleInformation {
|
||||
key: string;
|
||||
}
|
||||
@ -43,22 +12,6 @@ export class ViewConfig extends BaseViewModel<Config> implements ConfigTitleInfo
|
||||
public static COLLECTIONSTRING = Config.COLLECTIONSTRING;
|
||||
protected _collectionString = Config.COLLECTIONSTRING;
|
||||
|
||||
/* This private members are set by setConstantsInfo. */
|
||||
private _helpText: string;
|
||||
private _inputType: ConfigInputType;
|
||||
private _label: string;
|
||||
private _choices: ConfigChoice[];
|
||||
private _defaultValue: any;
|
||||
|
||||
/**
|
||||
* Saves, if this config already got constants information.
|
||||
*/
|
||||
private _hasConstantsInfo = false;
|
||||
|
||||
public get hasConstantsInfo(): boolean {
|
||||
return this._hasConstantsInfo;
|
||||
}
|
||||
|
||||
public get config(): Config {
|
||||
return this._model;
|
||||
}
|
||||
@ -67,28 +20,48 @@ export class ViewConfig extends BaseViewModel<Config> implements ConfigTitleInfo
|
||||
return this.config.key;
|
||||
}
|
||||
|
||||
public get value(): Object {
|
||||
public get value(): any {
|
||||
return this.config.value;
|
||||
}
|
||||
|
||||
public get data(): ConfigData | null {
|
||||
return this.config.data;
|
||||
}
|
||||
|
||||
public get hidden(): boolean {
|
||||
return !this.data;
|
||||
}
|
||||
|
||||
public get label(): string {
|
||||
return this._label;
|
||||
return this.data.label;
|
||||
}
|
||||
|
||||
public get inputType(): ConfigInputType {
|
||||
return this._inputType;
|
||||
public get inputType(): ConfigInputType | null {
|
||||
return this.data.inputType;
|
||||
}
|
||||
|
||||
public get helpText(): string {
|
||||
return this._helpText;
|
||||
public get helpText(): string | null {
|
||||
return this.data.helpText;
|
||||
}
|
||||
|
||||
public get choices(): Object {
|
||||
return this._choices;
|
||||
public get choices(): ConfigChoice[] | null {
|
||||
return this.data.choices;
|
||||
}
|
||||
|
||||
public get defaultValue(): any {
|
||||
return this._defaultValue;
|
||||
return this.data.defaultValue;
|
||||
}
|
||||
|
||||
public get weight(): number {
|
||||
return this.hidden ? 0 : this.data.weight;
|
||||
}
|
||||
|
||||
public get group(): string {
|
||||
return this.data.group;
|
||||
}
|
||||
|
||||
public get subgroup(): string | null {
|
||||
return this.data.subgroup;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -105,19 +78,4 @@ export class ViewConfig extends BaseViewModel<Config> implements ConfigTitleInfo
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This should be called, if the constants are loaded, so all extra info can be updated.
|
||||
* @param constant The constant info
|
||||
*/
|
||||
public setConstantsInfo(constant: ConfigConstant): void {
|
||||
this._label = constant.label;
|
||||
this._helpText = constant.help_text;
|
||||
this._inputType = constant.input_type;
|
||||
this._choices = constant.choices;
|
||||
if (constant.default_value !== undefined) {
|
||||
this._defaultValue = constant.default_value;
|
||||
}
|
||||
this._hasConstantsInfo = true;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<os-head-bar [mainButton]="canEdit" [multiSelectMode]="isMultiSelect" (mainEvent)="onMainEvent()">
|
||||
<os-head-bar [hasMainButton]="canEdit" [multiSelectMode]="isMultiSelect" (mainEvent)="onMainEvent()">
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 translate>Files</h2>
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { WatchSortingTreeGuard } from 'app/shared/utils/watch-sorting-tree.guard';
|
||||
import { WatchForChangesGuard } from 'app/shared/utils/watch-for-changes.guard';
|
||||
import { CallListComponent } from './call-list.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: CallListComponent, pathMatch: 'full', canDeactivate: [WatchSortingTreeGuard] }
|
||||
{ path: '', component: CallListComponent, pathMatch: 'full', canDeactivate: [WatchForChangesGuard] }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { WatchSortingTreeGuard } from 'app/shared/utils/watch-sorting-tree.guard';
|
||||
import { WatchForChangesGuard } from 'app/shared/utils/watch-for-changes.guard';
|
||||
import { CategoriesSortComponent } from './components/categories-sort/categories-sort.component';
|
||||
import { CategoryDetailComponent } from './components/category-detail/category-detail.component';
|
||||
import { CategoryListComponent } from './components/category-list/category-list.component';
|
||||
@ -9,8 +9,8 @@ import { CategoryMotionsSortComponent } from './components/category-motions-sort
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: CategoryListComponent, pathMatch: 'full' },
|
||||
{ path: ':id/sort', component: CategoryMotionsSortComponent, canDeactivate: [WatchSortingTreeGuard] },
|
||||
{ path: 'sort', component: CategoriesSortComponent, canDeactivate: [WatchSortingTreeGuard] },
|
||||
{ path: ':id/sort', component: CategoryMotionsSortComponent, canDeactivate: [WatchForChangesGuard] },
|
||||
{ path: 'sort', component: CategoriesSortComponent, canDeactivate: [WatchForChangesGuard] },
|
||||
{ path: ':id', component: CategoryDetailComponent }
|
||||
];
|
||||
|
||||
|
@ -8,7 +8,7 @@ import { Observable } from 'rxjs';
|
||||
import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { SortingTreeComponent } from 'app/shared/components/sorting-tree/sorting-tree.component';
|
||||
import { CanComponentDeactivate } from 'app/shared/utils/watch-sorting-tree.guard';
|
||||
import { CanComponentDeactivate } from 'app/shared/utils/watch-for-changes.guard';
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { ViewCategory } from 'app/site/motions/models/view-category';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
<os-head-bar prevUrl="../.." [nav]="false" [mainButton]="canEdit" (mainEvent)="onPlusButton()">
|
||||
<os-head-bar prevUrl="../.." [nav]="false" [hasMainButton]="canEdit" (mainEvent)="onPlusButton()">
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 translate>Categories</h2>
|
||||
|
@ -11,7 +11,7 @@ import { MotionRepositoryService } from 'app/core/repositories/motions/motion-re
|
||||
import { ChoiceService } from 'app/core/ui-services/choice.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { SortingListComponent } from 'app/shared/components/sorting-list/sorting-list.component';
|
||||
import { CanComponentDeactivate } from 'app/shared/utils/watch-sorting-tree.guard';
|
||||
import { CanComponentDeactivate } from 'app/shared/utils/watch-for-changes.guard';
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { ViewCategory } from 'app/site/motions/models/view-category';
|
||||
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
||||
|
@ -1,4 +1,4 @@
|
||||
<os-head-bar prevUrl="../.." [nav]="false" [mainButton]="canEdit" (mainEvent)="onPlusButton()">
|
||||
<os-head-bar prevUrl="../.." [nav]="false" [hasMainButton]="canEdit" (mainEvent)="onPlusButton()">
|
||||
<!-- Title -->
|
||||
<div class="title-slot"><h2 translate>Motion blocks</h2></div>
|
||||
</os-head-bar>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<os-head-bar prevUrl="../.." [nav]="false" [mainButton]="true" (mainEvent)="openDialog()">
|
||||
<os-head-bar prevUrl="../.." [nav]="false" [hasMainButton]="true" (mainEvent)="openDialog()">
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 translate>Comment fields</h2>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<os-head-bar
|
||||
[mainButton]="perms.isAllowed('can_create_amendments', motion)"
|
||||
[hasMainButton]="perms.isAllowed('can_create_amendments', motion)"
|
||||
mainActionTooltip="New amendment"
|
||||
[prevUrl]="getPrevUrl()"
|
||||
[nav]="false"
|
||||
|
@ -1,4 +1,4 @@
|
||||
<os-head-bar [mainButton]="perms.isAllowed('create')" (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect">
|
||||
<os-head-bar [hasMainButton]="perms.isAllowed('create')" (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect">
|
||||
<!-- Title -->
|
||||
<div class="title-slot"><h2 translate>Motions</h2></div>
|
||||
|
||||
@ -315,6 +315,14 @@
|
||||
<span translate>Import</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<!-- Settings -->
|
||||
<button mat-menu-item *osPerms="'core.can_manage_config'" routerLink="/settings/motions">
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span translate>Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="isMultiSelect">
|
||||
<button mat-menu-item (click)="selectAll()">
|
||||
|
@ -1,4 +1,4 @@
|
||||
<os-head-bar [nav]="false" [mainButton]="true" (mainEvent)="onNewStateButton()">
|
||||
<os-head-bar [nav]="false" [hasMainButton]="true" (mainEvent)="onNewStateButton()">
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 *ngIf="workflow">
|
||||
|
@ -1,4 +1,4 @@
|
||||
<os-head-bar prevUrl="../.." [nav]="false" [mainButton]="true" (mainEvent)="onNewButton(newWorkflowDialog)">
|
||||
<os-head-bar prevUrl="../.." [nav]="false" [hasMainButton]="true" (mainEvent)="onNewButton(newWorkflowDialog)">
|
||||
<!-- Title -->
|
||||
<div class="title-slot"><h2 translate>Workflows</h2></div>
|
||||
</os-head-bar>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<os-head-bar prevUrl="../.." [nav]="false" [mainButton]="true" (mainEvent)="openDialog()">
|
||||
<os-head-bar prevUrl="../.." [nav]="false" [hasMainButton]="true" (mainEvent)="openDialog()">
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 translate>Statute</h2>
|
||||
@ -54,7 +54,7 @@
|
||||
</button>
|
||||
<button mat-menu-item *osPerms="'motions.can_manage'" routerLink="import">
|
||||
<mat-icon>cloud_upload</mat-icon>
|
||||
<span translate>Import</span><span> ...</span>
|
||||
<span translate>Import</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
<os-head-bar [nav]="true" [mainButton]="canManage" (mainEvent)="onPlusButton()">
|
||||
<os-head-bar [nav]="true" [hasMainButton]="canManage" (mainEvent)="onPlusButton()">
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 translate>Projectors</h2>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<os-head-bar [mainButton]="true" [nav]="true" (mainEvent)="openTagDialog()" [multiSelectMode]="isMultiSelect">
|
||||
<os-head-bar [hasMainButton]="true" [nav]="true" (mainEvent)="openTagDialog()" [multiSelectMode]="isMultiSelect">
|
||||
<div class="title-slot"><h2 translate>Tags</h2></div>
|
||||
</os-head-bar>
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<os-head-bar
|
||||
[mainButton]="isAllowed('edit')"
|
||||
[hasMainButton]="isAllowed('edit')"
|
||||
mainButtonIcon="edit"
|
||||
[nav]="false"
|
||||
[goBack]="true"
|
||||
|
@ -1,4 +1,4 @@
|
||||
<os-head-bar [mainButton]="true" [nav]="false" (mainEvent)="setEditMode(!editGroup)">
|
||||
<os-head-bar [hasMainButton]="true" [nav]="false" (mainEvent)="setEditMode(!editGroup)">
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 translate>Groups</h2>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<os-head-bar (mainEvent)="goBack()" [mainButton]="true" [nav]="false" [editMode]="true" (saveEvent)="save()">a
|
||||
<os-head-bar (mainEvent)="goBack()" [hasMainButton]="true" [nav]="false" [editMode]="true" (saveEvent)="save()">a
|
||||
<!-- Title -->
|
||||
<div class="title-slot"><h2 translate>Change password</h2></div>
|
||||
</os-head-bar>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<os-head-bar [mainButton]="false" [nav]="false">
|
||||
<os-head-bar [hasMainButton]="false" [nav]="false">
|
||||
<!-- Title -->
|
||||
<div class="title-slot"><h2 translate>Presence</h2></div>
|
||||
</os-head-bar>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<os-head-bar
|
||||
[mainButton]="isAllowed('changePersonal')"
|
||||
[hasMainButton]="isAllowed('changePersonal')"
|
||||
mainButtonIcon="edit"
|
||||
[nav]="false"
|
||||
[goBack]="!isAllowed('seeOtherUsers')"
|
||||
|
@ -1,4 +1,4 @@
|
||||
<os-head-bar [mainButton]="canAddUser" (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect">
|
||||
<os-head-bar [hasMainButton]="canAddUser" (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect">
|
||||
<!-- Title -->
|
||||
<div class="title-slot"><h2 translate>Participants</h2></div>
|
||||
|
||||
@ -124,9 +124,9 @@
|
||||
|
||||
<!-- Presence -->
|
||||
<button mat-menu-item (click)="setPresent(user)">
|
||||
<mat-icon color="accent"> {{ user.is_present ? 'check_box' : 'check_box_outline_blank' }} </mat-icon>
|
||||
<span translate>Present</span>
|
||||
</button>
|
||||
<mat-icon color="accent"> {{ user.is_present ? 'check_box' : 'check_box_outline_blank' }} </mat-icon>
|
||||
<span translate>Present</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
</mat-menu>
|
||||
|
||||
@ -166,7 +166,15 @@
|
||||
|
||||
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="import">
|
||||
<mat-icon>cloud_upload</mat-icon>
|
||||
<span translate>Import</span><span> ...</span>
|
||||
<span translate>Import</span>
|
||||
</button>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<!-- Settings -->
|
||||
<button mat-menu-item *osPerms="'core.can_manage_config'" routerLink="/settings/participants">
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span translate>Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="isMultiSelect">
|
||||
@ -199,11 +207,6 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="import">
|
||||
<mat-icon>save_alt</mat-icon>
|
||||
<span translate>Import</span><span> ...</span>
|
||||
</button>
|
||||
|
||||
<div *osPerms="'users.can_manage'">
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
|
@ -34,7 +34,8 @@
|
||||
}
|
||||
|
||||
//custom table header for search button, filtering and more. Used in ListViews
|
||||
.custom-table-header {
|
||||
.custom-table-header,
|
||||
.background--default {
|
||||
background: mat-color($background, background);
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,6 @@ def get_config_variables():
|
||||
help_text="Input format: DD.MM.YYYY HH:MM",
|
||||
weight=200,
|
||||
group="Agenda",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
@ -30,7 +29,6 @@ def get_config_variables():
|
||||
label="Show subtitles in the agenda",
|
||||
weight=201,
|
||||
group="Agenda",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
# Numbering
|
||||
|
@ -1,8 +1,6 @@
|
||||
import os
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from operator import attrgetter
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
@ -127,8 +125,6 @@ class CoreAppConfig(AppConfig):
|
||||
yield self.get_model(model_name)
|
||||
|
||||
def get_angular_constants(self):
|
||||
from .config import config
|
||||
|
||||
constants: Dict[str, Any] = {}
|
||||
|
||||
# Client settings
|
||||
@ -147,34 +143,7 @@ class CoreAppConfig(AppConfig):
|
||||
pass
|
||||
constants["Settings"] = client_settings_dict
|
||||
|
||||
# Config variables
|
||||
config_groups: List[Any] = []
|
||||
for config_variable in sorted(
|
||||
config.config_variables.values(), key=attrgetter("weight")
|
||||
):
|
||||
if config_variable.is_hidden():
|
||||
# Skip hidden config variables. Do not even check groups and subgroups.
|
||||
continue
|
||||
if not config_groups or config_groups[-1]["name"] != config_variable.group:
|
||||
# Add new group.
|
||||
config_groups.append(
|
||||
OrderedDict(name=config_variable.group, subgroups=[])
|
||||
)
|
||||
if (
|
||||
not config_groups[-1]["subgroups"]
|
||||
or config_groups[-1]["subgroups"][-1]["name"]
|
||||
!= config_variable.subgroup
|
||||
):
|
||||
# Add new subgroup.
|
||||
config_groups[-1]["subgroups"].append(
|
||||
OrderedDict(name=config_variable.subgroup, items=[])
|
||||
)
|
||||
# Add the config variable to the current group and subgroup.
|
||||
config_groups[-1]["subgroups"][-1]["items"].append(config_variable.data)
|
||||
constants["ConfigVariables"] = config_groups
|
||||
|
||||
constants["SchemaVersion"] = schema_version_handler.get()
|
||||
|
||||
return constants
|
||||
|
||||
|
||||
|
@ -24,6 +24,8 @@ INPUT_TYPE_MAPPING = {
|
||||
"translations": list,
|
||||
}
|
||||
|
||||
ALLOWED_NONE = ("datetimepicker",)
|
||||
|
||||
build_key_to_id_lock = asyncio.Lock()
|
||||
|
||||
|
||||
@ -119,12 +121,15 @@ class ConfigHandler:
|
||||
expected_type = INPUT_TYPE_MAPPING[config_variable.input_type]
|
||||
|
||||
# Try to convert value into the expected datatype
|
||||
try:
|
||||
value = expected_type(value)
|
||||
except ValueError:
|
||||
raise ConfigError(
|
||||
f"Wrong datatype. Expected {expected_type}, got {type(value)}."
|
||||
)
|
||||
if value is None and config_variable.input_type not in ALLOWED_NONE:
|
||||
raise ConfigError(f"Got None for {key}")
|
||||
elif value is not None:
|
||||
try:
|
||||
value = expected_type(value)
|
||||
except (ValueError, TypeError):
|
||||
raise ConfigError(
|
||||
f"Wrong datatype. Expected {expected_type}, got {type(value)}."
|
||||
)
|
||||
|
||||
if config_variable.input_type == "choice":
|
||||
# Choices can be a callable. In this case call it at this place
|
||||
@ -267,12 +272,14 @@ OnChangeType = Callable[[], None]
|
||||
ConfigVariableDict = TypedDict(
|
||||
"ConfigVariableDict",
|
||||
{
|
||||
"key": str,
|
||||
"default_value": Any,
|
||||
"input_type": str,
|
||||
"defaultValue": Any,
|
||||
"inputType": str,
|
||||
"label": str,
|
||||
"help_text": str,
|
||||
"helpText": str,
|
||||
"choices": ChoiceType,
|
||||
"weight": int,
|
||||
"group": str,
|
||||
"subgroup": Optional[str],
|
||||
},
|
||||
)
|
||||
|
||||
@ -314,8 +321,8 @@ class ConfigVariable:
|
||||
choices: ChoiceCallableType = None,
|
||||
hidden: bool = False,
|
||||
weight: int = 0,
|
||||
group: str = None,
|
||||
subgroup: str = None,
|
||||
group: str = "General",
|
||||
subgroup: str = "General",
|
||||
validators: ValidatorsType = None,
|
||||
on_change: OnChangeType = None,
|
||||
) -> None:
|
||||
@ -339,28 +346,26 @@ class ConfigVariable:
|
||||
self.choices = choices
|
||||
self.hidden = hidden
|
||||
self.weight = weight
|
||||
self.group = group or "General"
|
||||
self.group = group
|
||||
self.subgroup = subgroup
|
||||
self.validators = validators or ()
|
||||
self.on_change = on_change
|
||||
|
||||
@property
|
||||
def data(self) -> ConfigVariableDict:
|
||||
def data(self) -> Optional[ConfigVariableDict]:
|
||||
"""
|
||||
Property with all data for AngularJS variable on startup.
|
||||
Property with all data for Angular variable on startup.
|
||||
"""
|
||||
return ConfigVariableDict(
|
||||
key=self.name,
|
||||
default_value=self.default_value,
|
||||
input_type=self.input_type,
|
||||
label=self.label,
|
||||
help_text=self.help_text,
|
||||
choices=self.choices() if callable(self.choices) else self.choices,
|
||||
)
|
||||
if self.hidden:
|
||||
return None
|
||||
|
||||
def is_hidden(self) -> bool:
|
||||
"""
|
||||
Returns True if the config variable is hidden so it can be removed
|
||||
from response of OPTIONS request.
|
||||
"""
|
||||
return self.hidden
|
||||
return ConfigVariableDict(
|
||||
defaultValue=self.default_value,
|
||||
inputType=self.input_type,
|
||||
label=self.label,
|
||||
helpText=self.help_text,
|
||||
choices=self.choices() if callable(self.choices) else self.choices,
|
||||
weight=self.weight,
|
||||
group=self.group,
|
||||
subgroup=self.subgroup,
|
||||
)
|
||||
|
@ -18,7 +18,6 @@ def get_config_variables():
|
||||
default_value="OpenSlides",
|
||||
label="Event name",
|
||||
weight=110,
|
||||
group="General",
|
||||
subgroup="Event",
|
||||
validators=(MaxLengthValidator(100),),
|
||||
)
|
||||
@ -28,7 +27,6 @@ def get_config_variables():
|
||||
default_value="Presentation and assembly system",
|
||||
label="Short description of event",
|
||||
weight=115,
|
||||
group="General",
|
||||
subgroup="Event",
|
||||
validators=(MaxLengthValidator(100),),
|
||||
)
|
||||
@ -38,7 +36,6 @@ def get_config_variables():
|
||||
default_value="",
|
||||
label="Event date",
|
||||
weight=120,
|
||||
group="General",
|
||||
subgroup="Event",
|
||||
)
|
||||
|
||||
@ -47,7 +44,6 @@ def get_config_variables():
|
||||
default_value="",
|
||||
label="Event location",
|
||||
weight=125,
|
||||
group="General",
|
||||
subgroup="Event",
|
||||
)
|
||||
|
||||
@ -60,7 +56,6 @@ def get_config_variables():
|
||||
input_type="markupText",
|
||||
label="Legal notice",
|
||||
weight=132,
|
||||
group="General",
|
||||
subgroup="Event",
|
||||
)
|
||||
|
||||
@ -70,7 +65,6 @@ def get_config_variables():
|
||||
input_type="markupText",
|
||||
label="Privacy policy",
|
||||
weight=132,
|
||||
group="General",
|
||||
subgroup="Event",
|
||||
)
|
||||
|
||||
@ -79,7 +73,6 @@ def get_config_variables():
|
||||
default_value="Welcome to OpenSlides",
|
||||
label="Front page title",
|
||||
weight=134,
|
||||
group="General",
|
||||
subgroup="Event",
|
||||
)
|
||||
|
||||
@ -89,7 +82,6 @@ def get_config_variables():
|
||||
input_type="markupText",
|
||||
label="Front page text",
|
||||
weight=136,
|
||||
group="General",
|
||||
subgroup="Event",
|
||||
)
|
||||
|
||||
@ -101,7 +93,6 @@ def get_config_variables():
|
||||
input_type="boolean",
|
||||
label="Allow access for anonymous guest users",
|
||||
weight=138,
|
||||
group="General",
|
||||
subgroup="System",
|
||||
)
|
||||
|
||||
@ -110,7 +101,6 @@ def get_config_variables():
|
||||
default_value="",
|
||||
label="Show this text on the login page",
|
||||
weight=140,
|
||||
group="General",
|
||||
subgroup="System",
|
||||
)
|
||||
|
||||
@ -129,7 +119,6 @@ def get_config_variables():
|
||||
},
|
||||
),
|
||||
weight=141,
|
||||
group="General",
|
||||
subgroup="System",
|
||||
)
|
||||
|
||||
@ -140,7 +129,6 @@ def get_config_variables():
|
||||
default_value=",",
|
||||
label="Separator used for all csv exports and examples",
|
||||
weight=160,
|
||||
group="General",
|
||||
subgroup="Export",
|
||||
)
|
||||
|
||||
@ -154,7 +142,6 @@ def get_config_variables():
|
||||
{"value": "iso-8859-15", "display_name": "ISO-8859-15"},
|
||||
),
|
||||
weight=162,
|
||||
group="General",
|
||||
subgroup="Export",
|
||||
)
|
||||
|
||||
@ -169,7 +156,6 @@ def get_config_variables():
|
||||
{"value": "right", "display_name": "Right"},
|
||||
),
|
||||
weight=164,
|
||||
group="General",
|
||||
subgroup="Export",
|
||||
)
|
||||
|
||||
@ -184,7 +170,6 @@ def get_config_variables():
|
||||
{"value": "12", "display_name": "12"},
|
||||
),
|
||||
weight=166,
|
||||
group="General",
|
||||
subgroup="Export",
|
||||
)
|
||||
|
||||
@ -198,7 +183,6 @@ def get_config_variables():
|
||||
{"value": "A5", "display_name": "DIN A5"},
|
||||
),
|
||||
weight=168,
|
||||
group="General",
|
||||
subgroup="Export",
|
||||
)
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
from typing import Any
|
||||
|
||||
from ..core.config import config
|
||||
from ..utils.projector import projector_slides
|
||||
from ..utils.rest_api import (
|
||||
Field,
|
||||
IdPrimaryKeyRelatedField,
|
||||
IntegerField,
|
||||
ModelSerializer,
|
||||
SerializerMethodField,
|
||||
ValidationError,
|
||||
)
|
||||
from ..utils.validate import validate_html
|
||||
@ -136,10 +138,14 @@ class ConfigSerializer(ModelSerializer):
|
||||
"""
|
||||
|
||||
value = JSONSerializerField()
|
||||
data = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ConfigStore
|
||||
fields = ("id", "key", "value")
|
||||
fields = ("id", "key", "value", "data")
|
||||
|
||||
def get_data(self, db_config):
|
||||
return config.config_variables[db_config.key].data
|
||||
|
||||
|
||||
class ProjectorMessageSerializer(ModelSerializer):
|
||||
|
@ -37,6 +37,7 @@ from ..utils.rest_api import (
|
||||
RetrieveModelMixin,
|
||||
ValidationError,
|
||||
detail_route,
|
||||
list_route,
|
||||
)
|
||||
from .access_permissions import (
|
||||
ConfigAccessPermissions,
|
||||
@ -390,30 +391,46 @@ class ConfigViewSet(ModelViewSet):
|
||||
access_permissions = ConfigAccessPermissions()
|
||||
queryset = ConfigStore.objects.all()
|
||||
|
||||
can_manage_config = None
|
||||
can_manage_logos_and_fonts = None
|
||||
|
||||
def check_view_permissions(self):
|
||||
"""
|
||||
Returns True if the user has required permissions.
|
||||
"""
|
||||
if self.action in ("list", "retrieve"):
|
||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||
elif self.action == "metadata":
|
||||
# Every authenticated user can see the metadata and list or
|
||||
# retrieve the config. Anonymous users can do so if they are
|
||||
# enabled.
|
||||
result = self.request.user.is_authenticated or anonymous_is_enabled()
|
||||
elif self.action in ("partial_update", "update"):
|
||||
# The user needs 'core.can_manage_logos_and_fonts' for all config values
|
||||
# starting with 'logo' and 'font'. For all other config values th euser needs
|
||||
# the default permissions 'core.can_manage_config'.
|
||||
pk = self.kwargs["pk"]
|
||||
if pk.startswith("logo") or pk.startswith("font"):
|
||||
result = has_perm(self.request.user, "core.can_manage_logos_and_fonts")
|
||||
else:
|
||||
result = has_perm(self.request.user, "core.can_manage_config")
|
||||
result = self.check_config_permission(self.kwargs["pk"])
|
||||
elif self.action == "reset_groups":
|
||||
result = has_perm(self.request.user, "core.can_manage_config")
|
||||
elif self.action == "bulk_update":
|
||||
result = True # will be checked in the view
|
||||
else:
|
||||
result = False
|
||||
return result
|
||||
|
||||
def check_config_permission(self, key):
|
||||
"""
|
||||
Checks the permissions for one config key.
|
||||
Users needs 'core.can_manage_logos_and_fonts' for all config values starting
|
||||
with 'logo' and 'font'. For all other config values the user needs the default
|
||||
permissions 'core.can_manage_config'.
|
||||
The result is cached for one request to reduce has_perm queries in e.g. bulk updates.
|
||||
"""
|
||||
if key.startswith("logo") or key.startswith("font"):
|
||||
if self.can_manage_logos_and_fonts is None:
|
||||
self.can_manage_logos_and_fonts = has_perm(
|
||||
self.request.user, "core.can_manage_logos_and_fonts"
|
||||
)
|
||||
return self.can_manage_logos_and_fonts
|
||||
else:
|
||||
if self.can_manage_config is None:
|
||||
self.can_manage_config = has_perm(
|
||||
self.request.user, "core.can_manage_config"
|
||||
)
|
||||
return self.can_manage_config
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
"""
|
||||
Updates a config variable. Only managers can do this.
|
||||
@ -422,8 +439,6 @@ class ConfigViewSet(ModelViewSet):
|
||||
"""
|
||||
key = kwargs["pk"]
|
||||
value = request.data.get("value")
|
||||
if value is None:
|
||||
raise ValidationError({"detail": "Invalid input. Config value is missing."})
|
||||
|
||||
# Validate and change value.
|
||||
try:
|
||||
@ -436,6 +451,60 @@ class ConfigViewSet(ModelViewSet):
|
||||
# Return response.
|
||||
return Response({"key": key, "value": value})
|
||||
|
||||
@list_route(methods=["post"])
|
||||
def bulk_update(self, request):
|
||||
"""
|
||||
Updates many config variables:
|
||||
[{key: <key>, value: <value>}, ...]
|
||||
"""
|
||||
if not isinstance(request.data, list):
|
||||
raise ValidationError({"detail": "The data needs to be a list"})
|
||||
|
||||
for entry in request.data:
|
||||
key = entry.get("key")
|
||||
if not isinstance(key, str):
|
||||
raise ValidationError({"detail": "The key must be a string."})
|
||||
if not config.exists(key):
|
||||
raise ValidationError(
|
||||
{"detail": "The key {0} does not exist.", "args": [key]}
|
||||
)
|
||||
if not self.check_config_permission(key):
|
||||
self.permission_denied(request, message=key)
|
||||
if "value" not in entry:
|
||||
raise ValidationError(
|
||||
{"detail": "Invalid input. Config value is missing."}
|
||||
)
|
||||
|
||||
errors = {}
|
||||
for entry in request.data:
|
||||
try:
|
||||
config[entry["key"]] = entry["value"]
|
||||
except ConfigError as err:
|
||||
errors[entry["key"]] = str(err)
|
||||
|
||||
return Response({"errors": errors})
|
||||
|
||||
@list_route(methods=["post"])
|
||||
def reset_groups(self, request):
|
||||
"""
|
||||
Resets multiple groups. The request data contains all
|
||||
(main) group names: [<group1>, ...]
|
||||
"""
|
||||
if not isinstance(request.data, list):
|
||||
raise ValidationError("The data must be a list")
|
||||
for group in request.data:
|
||||
if not isinstance(group, str):
|
||||
raise ValidationError("Every group must be a string")
|
||||
|
||||
for key, config_variable in config.config_variables.items():
|
||||
if (
|
||||
config_variable.group in request.data
|
||||
and config[key] != config_variable.default_value
|
||||
):
|
||||
config[key] = config_variable.default_value
|
||||
|
||||
return Response()
|
||||
|
||||
|
||||
class ProjectorMessageViewSet(ModelViewSet):
|
||||
"""
|
||||
|
@ -35,7 +35,6 @@ def get_config_variables():
|
||||
choices=get_workflow_choices,
|
||||
weight=310,
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
@ -46,7 +45,6 @@ def get_config_variables():
|
||||
choices=get_workflow_choices,
|
||||
weight=312,
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
@ -55,7 +53,6 @@ def get_config_variables():
|
||||
label="Motion preamble",
|
||||
weight=320,
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
@ -70,7 +67,6 @@ def get_config_variables():
|
||||
),
|
||||
weight=322,
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
@ -81,7 +77,6 @@ def get_config_variables():
|
||||
help_text="The maximum number of characters per line. Relevant when line numbering is enabled. Min: 40",
|
||||
weight=323,
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
validators=(MinValueValidator(40),),
|
||||
)
|
||||
|
||||
@ -92,7 +87,6 @@ def get_config_variables():
|
||||
label="Reason required for creating new motion",
|
||||
weight=324,
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
@ -102,7 +96,6 @@ def get_config_variables():
|
||||
label="Hide motion text on projector",
|
||||
weight=325,
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
@ -112,7 +105,6 @@ def get_config_variables():
|
||||
label="Hide reason on projector",
|
||||
weight=326,
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
@ -122,7 +114,6 @@ def get_config_variables():
|
||||
label="Hide meta information box on projector",
|
||||
weight=327,
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
@ -132,7 +123,6 @@ def get_config_variables():
|
||||
label="Hide recommendation on projector",
|
||||
weight=328,
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
@ -142,7 +132,6 @@ def get_config_variables():
|
||||
label="Hide referring motions",
|
||||
weight=329,
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
@ -153,7 +142,6 @@ def get_config_variables():
|
||||
help_text="In motion list, motion detail and PDF.",
|
||||
weight=330,
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
@ -163,7 +151,6 @@ def get_config_variables():
|
||||
help_text="Will be displayed as label before selected recommendation. Use an empty value to disable the recommendation system.",
|
||||
weight=332,
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
@ -173,7 +160,6 @@ def get_config_variables():
|
||||
help_text="Will be displayed as label before selected recommendation in statute amendments.",
|
||||
weight=333,
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
@ -189,7 +175,6 @@ def get_config_variables():
|
||||
),
|
||||
weight=334,
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
@ -203,7 +188,6 @@ def get_config_variables():
|
||||
),
|
||||
weight=335,
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
# Numbering
|
||||
|
@ -23,7 +23,6 @@ def get_config_variables():
|
||||
),
|
||||
weight=510,
|
||||
group="Participants",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
@ -33,7 +32,6 @@ def get_config_variables():
|
||||
label="Enable participant presence view",
|
||||
weight=511,
|
||||
group="Participants",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
# PDF
|
||||
|
@ -223,7 +223,7 @@ class ConfigViewSet(TestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(
|
||||
response.data, {"detail": "Invalid input. Config value is missing."}
|
||||
response.data, {"detail": "Got None for test_var_Xeiizi7ooH8Thuk5aida"}
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,4 +1,9 @@
|
||||
import random
|
||||
import string
|
||||
|
||||
import pytest
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
@ -6,6 +11,7 @@ from rest_framework.test import APIClient
|
||||
from openslides.core.config import config
|
||||
from openslides.core.models import Projector, Tag
|
||||
from openslides.users.models import User
|
||||
from openslides.utils.auth import get_group_model
|
||||
from openslides.utils.autoupdate import inform_changed_data
|
||||
from openslides.utils.test import TestCase
|
||||
from tests.common_groups import GROUP_ADMIN_PK, GROUP_DELEGATE_PK
|
||||
@ -187,3 +193,204 @@ class Projection(TestCase):
|
||||
self.assertEqual(self.projector.elements, [])
|
||||
self.assertEqual(self.projector.elements_preview, elements)
|
||||
self.assertEqual(self.projector.elements_history, [])
|
||||
|
||||
|
||||
class ConfigViewSet(TestCase):
|
||||
"""
|
||||
Tests (currently just parts) of the ProjectorViewSet.
|
||||
"""
|
||||
|
||||
string_config_key = "general_event_name"
|
||||
"""
|
||||
The config used for testing. It should accept string.
|
||||
"""
|
||||
logo_config_key = "logo_web_header"
|
||||
|
||||
def random_string(self):
|
||||
return "".join(
|
||||
random.choice(string.ascii_letters + string.digits) for i in range(20)
|
||||
)
|
||||
|
||||
def get_static_config_value(self):
|
||||
return {
|
||||
"path": f"test_path_{self.random_string()}",
|
||||
"display_name": f"test_display_name_{self.random_string()}",
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.client.login(username="admin", password="admin")
|
||||
|
||||
def test_create(self):
|
||||
response = self.client.post(
|
||||
reverse("config-list"), {"key": "test_key_fj3f2oqsjcqpsjclqwoO"}
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(config.exists("test_key_fj3f2oqsjcqpsjclqwoO"))
|
||||
|
||||
def test_delete(self):
|
||||
response = self.client.delete(
|
||||
reverse("config-detail", args=[self.string_config_key])
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertTrue(config.exists(self.string_config_key))
|
||||
|
||||
def test_update(self):
|
||||
response = self.client.put(
|
||||
reverse("config-detail", args=[self.string_config_key]),
|
||||
{"value": "test_name_39gw4cishcvev2acoqnw"},
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
config[self.string_config_key], "test_name_39gw4cishcvev2acoqnw"
|
||||
)
|
||||
|
||||
def test_set_none(self):
|
||||
"""
|
||||
The agenda_start_event_date_time is of type "datepicker" which
|
||||
can be set to None
|
||||
"""
|
||||
response = self.client.put(
|
||||
reverse("config-detail", args=["agenda_start_event_date_time"]),
|
||||
{"value": None},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(config["agenda_start_event_date_time"], None)
|
||||
|
||||
def test_set_invalid_none(self):
|
||||
"""
|
||||
Try to set motions_identifier_min_digits to None, which should fail
|
||||
"""
|
||||
response = self.client.put(
|
||||
reverse("config-detail", args=["motions_identifier_min_digits"]),
|
||||
{"value": None},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def degrade_admin(self, can_manage_config=False, can_manage_logos_and_fonts=False):
|
||||
admin = get_user_model().objects.get(username="admin")
|
||||
admin.groups.remove(GROUP_ADMIN_PK)
|
||||
admin.groups.add(GROUP_DELEGATE_PK)
|
||||
if can_manage_config or can_manage_logos_and_fonts:
|
||||
delegate_group = get_group_model().objects.get(pk=GROUP_DELEGATE_PK)
|
||||
if can_manage_config:
|
||||
delegate_group.permissions.add(
|
||||
Permission.objects.get(
|
||||
content_type__app_label="core", codename="can_manage_config"
|
||||
)
|
||||
)
|
||||
if can_manage_logos_and_fonts:
|
||||
delegate_group.permissions.add(
|
||||
Permission.objects.get(
|
||||
content_type__app_label="core",
|
||||
codename="can_manage_logos_and_fonts",
|
||||
)
|
||||
)
|
||||
inform_changed_data(delegate_group)
|
||||
inform_changed_data(admin)
|
||||
|
||||
def test_update_no_permissions(self):
|
||||
self.degrade_admin()
|
||||
response = self.client.put(
|
||||
reverse("config-detail", args=[self.string_config_key]),
|
||||
{"value": "test_name_vp2sjjf29jswlvwaxwre"},
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
||||
|
||||
def test_update_logo_no_config_permissions(self):
|
||||
self.degrade_admin(can_manage_logos_and_fonts=True)
|
||||
value = self.get_static_config_value()
|
||||
response = self.client.put(
|
||||
reverse("config-detail", args=[self.logo_config_key]),
|
||||
{"value": value},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(config[self.logo_config_key], value)
|
||||
|
||||
def test_bulk_update(self):
|
||||
string_value = "test_value_k2jqvjwrorepjadvpo2J"
|
||||
logo_value = self.get_static_config_value()
|
||||
response = self.client.post(
|
||||
reverse("config-bulk-update"),
|
||||
[
|
||||
{"key": self.string_config_key, "value": string_value},
|
||||
{"key": self.logo_config_key, "value": logo_value},
|
||||
],
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["errors"], {})
|
||||
self.assertEqual(config[self.string_config_key], string_value)
|
||||
self.assertEqual(config[self.logo_config_key], logo_value)
|
||||
|
||||
def test_bulk_update_no_perm(self):
|
||||
self.degrade_admin()
|
||||
string_value = "test_value_gjscneuqoscmqf2qow91"
|
||||
response = self.client.post(
|
||||
reverse("config-bulk-update"),
|
||||
[{"key": self.string_config_key, "value": string_value}],
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
||||
|
||||
def test_bulk_update_no_list(self):
|
||||
string_value = "test_value_fjewqpqayqfijnqm%cqi"
|
||||
response = self.client.post(
|
||||
reverse("config-bulk-update"),
|
||||
{"key": self.string_config_key, "value": string_value},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
||||
|
||||
def test_bulk_update_no_key(self):
|
||||
string_value = "test_value_glwe32qc&Lml2lclmqmc"
|
||||
response = self.client.post(
|
||||
reverse("config-bulk-update"), [{"value": string_value}], format="json"
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
||||
|
||||
def test_bulk_update_no_value(self):
|
||||
response = self.client.post(
|
||||
reverse("config-bulk-update"),
|
||||
[{"key": self.string_config_key}],
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
||||
|
||||
def test_reset_group(self):
|
||||
config["general_event_name"] = "test_name_of20w2fj20clqwcm2pij" # Group General
|
||||
config["agenda_show_subtitle"] = True # Group Agenda
|
||||
config[
|
||||
"motions_preamble"
|
||||
] = "test_preamble_2390jvwohjwo1oigefoq" # Group motions
|
||||
response = self.client.post(
|
||||
reverse("config-reset-groups"), ["General", "Agenda"], format="json"
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(config["general_event_name"], "OpenSlides")
|
||||
self.assertEqual(config["agenda_show_subtitle"], False)
|
||||
self.assertEqual(
|
||||
config["motions_preamble"], "test_preamble_2390jvwohjwo1oigefoq"
|
||||
)
|
||||
|
||||
def test_reset_group_wrong_format_1(self):
|
||||
response = self.client.post(
|
||||
reverse("config-reset-groups"), {"wrong": "format"}, format="json"
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_reset_group_wrong_format_2(self):
|
||||
response = self.client.post(
|
||||
reverse("config-reset-groups"),
|
||||
["some_string", {"wrong": "format"}],
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
@ -1,7 +1,8 @@
|
||||
from typing import cast
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from openslides.core.config import ConfigVariable, config
|
||||
from openslides.core.config import ConfigVariable, ConfigVariableDict, config
|
||||
from openslides.core.exceptions import ConfigNotFound
|
||||
|
||||
|
||||
@ -14,15 +15,15 @@ class TestConfigVariable(TestCase):
|
||||
"""
|
||||
config_variable = ConfigVariable("test_variable", "test_default_value")
|
||||
|
||||
self.assertIn(
|
||||
"default_value",
|
||||
config_variable.data,
|
||||
"Config_varialbe.data should have a key 'default_value'",
|
||||
self.assertTrue(
|
||||
"defaultValue" in cast(ConfigVariableDict, config_variable.data)
|
||||
)
|
||||
data = config_variable.data
|
||||
self.assertTrue(data)
|
||||
self.assertEqual(
|
||||
config_variable.data["default_value"],
|
||||
cast(ConfigVariableDict, config_variable.data)["defaultValue"],
|
||||
"test_default_value",
|
||||
"The value of config_variable.data['default_value'] should be the same "
|
||||
"The value of config_variable.data['defaultValue'] should be the same "
|
||||
"as set as second argument of ConfigVariable()",
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user