Merge pull request #4759 from FinnStutzenstein/optionalAgendaItem
Optional agenda items
This commit is contained in:
commit
3d573441ca
@ -1,7 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { map } from 'rxjs/operators';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
@ -14,7 +12,8 @@ import { TreeIdNode } from 'app/core/ui-services/tree.service';
|
|||||||
import { ViewItem, ItemTitleInformation } from 'app/site/agenda/models/view-item';
|
import { ViewItem, ItemTitleInformation } from 'app/site/agenda/models/view-item';
|
||||||
import {
|
import {
|
||||||
BaseViewModelWithAgendaItem,
|
BaseViewModelWithAgendaItem,
|
||||||
isBaseViewModelWithAgendaItem
|
isBaseViewModelWithAgendaItem,
|
||||||
|
IBaseViewModelWithAgendaItem
|
||||||
} from 'app/site/base/base-view-model-with-agenda-item';
|
} from 'app/site/base/base-view-model-with-agenda-item';
|
||||||
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
import { BaseViewModel } from 'app/site/base/base-view-model';
|
import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||||
@ -24,6 +23,7 @@ import { Topic } from 'app/shared/models/topics/topic';
|
|||||||
import { Assignment } from 'app/shared/models/assignments/assignment';
|
import { Assignment } from 'app/shared/models/assignments/assignment';
|
||||||
import { BaseIsAgendaItemContentObjectRepository } from '../base-is-agenda-item-content-object-repository';
|
import { BaseIsAgendaItemContentObjectRepository } from '../base-is-agenda-item-content-object-repository';
|
||||||
import { BaseHasContentObjectRepository } from '../base-has-content-object-repository';
|
import { BaseHasContentObjectRepository } from '../base-has-content-object-repository';
|
||||||
|
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository service for items
|
* Repository service for items
|
||||||
@ -105,16 +105,10 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository<
|
|||||||
agendaItem.content_object.collection,
|
agendaItem.content_object.collection,
|
||||||
agendaItem.content_object.id
|
agendaItem.content_object.id
|
||||||
);
|
);
|
||||||
if (!contentObject) {
|
if (!contentObject || !isBaseViewModelWithAgendaItem(contentObject)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (isBaseViewModelWithAgendaItem(contentObject)) {
|
|
||||||
return contentObject;
|
return contentObject;
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`The content object (${agendaItem.content_object.collection}, ${agendaItem.content_object.id}) of item ${agendaItem.id} is not a BaseAgendaItemViewModel.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -124,19 +118,6 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository<
|
|||||||
await this.httpService.post('/rest/agenda/item/numbering/');
|
await this.httpService.post('/rest/agenda/item/numbering/');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @ignore
|
|
||||||
*
|
|
||||||
* TODO: Usually, agenda items are deleted with their corresponding content object
|
|
||||||
* However, deleting an agenda item might be interpretet with "removing an item
|
|
||||||
* from the agenda" permanently. Usually, items might juse be hidden but not
|
|
||||||
* deleted (right now)
|
|
||||||
*/
|
|
||||||
public async delete(item: ViewItem): Promise<void> {
|
|
||||||
const restUrl = `/rest/${item.contentObject.collectionString}/${item.contentObject.id}/`;
|
|
||||||
await this.httpService.delete(restUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Copied from BaseRepository and added the cloned model to write back the
|
* TODO: Copied from BaseRepository and added the cloned model to write back the
|
||||||
* item_number correctly. This must be reversed with #4738 (indroduced with #4639)
|
* item_number correctly. This must be reversed with #4738 (indroduced with #4639)
|
||||||
@ -158,13 +139,23 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository<
|
|||||||
return await this.httpService.put(restPath, clone);
|
return await this.httpService.put(restPath, clone);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public async addItemToAgenda(contentObject: IBaseViewModelWithAgendaItem<any>): Promise<Identifiable> {
|
||||||
* Get agenda visibility from the config
|
return await this.httpService.post('/rest/agenda/item/', {
|
||||||
*
|
collection: contentObject.collectionString,
|
||||||
* @return An observable to the default agenda visibility
|
id: contentObject.id
|
||||||
*/
|
});
|
||||||
public getDefaultAgendaVisibility(): Observable<number> {
|
}
|
||||||
return this.config.get('agenda_new_items_default_visibility').pipe(map(key => +key));
|
|
||||||
|
public async removeFromAgenda(item: ViewItem): Promise<void> {
|
||||||
|
return await this.httpService.delete(`/rest/agenda/item/${item.id}/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async create(item: Item): Promise<Identifiable> {
|
||||||
|
throw new Error('Use `addItemToAgenda` for creations');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delete(item: ViewItem): Promise<void> {
|
||||||
|
throw new Error('Use `removeFromAgenda` for deletions');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -243,10 +243,10 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
const sendModel = new this.baseModelCtor();
|
const sendModel = new this.baseModelCtor();
|
||||||
sendModel.patchValues(model);
|
sendModel.patchValues(model);
|
||||||
|
|
||||||
// Strips empty fields from the sending mode data.
|
// Strips empty fields from the sending mode data (except false)
|
||||||
// required for i.e. users, since group list is mandatory
|
// required for i.e. users, since group list is mandatory
|
||||||
Object.keys(sendModel).forEach(key => {
|
Object.keys(sendModel).forEach(key => {
|
||||||
if (!sendModel[key]) {
|
if (!sendModel[key] && sendModel[key] !== false) {
|
||||||
delete sendModel[key];
|
delete sendModel[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
<ng-container *ngIf="showForm">
|
||||||
|
<div [formGroup]="form">
|
||||||
|
<mat-checkbox formControlName="agenda_create">
|
||||||
|
<span translate>Add to agenda</span>
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container *ngIf="!!checkbox.value">
|
||||||
|
<!-- Visibility -->
|
||||||
|
<div>
|
||||||
|
<mat-form-field [formGroup]="form">
|
||||||
|
<mat-select formControlName="agenda_type" placeholder="{{ 'Agenda visibility' | translate }}">
|
||||||
|
<mat-option *ngFor="let type of ItemVisibilityChoices" [value]="type.key">
|
||||||
|
<span>{{ type.name | translate }}</span>
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Parent item -->
|
||||||
|
<div *ngIf="itemObserver.value.length > 0">
|
||||||
|
<os-search-value-selector
|
||||||
|
ngDefaultControl
|
||||||
|
[form]="form"
|
||||||
|
[formControl]="form.get('agenda_parent_id')"
|
||||||
|
[multiple]="false"
|
||||||
|
[includeNone]="true"
|
||||||
|
listname="{{ 'Parent agenda item' | translate }}"
|
||||||
|
[InputListValues]="itemObserver"
|
||||||
|
></os-search-value-selector>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
@ -0,0 +1,33 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FormBuilder } from '@angular/forms';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
import { AgendaContentObjectFormComponent } from './agenda-content-object-form.component';
|
||||||
|
|
||||||
|
describe('AgendaContentObjectFormComponent', () => {
|
||||||
|
let component: AgendaContentObjectFormComponent;
|
||||||
|
let fixture: ComponentFixture<AgendaContentObjectFormComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AgendaContentObjectFormComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
const formBuilder: FormBuilder = TestBed.get(FormBuilder);
|
||||||
|
component.form = formBuilder.group({
|
||||||
|
agenda_create: [''],
|
||||||
|
agenda_parent_id: [],
|
||||||
|
agenda_type: ['']
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,66 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { FormGroup, FormControl } from '@angular/forms';
|
||||||
|
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
|
import { ViewItem } from 'app/site/agenda/models/view-item';
|
||||||
|
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
|
||||||
|
import { ItemVisibilityChoices } from 'app/shared/models/agenda/item';
|
||||||
|
|
||||||
|
type AgendaItemCreateChoices = 'always' | 'never' | 'default_yes' | 'default_no';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'os-agenda-content-object-form',
|
||||||
|
templateUrl: './agenda-content-object-form.component.html',
|
||||||
|
styleUrls: ['./agenda-content-object-form.component.scss']
|
||||||
|
})
|
||||||
|
export class AgendaContentObjectFormComponent implements OnInit {
|
||||||
|
@Input()
|
||||||
|
public form: FormGroup;
|
||||||
|
|
||||||
|
public showForm = false;
|
||||||
|
|
||||||
|
public checkbox: FormControl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine visibility states for the agenda that will be created implicitly
|
||||||
|
*/
|
||||||
|
public ItemVisibilityChoices = ItemVisibilityChoices;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subject for agenda items
|
||||||
|
*/
|
||||||
|
public itemObserver: BehaviorSubject<ViewItem[]>;
|
||||||
|
|
||||||
|
public constructor(private configService: ConfigService, private itemRepo: ItemRepositoryService) {}
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.checkbox = this.form.controls.agenda_create as FormControl;
|
||||||
|
|
||||||
|
this.configService.get<AgendaItemCreateChoices>('agenda_item_creation').subscribe(value => {
|
||||||
|
if (value === 'always') {
|
||||||
|
this.showForm = true;
|
||||||
|
this.checkbox.disable();
|
||||||
|
this.form.patchValue({ agenda_create: true });
|
||||||
|
} else if (value === 'never') {
|
||||||
|
this.showForm = false;
|
||||||
|
this.checkbox.disable();
|
||||||
|
this.form.patchValue({ agenda_create: false });
|
||||||
|
} else {
|
||||||
|
const defaultValue = value === 'default_yes';
|
||||||
|
// check if alrady touched..
|
||||||
|
this.showForm = true;
|
||||||
|
this.checkbox.enable();
|
||||||
|
this.form.patchValue({ agenda_create: defaultValue });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the default visibility using observers
|
||||||
|
this.configService.get('agenda_new_items_default_visibility').subscribe(visibility => {
|
||||||
|
this.form.get('agenda_type').setValue(+visibility);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.itemObserver = this.itemRepo.getViewModelListBehaviorSubject();
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@ import { BaseModelWithContentObject } from '../base/base-model-with-content-obje
|
|||||||
* Determine visibility states for agenda items
|
* Determine visibility states for agenda items
|
||||||
* Coming from "ConfigVariables" property "agenda_hide_internal_items_on_projector"
|
* Coming from "ConfigVariables" property "agenda_hide_internal_items_on_projector"
|
||||||
*/
|
*/
|
||||||
export const itemVisibilityChoices = [
|
export const ItemVisibilityChoices = [
|
||||||
{ key: 1, name: 'public', csvName: '' },
|
{ key: 1, name: 'public', csvName: '' },
|
||||||
{ key: 2, name: 'internal', csvName: 'internal' },
|
{ key: 2, name: 'internal', csvName: 'internal' },
|
||||||
{ key: 3, name: 'hidden', csvName: 'hidden' }
|
{ key: 3, name: 'hidden', csvName: 'hidden' }
|
||||||
|
@ -94,6 +94,7 @@ import { TileComponent } from './components/tile/tile.component';
|
|||||||
import { BlockTileComponent } from './components/block-tile/block-tile.component';
|
import { BlockTileComponent } from './components/block-tile/block-tile.component';
|
||||||
import { IconContainerComponent } from './components/icon-container/icon-container.component';
|
import { IconContainerComponent } from './components/icon-container/icon-container.component';
|
||||||
import { ListViewTableComponent } from './components/list-view-table/list-view-table.component';
|
import { ListViewTableComponent } from './components/list-view-table/list-view-table.component';
|
||||||
|
import { AgendaContentObjectFormComponent } from './components/agenda-content-object-form/agenda-content-object-form.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Share Module for all "dumb" components and pipes.
|
* Share Module for all "dumb" components and pipes.
|
||||||
@ -230,7 +231,8 @@ import { ListViewTableComponent } from './components/list-view-table/list-view-t
|
|||||||
SpeakerButtonComponent,
|
SpeakerButtonComponent,
|
||||||
PblNgridModule,
|
PblNgridModule,
|
||||||
PblNgridMaterialModule,
|
PblNgridMaterialModule,
|
||||||
ListViewTableComponent
|
ListViewTableComponent,
|
||||||
|
AgendaContentObjectFormComponent
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
PermsDirective,
|
PermsDirective,
|
||||||
@ -264,7 +266,8 @@ import { ListViewTableComponent } from './components/list-view-table/list-view-t
|
|||||||
TileComponent,
|
TileComponent,
|
||||||
BlockTileComponent,
|
BlockTileComponent,
|
||||||
IconContainerComponent,
|
IconContainerComponent,
|
||||||
ListViewTableComponent
|
ListViewTableComponent,
|
||||||
|
AgendaContentObjectFormComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },
|
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },
|
||||||
|
@ -6,7 +6,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
import { BaseImportService, NewEntry } from 'app/core/ui-services/base-import.service';
|
import { BaseImportService, NewEntry } from 'app/core/ui-services/base-import.service';
|
||||||
import { CreateTopic } from './models/create-topic';
|
import { CreateTopic } from './models/create-topic';
|
||||||
import { DurationService } from 'app/core/ui-services/duration.service';
|
import { DurationService } from 'app/core/ui-services/duration.service';
|
||||||
import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
|
import { ItemVisibilityChoices } from 'app/shared/models/agenda/item';
|
||||||
import { TopicRepositoryService } from '../../core/repositories/topics/topic-repository.service';
|
import { TopicRepositoryService } from '../../core/repositories/topics/topic-repository.service';
|
||||||
import { ViewCreateTopic } from './models/view-create-topic';
|
import { ViewCreateTopic } from './models/view-create-topic';
|
||||||
|
|
||||||
@ -162,13 +162,13 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
|
|||||||
if (!input) {
|
if (!input) {
|
||||||
return 1; // default, public item
|
return 1; // default, public item
|
||||||
} else if (typeof input === 'string') {
|
} else if (typeof input === 'string') {
|
||||||
const visibility = itemVisibilityChoices.find(choice => choice.csvName === input);
|
const visibility = ItemVisibilityChoices.find(choice => choice.csvName === input);
|
||||||
if (visibility) {
|
if (visibility) {
|
||||||
return visibility.key;
|
return visibility.key;
|
||||||
}
|
}
|
||||||
} else if (input === 1) {
|
} else if (input === 1) {
|
||||||
// Compatibility with the old client's isInternal column
|
// Compatibility with the old client's isInternal column
|
||||||
const visibility = itemVisibilityChoices.find(choice => choice.name === 'Internal item');
|
const visibility = ItemVisibilityChoices.find(choice => choice.name === 'Internal item');
|
||||||
if (visibility) {
|
if (visibility) {
|
||||||
return visibility.key;
|
return visibility.key;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import { AgendaImportService } from '../../agenda-import.service';
|
|||||||
import { BaseImportListComponent } from 'app/site/base/base-import-list';
|
import { BaseImportListComponent } from 'app/site/base/base-import-list';
|
||||||
import { CsvExportService } from 'app/core/ui-services/csv-export.service';
|
import { CsvExportService } from 'app/core/ui-services/csv-export.service';
|
||||||
import { DurationService } from 'app/core/ui-services/duration.service';
|
import { DurationService } from 'app/core/ui-services/duration.service';
|
||||||
import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
|
import { ItemVisibilityChoices } from 'app/shared/models/agenda/item';
|
||||||
import { ViewCreateTopic } from '../../models/view-create-topic';
|
import { ViewCreateTopic } from '../../models/view-create-topic';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,7 +72,7 @@ export class AgendaImportListComponent extends BaseImportListComponent<ViewCreat
|
|||||||
* @returns A string, which may be empty if the type is not found in the visibilityChoices
|
* @returns A string, which may be empty if the type is not found in the visibilityChoices
|
||||||
*/
|
*/
|
||||||
public getTypeString(type: number): string {
|
public getTypeString(type: number): string {
|
||||||
const visibility = itemVisibilityChoices.find(choice => choice.key === type);
|
const visibility = ItemVisibilityChoices.find(choice => choice.key === type);
|
||||||
return visibility ? visibility.name : '';
|
return visibility ? visibility.name : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,9 +175,9 @@
|
|||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
<!-- Delete selected -->
|
<!-- Delete selected -->
|
||||||
<button mat-menu-item [disabled]="!selectedRows.length" class="red-warning-text" (click)="deleteSelected()">
|
<button mat-menu-item [disabled]="!selectedRows.length" (click)="removeSelected()">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>remove</mat-icon>
|
||||||
<span translate>Delete</span>
|
<span translate>Remove from agenda</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -198,7 +198,12 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Delete Button -->
|
<!-- Delete Button -->
|
||||||
<button mat-menu-item class="red-warning-text" (click)="onDelete(item)">
|
<button mat-menu-item (click)="removeFromAgenda(item)" *ngIf="item.contentObjectData.collection !== 'topics/topic'">
|
||||||
|
<mat-icon>remove</mat-icon>
|
||||||
|
<span translate>Remove from agenda</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button mat-menu-item class="red-warning-text" (click)="deleteTopic(item)" *ngIf="item.contentObjectData.collection === 'topics/topic'">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
<span translate>Delete</span>
|
<span translate>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -24,6 +24,8 @@ import { ViewportService } from 'app/core/ui-services/viewport.service';
|
|||||||
import { ViewItem } from '../../models/view-item';
|
import { ViewItem } from '../../models/view-item';
|
||||||
import { ViewListOfSpeakers } from '../../models/view-list-of-speakers';
|
import { ViewListOfSpeakers } from '../../models/view-list-of-speakers';
|
||||||
import { _ } from 'app/core/translate/translation-marker';
|
import { _ } from 'app/core/translate/translation-marker';
|
||||||
|
import { TopicRepositoryService } from 'app/core/repositories/topics/topic-repository.service';
|
||||||
|
import { ViewTopic } from '../../models/view-topic';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List view for the agenda.
|
* List view for the agenda.
|
||||||
@ -123,7 +125,8 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
|||||||
public filterService: AgendaFilterListService,
|
public filterService: AgendaFilterListService,
|
||||||
private agendaPdfService: AgendaPdfService,
|
private agendaPdfService: AgendaPdfService,
|
||||||
private pdfService: PdfDocumentService,
|
private pdfService: PdfDocumentService,
|
||||||
private listOfSpeakersRepo: ListOfSpeakersRepositoryService
|
private listOfSpeakersRepo: ListOfSpeakersRepositoryService,
|
||||||
|
private topicRepo: TopicRepositoryService
|
||||||
) {
|
) {
|
||||||
super(titleService, translate, matSnackBar, storage);
|
super(titleService, translate, matSnackBar, storage);
|
||||||
this.canMultiSelect = true;
|
this.canMultiSelect = true;
|
||||||
@ -194,7 +197,7 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
|||||||
*/
|
*/
|
||||||
public async onAutoNumbering(): Promise<void> {
|
public async onAutoNumbering(): Promise<void> {
|
||||||
const title = this.translate.instant('Are you sure you want to number all agenda items?');
|
const title = this.translate.instant('Are you sure you want to number all agenda items?');
|
||||||
if (await this.promptService.open(title, null)) {
|
if (await this.promptService.open(title)) {
|
||||||
await this.repo.autoNumbering().then(null, this.raiseError);
|
await this.repo.autoNumbering().then(null, this.raiseError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -215,15 +218,26 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete handler for a single item
|
* Remove handler for a single item
|
||||||
*
|
*
|
||||||
* @param item The item to delete
|
* @param item The item to remove from the agenda
|
||||||
*/
|
*/
|
||||||
public async onDelete(item: ViewItem): Promise<void> {
|
public async removeFromAgenda(item: ViewItem): Promise<void> {
|
||||||
const title = this.translate.instant('Are you sure you want to delete this entry?');
|
const title = this.translate.instant('Are you sure you want to remove this entry from the agenda?');
|
||||||
const content = item.contentObject.getTitle();
|
const content = item.contentObject.getTitle();
|
||||||
if (await this.promptService.open(title, content)) {
|
if (await this.promptService.open(title, content)) {
|
||||||
await this.repo.delete(item).then(null, this.raiseError);
|
await this.repo.removeFromAgenda(item).then(null, this.raiseError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteTopic(item: ViewItem): Promise<void> {
|
||||||
|
if (!(item.contentObject instanceof ViewTopic)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const title = this.translate.instant('Are you sure you want to delete this topic?');
|
||||||
|
const content = item.contentObject.getTitle();
|
||||||
|
if (await this.promptService.open(title, content)) {
|
||||||
|
await this.topicRepo.delete(item.contentObject).then(null, this.raiseError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,11 +245,16 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
|||||||
* Handler for deleting multiple entries. Needs items in selectedRows, which
|
* Handler for deleting multiple entries. Needs items in selectedRows, which
|
||||||
* is only filled with any data in multiSelect mode
|
* is only filled with any data in multiSelect mode
|
||||||
*/
|
*/
|
||||||
public async deleteSelected(): Promise<void> {
|
public async removeSelected(): Promise<void> {
|
||||||
const title = this.translate.instant('Are you sure you want to delete all selected items?');
|
const title = this.translate.instant('Are you sure you want to remove all selected items from the agenda?');
|
||||||
if (await this.promptService.open(title, null)) {
|
const content = this.translate.instant("All topics will be deleted and won't be accessible afterwards.");
|
||||||
for (const agenda of this.selectedRows) {
|
if (await this.promptService.open(title, content)) {
|
||||||
await this.repo.delete(agenda);
|
for (const item of this.selectedRows) {
|
||||||
|
if (item.contentObject instanceof ViewTopic) {
|
||||||
|
await this.topicRepo.delete(item.contentObject);
|
||||||
|
} else {
|
||||||
|
await this.repo.removeFromAgenda(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -247,8 +266,8 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
|||||||
* @param closed true if the item is to be considered done
|
* @param closed true if the item is to be considered done
|
||||||
*/
|
*/
|
||||||
public async setClosedSelected(closed: boolean): Promise<void> {
|
public async setClosedSelected(closed: boolean): Promise<void> {
|
||||||
for (const agenda of this.selectedRows) {
|
for (const item of this.selectedRows) {
|
||||||
await this.repo.update({ closed: closed }, agenda);
|
await this.repo.update({ closed: closed }, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,8 +278,8 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
|||||||
* @param visible true if the item is to be shown
|
* @param visible true if the item is to be shown
|
||||||
*/
|
*/
|
||||||
public async setAgendaType(agendaType: number): Promise<void> {
|
public async setAgendaType(agendaType: number): Promise<void> {
|
||||||
for (const agenda of this.selectedRows) {
|
for (const item of this.selectedRows) {
|
||||||
await this.repo.update({ type: agendaType }, agenda).then(null, this.raiseError);
|
await this.repo.update({ type: agendaType }, item).then(null, this.raiseError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,11 +5,11 @@ import { MatSnackBar } from '@angular/material';
|
|||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
|
|
||||||
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
|
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
|
||||||
import { SortTreeViewComponent, SortTreeFilterOption } from 'app/site/base/sort-tree.component';
|
import { SortTreeViewComponent, SortTreeFilterOption } from 'app/site/base/sort-tree.component';
|
||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { ViewItem } from '../../models/view-item';
|
import { ViewItem } from '../../models/view-item';
|
||||||
|
import { ItemVisibilityChoices } from 'app/shared/models/agenda/item';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort view for the agenda.
|
* Sort view for the agenda.
|
||||||
@ -30,7 +30,7 @@ export class AgendaSortComponent extends SortTreeViewComponent<ViewItem> impleme
|
|||||||
* Adds the property `state` to identify if the option is marked as active.
|
* Adds the property `state` to identify if the option is marked as active.
|
||||||
* When reset the filters, the option `state` will be set to `false`.
|
* When reset the filters, the option `state` will be set to `false`.
|
||||||
*/
|
*/
|
||||||
public filterOptions: SortTreeFilterOption[] = itemVisibilityChoices.map(item => {
|
public filterOptions: SortTreeFilterOption[] = ItemVisibilityChoices.map(item => {
|
||||||
return { label: item.name, id: item.key, state: false };
|
return { label: item.name, id: item.key, state: false };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { FormBuilder, FormGroup } from '@angular/forms';
|
|||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||||
|
|
||||||
import { ViewItem } from '../../models/view-item';
|
import { ViewItem } from '../../models/view-item';
|
||||||
import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
|
import { ItemVisibilityChoices } from 'app/shared/models/agenda/item';
|
||||||
import { DurationService } from 'app/core/ui-services/duration.service';
|
import { DurationService } from 'app/core/ui-services/duration.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,7 +23,7 @@ export class ItemInfoDialogComponent {
|
|||||||
/**
|
/**
|
||||||
* Hold item visibility
|
* Hold item visibility
|
||||||
*/
|
*/
|
||||||
public itemVisibility = itemVisibilityChoices;
|
public itemVisibility = ItemVisibilityChoices;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
|
@ -403,7 +403,7 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
|||||||
const title = this.translate.instant(
|
const title = this.translate.instant(
|
||||||
'Are you sure you want to delete all speakers from this list of speakers?'
|
'Are you sure you want to delete all speakers from this list of speakers?'
|
||||||
);
|
);
|
||||||
if (await this.promptService.open(title, null)) {
|
if (await this.promptService.open(title)) {
|
||||||
this.listOfSpeakersRepo.deleteAllSpeakers(this.viewListOfSpeakers);
|
this.listOfSpeakersRepo.deleteAllSpeakers(this.viewListOfSpeakers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import { TopicRepositoryService } from 'app/core/repositories/topics/topic-repos
|
|||||||
import { ViewTopic } from '../../models/view-topic';
|
import { ViewTopic } from '../../models/view-topic';
|
||||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
|
import { ItemVisibilityChoices } from 'app/shared/models/agenda/item';
|
||||||
import { CreateTopic } from '../../models/create-topic';
|
import { CreateTopic } from '../../models/create-topic';
|
||||||
import { Topic } from 'app/shared/models/topics/topic';
|
import { Topic } from 'app/shared/models/topics/topic';
|
||||||
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
||||||
@ -62,7 +62,7 @@ export class TopicDetailComponent extends BaseViewComponent {
|
|||||||
/**
|
/**
|
||||||
* Determine visibility states for the agenda that will be created implicitly
|
* Determine visibility states for the agenda that will be created implicitly
|
||||||
*/
|
*/
|
||||||
public itemVisibility = itemVisibilityChoices;
|
public itemVisibility = ItemVisibilityChoices;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for the topic detail page.
|
* Constructor for the topic detail page.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Item, itemVisibilityChoices } from 'app/shared/models/agenda/item';
|
import { Item, ItemVisibilityChoices } from 'app/shared/models/agenda/item';
|
||||||
import {
|
import {
|
||||||
BaseViewModelWithAgendaItem,
|
BaseViewModelWithAgendaItem,
|
||||||
isBaseViewModelWithAgendaItem
|
isBaseViewModelWithAgendaItem
|
||||||
@ -56,7 +56,7 @@ export class ViewItem extends BaseViewModelWithContentObject<Item, BaseViewModel
|
|||||||
if (!this.type) {
|
if (!this.type) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const type = itemVisibilityChoices.find(choice => choice.key === this.type);
|
const type = ItemVisibilityChoices.find(choice => choice.key === this.type);
|
||||||
return type ? type.name : '';
|
return type ? type.name : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ export class ViewItem extends BaseViewModelWithContentObject<Item, BaseViewModel
|
|||||||
if (!this.type) {
|
if (!this.type) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const type = itemVisibilityChoices.find(choice => choice.key === this.type);
|
const type = ItemVisibilityChoices.find(choice => choice.key === this.type);
|
||||||
return type ? type.csvName : '';
|
return type ? type.csvName : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { BaseFilterListService, OsFilter, OsFilterOption } from 'app/core/ui-services/base-filter-list.service';
|
import { BaseFilterListService, OsFilter, OsFilterOption } from 'app/core/ui-services/base-filter-list.service';
|
||||||
import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
|
import { ItemVisibilityChoices } from 'app/shared/models/agenda/item';
|
||||||
import { ViewItem } from '../models/view-item';
|
import { ViewItem } from '../models/view-item';
|
||||||
import { StorageService } from 'app/core/core-services/storage.service';
|
import { StorageService } from 'app/core/core-services/storage.service';
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ export class AgendaFilterListService extends BaseFilterListService<ViewItem> {
|
|||||||
* @returns a list of choices to filter from
|
* @returns a list of choices to filter from
|
||||||
*/
|
*/
|
||||||
private createVisibilityFilterOptions(): OsFilterOption[] {
|
private createVisibilityFilterOptions(): OsFilterOption[] {
|
||||||
return itemVisibilityChoices.map(choice => ({
|
return ItemVisibilityChoices.map(choice => ({
|
||||||
condition: choice.key as number,
|
condition: choice.key as number,
|
||||||
label: choice.name
|
label: choice.name
|
||||||
}));
|
}));
|
||||||
|
@ -33,6 +33,18 @@
|
|||||||
<!-- Project -->
|
<!-- Project -->
|
||||||
<os-projector-button [object]="assignment" [menuItem]="true"></os-projector-button>
|
<os-projector-button [object]="assignment" [menuItem]="true"></os-projector-button>
|
||||||
|
|
||||||
|
<!-- Add/remove to/from agenda -->
|
||||||
|
<div *osPerms="'agenda.can_manage'">
|
||||||
|
<button mat-menu-item (click)="addToAgenda()" *ngIf="assignment && !assignment.agendaItem">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
<span translate>Add to agenda</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item (click)="removeFromAgenda()" *ngIf="assignment && assignment.agendaItem">
|
||||||
|
<mat-icon>remove</mat-icon>
|
||||||
|
<span translate>Remove from agenda</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Delete -->
|
<!-- Delete -->
|
||||||
<div *ngIf="assignment && hasPerms('manage')">
|
<div *ngIf="assignment && hasPerms('manage')">
|
||||||
<!-- Delete -->
|
<!-- Delete -->
|
||||||
@ -268,18 +280,7 @@
|
|||||||
></os-search-value-selector>
|
></os-search-value-selector>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- searchValueSelector: agendaItem -->
|
<os-agenda-content-object-form *ngIf="newAssignment" [form]="assignmentForm"></os-agenda-content-object-form>
|
||||||
<div class="content-field" *ngIf="parentsAvailable">
|
|
||||||
<os-search-value-selector
|
|
||||||
ngDefaultControl
|
|
||||||
[form]="assignmentForm"
|
|
||||||
[formControl]="assignmentForm.get('agenda_item_id')"
|
|
||||||
[multiple]="false"
|
|
||||||
[includeNone]="false"
|
|
||||||
listname="{{ 'Parent agenda item' | translate }}"
|
|
||||||
[InputListValues]="agendaObserver"
|
|
||||||
></os-search-value-selector>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- poll_description_default -->
|
<!-- poll_description_default -->
|
||||||
<div>
|
<div>
|
||||||
|
@ -161,7 +161,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
* @param repo
|
* @param repo
|
||||||
* @param userRepo
|
* @param userRepo
|
||||||
* @param pollService
|
* @param pollService
|
||||||
* @param agendaRepo
|
* @param itemRepo
|
||||||
* @param tagRepo
|
* @param tagRepo
|
||||||
* @param promptService
|
* @param promptService
|
||||||
*/
|
*/
|
||||||
@ -177,7 +177,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
public repo: AssignmentRepositoryService,
|
public repo: AssignmentRepositoryService,
|
||||||
private userRepo: UserRepositoryService,
|
private userRepo: UserRepositoryService,
|
||||||
public pollService: AssignmentPollService,
|
public pollService: AssignmentPollService,
|
||||||
private agendaRepo: ItemRepositoryService,
|
private itemRepo: ItemRepositoryService,
|
||||||
private tagRepo: TagRepositoryService,
|
private tagRepo: TagRepositoryService,
|
||||||
private promptService: PromptService,
|
private promptService: PromptService,
|
||||||
private pdfService: AssignmentPdfExportService,
|
private pdfService: AssignmentPdfExportService,
|
||||||
@ -200,7 +200,9 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
description: '',
|
description: '',
|
||||||
poll_description_default: '',
|
poll_description_default: '',
|
||||||
open_posts: 0,
|
open_posts: 0,
|
||||||
agenda_item_id: '' // create agenda item
|
agenda_create: [''],
|
||||||
|
agenda_parent_id: [],
|
||||||
|
agenda_type: ['']
|
||||||
});
|
});
|
||||||
this.candidatesForm = formBuilder.group({
|
this.candidatesForm = formBuilder.group({
|
||||||
userId: null
|
userId: null
|
||||||
@ -212,7 +214,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
*/
|
*/
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
this.getAssignmentByUrl();
|
this.getAssignmentByUrl();
|
||||||
this.agendaObserver = this.agendaRepo.getViewModelListBehaviorSubject();
|
this.agendaObserver = this.itemRepo.getViewModelListBehaviorSubject();
|
||||||
this.tagsObserver = this.tagRepo.getViewModelListBehaviorSubject();
|
this.tagsObserver = this.tagRepo.getViewModelListBehaviorSubject();
|
||||||
this.mediafilesObserver = this.mediafileRepo.getViewModelListBehaviorSubject();
|
this.mediafilesObserver = this.mediafileRepo.getViewModelListBehaviorSubject();
|
||||||
}
|
}
|
||||||
@ -292,7 +294,6 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
title: assignment.title || '',
|
title: assignment.title || '',
|
||||||
tags_id: assignment.assignment.tags_id || [],
|
tags_id: assignment.assignment.tags_id || [],
|
||||||
attachments_id: assignment.assignment.attachments_id || [],
|
attachments_id: assignment.assignment.attachments_id || [],
|
||||||
agendaItem: assignment.assignment.agenda_item_id || null,
|
|
||||||
phase: assignment.phase, // todo default: 0?
|
phase: assignment.phase, // todo default: 0?
|
||||||
description: assignment.assignment.description || '',
|
description: assignment.assignment.description || '',
|
||||||
poll_description_default: assignment.assignment.poll_description_default,
|
poll_description_default: assignment.assignment.poll_description_default,
|
||||||
@ -516,4 +517,12 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
public getSanitizedText(text: string): SafeHtml {
|
public getSanitizedText(text: string): SafeHtml {
|
||||||
return this.sanitizer.bypassSecurityTrustHtml(text);
|
return this.sanitizer.bypassSecurityTrustHtml(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addToAgenda(): void {
|
||||||
|
this.itemRepo.addItemToAgenda(this.assignment).then(null, this.raiseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeFromAgenda(): void {
|
||||||
|
this.itemRepo.removeFromAgenda(this.assignment.agendaItem).then(null, this.raiseError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,7 @@ export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignmen
|
|||||||
*/
|
*/
|
||||||
public async deleteSelected(): Promise<void> {
|
public async deleteSelected(): Promise<void> {
|
||||||
const title = this.translate.instant('Are you sure you want to delete all selected elections?');
|
const title = this.translate.instant('Are you sure you want to delete all selected elections?');
|
||||||
if (await this.promptService.open(title, '')) {
|
if (await this.promptService.open(title)) {
|
||||||
for (const assignment of this.selectedRows) {
|
for (const assignment of this.selectedRows) {
|
||||||
await this.repo.delete(assignment);
|
await this.repo.delete(assignment);
|
||||||
}
|
}
|
||||||
|
@ -157,7 +157,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
*/
|
*/
|
||||||
public async onDeletePoll(): Promise<void> {
|
public async onDeletePoll(): Promise<void> {
|
||||||
const title = this.translate.instant('Are you sure you want to delete this ballot?');
|
const title = this.translate.instant('Are you sure you want to delete this ballot?');
|
||||||
if (await this.promptService.open(title, null)) {
|
if (await this.promptService.open(title)) {
|
||||||
await this.assignmentRepo.deletePoll(this.poll).then(null, this.raiseError);
|
await this.assignmentRepo.deletePoll(this.poll).then(null, this.raiseError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,10 +99,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<os-speaker-button [object]="file" [menuItem]="true"></os-speaker-button>
|
|
||||||
|
|
||||||
<!-- Edit and delete for all images -->
|
<!-- Edit and delete for all images -->
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
|
<os-speaker-button [object]="file" [menuItem]="true"></os-speaker-button>
|
||||||
<button mat-menu-item (click)="onEditFile(file)">
|
<button mat-menu-item (click)="onEditFile(file)">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
<span translate>Edit</span>
|
<span translate>Edit</span>
|
||||||
|
@ -218,7 +218,7 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile>
|
|||||||
*/
|
*/
|
||||||
public async deleteSelected(): Promise<void> {
|
public async deleteSelected(): Promise<void> {
|
||||||
const title = this.translate.instant('Are you sure you want to delete all selected files?');
|
const title = this.translate.instant('Are you sure you want to delete all selected files?');
|
||||||
if (await this.promptService.open(title, null)) {
|
if (await this.promptService.open(title)) {
|
||||||
for (const mediafile of this.selectedRows) {
|
for (const mediafile of this.selectedRows) {
|
||||||
await this.repo.delete(mediafile);
|
await this.repo.delete(mediafile);
|
||||||
}
|
}
|
||||||
|
@ -185,7 +185,7 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
|
|||||||
return this.motion.workflow_id;
|
return this.motion.workflow_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get state(): WorkflowState {
|
public get state(): WorkflowState | null {
|
||||||
return this._state;
|
return this._state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,7 +349,7 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
|
|||||||
* @returns a string representing a color
|
* @returns a string representing a color
|
||||||
*/
|
*/
|
||||||
public get stateCssColor(): string {
|
public get stateCssColor(): string {
|
||||||
return StateCssClassMapping[this.state.css_class] || '';
|
return this.state ? StateCssClassMapping[this.state.css_class] : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is set by the repository
|
// This is set by the repository
|
||||||
|
@ -157,7 +157,7 @@ export class CategoryMotionsSortComponent extends BaseViewComponent implements O
|
|||||||
*/
|
*/
|
||||||
public async sendUpdate(): Promise<void> {
|
public async sendUpdate(): Promise<void> {
|
||||||
const title = this.translate.instant('Do you really want to save your changes?');
|
const title = this.translate.instant('Do you really want to save your changes?');
|
||||||
if (await this.promptService.open(title, null)) {
|
if (await this.promptService.open(title)) {
|
||||||
const ids = this.motionsCopy.map(motion => motion.id);
|
const ids = this.motionsCopy.map(motion => motion.id);
|
||||||
this.repo.sortMotionsInCategory(this.category.category, ids);
|
this.repo.sortMotionsInCategory(this.category.category, ids);
|
||||||
this.hasChanged = false;
|
this.hasChanged = false;
|
||||||
|
@ -81,6 +81,17 @@
|
|||||||
|
|
||||||
<os-projector-button *ngIf="block" [object]="block" [menuItem]="true"></os-projector-button>
|
<os-projector-button *ngIf="block" [object]="block" [menuItem]="true"></os-projector-button>
|
||||||
|
|
||||||
|
<div *osPerms="'agenda.can_manage'">
|
||||||
|
<button mat-menu-item (click)="addToAgenda()" *ngIf="block && !block.agendaItem">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
<span translate>Add to agenda</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item (click)="removeFromAgenda()" *ngIf="block && block.agendaItem">
|
||||||
|
<mat-icon>remove</mat-icon>
|
||||||
|
<span translate>Remove from agenda</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
|
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
|
||||||
<button mat-menu-item (click)="toggleEditMode()">
|
<button mat-menu-item (click)="toggleEditMode()">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
|
@ -14,6 +14,7 @@ import { PromptService } from 'app/core/ui-services/prompt.service';
|
|||||||
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
||||||
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
|
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
|
||||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
|
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detail component to display one motion block
|
* Detail component to display one motion block
|
||||||
@ -105,7 +106,8 @@ export class MotionBlockDetailComponent extends BaseViewComponent implements OnI
|
|||||||
protected motionRepo: MotionRepositoryService,
|
protected motionRepo: MotionRepositoryService,
|
||||||
private promptService: PromptService,
|
private promptService: PromptService,
|
||||||
private fb: FormBuilder,
|
private fb: FormBuilder,
|
||||||
private dialog: MatDialog
|
private dialog: MatDialog,
|
||||||
|
private itemRepo: ItemRepositoryService
|
||||||
) {
|
) {
|
||||||
super(titleService, translate, matSnackBar);
|
super(titleService, translate, matSnackBar);
|
||||||
}
|
}
|
||||||
@ -249,4 +251,12 @@ export class MotionBlockDetailComponent extends BaseViewComponent implements OnI
|
|||||||
public getStateLabel(motion: ViewMotion): string {
|
public getStateLabel(motion: ViewMotion): string {
|
||||||
return this.motionRepo.getExtendedStateLabel(motion);
|
return this.motionRepo.getExtendedStateLabel(motion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addToAgenda(): void {
|
||||||
|
this.itemRepo.addItemToAgenda(this.block).then(null, this.raiseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeFromAgenda(): void {
|
||||||
|
this.itemRepo.removeFromAgenda(this.block.agendaItem).then(null, this.raiseError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,27 +23,7 @@
|
|||||||
<mat-checkbox formControlName="internal"><span translate>Internal</span></mat-checkbox>
|
<mat-checkbox formControlName="internal"><span translate>Internal</span></mat-checkbox>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Parent item -->
|
<os-agenda-content-object-form [form]="createBlockForm"></os-agenda-content-object-form>
|
||||||
<p>
|
|
||||||
<os-search-value-selector
|
|
||||||
ngDefaultControl
|
|
||||||
listname="{{ 'Parent agenda item' | translate }}"
|
|
||||||
[form]="createBlockForm"
|
|
||||||
[formControl]="createBlockForm.get('agenda_parent_id')"
|
|
||||||
[multiple]="false"
|
|
||||||
[includeNone]="true"
|
|
||||||
[InputListValues]="items"
|
|
||||||
></os-search-value-selector>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Visibility -->
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-select formControlName="agenda_type" placeholder="{{ 'Agenda visibility' | translate }}">
|
|
||||||
<mat-option *ngFor="let type of itemVisibility" [value]="type.key">
|
|
||||||
<span>{{ type.name | translate }}</span>
|
|
||||||
</mat-option>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
</form>
|
</form>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
import { PblColumnDefinition } from '@pebula/ngrid';
|
import { PblColumnDefinition } from '@pebula/ngrid';
|
||||||
|
|
||||||
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
|
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
|
||||||
import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
|
|
||||||
import { ListViewBaseComponent } from 'app/site/base/list-view-base';
|
import { ListViewBaseComponent } from 'app/site/base/list-view-base';
|
||||||
import { MotionBlock } from 'app/shared/models/motions/motion-block';
|
import { MotionBlock } from 'app/shared/models/motions/motion-block';
|
||||||
import { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.service';
|
import { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.service';
|
||||||
@ -47,11 +46,6 @@ export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBl
|
|||||||
*/
|
*/
|
||||||
public defaultVisibility: number;
|
public defaultVisibility: number;
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine visibility states for the agenda that will be created implicitly
|
|
||||||
*/
|
|
||||||
public itemVisibility = itemVisibilityChoices;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* helper for permission checks
|
* helper for permission checks
|
||||||
*
|
*
|
||||||
@ -82,23 +76,21 @@ export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBl
|
|||||||
* @param titleService sets the title
|
* @param titleService sets the title
|
||||||
* @param translate translpations
|
* @param translate translpations
|
||||||
* @param matSnackBar display errors in the snack bar
|
* @param matSnackBar display errors in the snack bar
|
||||||
* @param router routing to children
|
|
||||||
* @param route determine the local route
|
* @param route determine the local route
|
||||||
|
* @param storage
|
||||||
* @param repo the motion block repository
|
* @param repo the motion block repository
|
||||||
* @param agendaRepo the agenda repository service
|
|
||||||
* @param DS the dataStore
|
|
||||||
* @param formBuilder creates forms
|
* @param formBuilder creates forms
|
||||||
* @param promptService the delete prompt
|
* @param promptService the delete prompt
|
||||||
* @param itemRepo
|
* @param itemRepo
|
||||||
* @param operator permission checks
|
* @param operator permission checks
|
||||||
|
* @param sortService
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
titleService: Title,
|
titleService: Title,
|
||||||
translate: TranslateService,
|
translate: TranslateService,
|
||||||
matSnackBar: MatSnackBar,
|
matSnackBar: MatSnackBar,
|
||||||
storage: StorageService,
|
storage: StorageService,
|
||||||
public repo: MotionBlockRepositoryService,
|
private repo: MotionBlockRepositoryService,
|
||||||
private agendaRepo: ItemRepositoryService,
|
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private itemRepo: ItemRepositoryService,
|
private itemRepo: ItemRepositoryService,
|
||||||
private operator: OperatorService,
|
private operator: OperatorService,
|
||||||
@ -108,8 +100,9 @@ export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBl
|
|||||||
|
|
||||||
this.createBlockForm = this.formBuilder.group({
|
this.createBlockForm = this.formBuilder.group({
|
||||||
title: ['', Validators.required],
|
title: ['', Validators.required],
|
||||||
agenda_type: ['', Validators.required],
|
agenda_create: [''],
|
||||||
agenda_parent_id: [],
|
agenda_parent_id: [],
|
||||||
|
agenda_type: [''],
|
||||||
internal: [false]
|
internal: [false]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -120,7 +113,6 @@ export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBl
|
|||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
super.setTitle('Motion blocks');
|
super.setTitle('Motion blocks');
|
||||||
this.items = this.itemRepo.getViewModelListBehaviorSubject();
|
this.items = this.itemRepo.getViewModelListBehaviorSubject();
|
||||||
this.agendaRepo.getDefaultAgendaVisibility().subscribe(visibility => (this.defaultVisibility = visibility));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -155,7 +147,7 @@ export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBl
|
|||||||
* Click handler for the save button.
|
* Click handler for the save button.
|
||||||
* Sends the block to create to the repository and resets the form.
|
* Sends the block to create to the repository and resets the form.
|
||||||
*/
|
*/
|
||||||
public onSaveNewButton(): void {
|
public async onSaveNewButton(): Promise<void> {
|
||||||
if (this.createBlockForm.valid) {
|
if (this.createBlockForm.valid) {
|
||||||
const block = this.createBlockForm.value;
|
const block = this.createBlockForm.value;
|
||||||
if (!block.agenda_parent_id) {
|
if (!block.agenda_parent_id) {
|
||||||
@ -163,7 +155,7 @@ export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBl
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.repo.create(block);
|
await this.repo.create(block);
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
this.isCreatingNewBlock = false;
|
this.isCreatingNewBlock = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -269,7 +269,7 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
|
|||||||
$event.stopPropagation();
|
$event.stopPropagation();
|
||||||
$event.preventDefault();
|
$event.preventDefault();
|
||||||
const title = this.translate.instant('Are you sure you want to delete this change recommendation?');
|
const title = this.translate.instant('Are you sure you want to delete this change recommendation?');
|
||||||
if (await this.promptService.open(title, null)) {
|
if (await this.promptService.open(title)) {
|
||||||
this.recoRepo.delete(reco).then(null, this.raiseError);
|
this.recoRepo.delete(reco).then(null, this.raiseError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,17 @@
|
|||||||
[menuItem]="true"
|
[menuItem]="true"
|
||||||
*osPerms="'core.can_manage_projector'"
|
*osPerms="'core.can_manage_projector'"
|
||||||
></os-projector-button>
|
></os-projector-button>
|
||||||
|
<!-- Add/remove to/from agenda -->
|
||||||
|
<div *osPerms="'agenda.can_manage'">
|
||||||
|
<button mat-menu-item (click)="addToAgenda()" *ngIf="motion && !motion.agendaItem">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
<span translate>Add to agenda</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item (click)="removeFromAgenda()" *ngIf="motion && motion.agendaItem">
|
||||||
|
<mat-icon>remove</mat-icon>
|
||||||
|
<span translate>Remove from agenda</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<!-- New amendment -->
|
<!-- New amendment -->
|
||||||
<button mat-menu-item (click)="createAmendment()" *ngIf="perms.isAllowed('can_create_amendments', motion)">
|
<button mat-menu-item (click)="createAmendment()" *ngIf="perms.isAllowed('can_create_amendments', motion)">
|
||||||
<mat-icon>add</mat-icon>
|
<mat-icon>add</mat-icon>
|
||||||
@ -89,6 +100,7 @@
|
|||||||
<mat-icon>{{ !showAmendmentContext ? 'check_box_outline_blank' : 'check_box' }}</mat-icon>
|
<mat-icon>{{ !showAmendmentContext ? 'check_box_outline_blank' : 'check_box' }}</mat-icon>
|
||||||
<span translate>Show entire motion text</span>
|
<span translate>Show entire motion text</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div *ngIf="perms.isAllowed('manage')">
|
<div *ngIf="perms.isAllowed('manage')">
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<!-- Delete -->
|
<!-- Delete -->
|
||||||
@ -795,28 +807,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Visibility -->
|
<div *ngIf="newMotion">
|
||||||
<div class="content-field" *ngIf="newMotion">
|
<os-agenda-content-object-form [form]="contentForm"></os-agenda-content-object-form>
|
||||||
<mat-form-field>
|
|
||||||
<mat-select formControlName="agenda_type" placeholder="{{ 'Agenda visibility' | translate }}">
|
|
||||||
<mat-option *ngFor="let type of itemVisibility" [value]="type.key">
|
|
||||||
<span>{{ type.name | translate }}</span>
|
|
||||||
</mat-option>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Parent item -->
|
|
||||||
<div class="content-field" *ngIf="newMotion && agendaItemObserver.value.length > 0">
|
|
||||||
<os-search-value-selector
|
|
||||||
ngDefaultControl
|
|
||||||
[form]="contentForm"
|
|
||||||
[formControl]="contentForm.get('agenda_parent_id')"
|
|
||||||
[multiple]="false"
|
|
||||||
[includeNone]="true"
|
|
||||||
listname="{{ 'Parent agenda item' | translate }}"
|
|
||||||
[InputListValues]="agendaItemObserver"
|
|
||||||
></os-search-value-selector>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Supporter form -->
|
<!-- Supporter form -->
|
||||||
|
@ -13,8 +13,6 @@ import { ChangeRecommendationRepositoryService } from 'app/core/repositories/mot
|
|||||||
import { CreateMotion } from 'app/site/motions/models/create-motion';
|
import { CreateMotion } from 'app/site/motions/models/create-motion';
|
||||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
import { DiffLinesInParagraph, LineRange } from 'app/core/ui-services/diff.service';
|
import { DiffLinesInParagraph, LineRange } from 'app/core/ui-services/diff.service';
|
||||||
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
|
|
||||||
import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
|
|
||||||
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
|
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
|
||||||
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
|
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
|
||||||
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
|
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
|
||||||
@ -37,7 +35,6 @@ import { ViewWorkflow } from 'app/site/motions/models/view-workflow';
|
|||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { ViewCategory } from 'app/site/motions/models/view-category';
|
import { ViewCategory } from 'app/site/motions/models/view-category';
|
||||||
import { ViewCreateMotion } from 'app/site/motions/models/view-create-motion';
|
import { ViewCreateMotion } from 'app/site/motions/models/view-create-motion';
|
||||||
import { ViewItem } from 'app/site/agenda/models/view-item';
|
|
||||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||||
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
||||||
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation';
|
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation';
|
||||||
@ -58,6 +55,7 @@ import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.
|
|||||||
import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service';
|
import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service';
|
||||||
import { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.service';
|
import { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.service';
|
||||||
import { MotionSortListService } from 'app/site/motions/services/motion-sort-list.service';
|
import { MotionSortListService } from 'app/site/motions/services/motion-sort-list.service';
|
||||||
|
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for the motion detail view
|
* Component for the motion detail view
|
||||||
@ -254,11 +252,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
*/
|
*/
|
||||||
public mediafilesObserver: BehaviorSubject<ViewMediafile[]>;
|
public mediafilesObserver: BehaviorSubject<ViewMediafile[]>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Subject for agenda items
|
|
||||||
*/
|
|
||||||
public agendaItemObserver: BehaviorSubject<ViewItem[]>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subject for tags
|
* Subject for tags
|
||||||
*/
|
*/
|
||||||
@ -294,16 +287,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
*/
|
*/
|
||||||
public showAmendmentContext = false;
|
public showAmendmentContext = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the default agenda item visibility
|
|
||||||
*/
|
|
||||||
public defaultVisibility: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine visibility states for the agenda that will be created implicitly
|
|
||||||
*/
|
|
||||||
public itemVisibility = itemVisibilityChoices;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For using the enum constants from the template
|
* For using the enum constants from the template
|
||||||
*/
|
*/
|
||||||
@ -395,7 +378,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
* @param dialogService For opening dialogs
|
* @param dialogService For opening dialogs
|
||||||
* @param el The native element
|
* @param el The native element
|
||||||
* @param repo Motion Repository
|
* @param repo Motion Repository
|
||||||
* @param agendaRepo Read out agenda variables
|
|
||||||
* @param changeRecoRepo Change Recommendation Repository
|
* @param changeRecoRepo Change Recommendation Repository
|
||||||
* @param statuteRepo: Statute Paragraph Repository
|
* @param statuteRepo: Statute Paragraph Repository
|
||||||
* @param mediafileRepo Mediafile Repository
|
* @param mediafileRepo Mediafile Repository
|
||||||
@ -431,7 +413,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
private dialogService: MatDialog,
|
private dialogService: MatDialog,
|
||||||
private el: ElementRef,
|
private el: ElementRef,
|
||||||
public repo: MotionRepositoryService,
|
public repo: MotionRepositoryService,
|
||||||
private agendaRepo: ItemRepositoryService,
|
|
||||||
private changeRecoRepo: ChangeRecommendationRepositoryService,
|
private changeRecoRepo: ChangeRecommendationRepositoryService,
|
||||||
private statuteRepo: StatuteParagraphRepositoryService,
|
private statuteRepo: StatuteParagraphRepositoryService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
@ -447,8 +428,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
private mediaFilerepo: MediafileRepositoryService,
|
private mediaFilerepo: MediafileRepositoryService,
|
||||||
private workflowRepo: WorkflowRepositoryService,
|
private workflowRepo: WorkflowRepositoryService,
|
||||||
private blockRepo: MotionBlockRepositoryService,
|
private blockRepo: MotionBlockRepositoryService,
|
||||||
private itemRepo: ItemRepositoryService,
|
private motionSortService: MotionSortListService,
|
||||||
private motionSortService: MotionSortListService
|
private itemRepo: ItemRepositoryService
|
||||||
) {
|
) {
|
||||||
super(title, translate, matSnackBar);
|
super(title, translate, matSnackBar);
|
||||||
}
|
}
|
||||||
@ -463,7 +444,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
this.mediafilesObserver = this.mediaFilerepo.getViewModelListBehaviorSubject();
|
this.mediafilesObserver = this.mediaFilerepo.getViewModelListBehaviorSubject();
|
||||||
this.workflowObserver = this.workflowRepo.getViewModelListBehaviorSubject();
|
this.workflowObserver = this.workflowRepo.getViewModelListBehaviorSubject();
|
||||||
this.blockObserver = this.blockRepo.getViewModelListBehaviorSubject();
|
this.blockObserver = this.blockRepo.getViewModelListBehaviorSubject();
|
||||||
this.agendaItemObserver = this.itemRepo.getViewModelListBehaviorSubject();
|
|
||||||
this.motionObserver = this.repo.getViewModelListBehaviorSubject();
|
this.motionObserver = this.repo.getViewModelListBehaviorSubject();
|
||||||
this.submitterObserver = this.userRepo.getViewModelListBehaviorSubject();
|
this.submitterObserver = this.userRepo.getViewModelListBehaviorSubject();
|
||||||
this.supporterObserver = this.userRepo.getViewModelListBehaviorSubject();
|
this.supporterObserver = this.userRepo.getViewModelListBehaviorSubject();
|
||||||
@ -513,13 +493,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set the default visibility using observers
|
|
||||||
this.agendaRepo.getDefaultAgendaVisibility().subscribe(visibility => {
|
|
||||||
if (visibility && this.newMotion) {
|
|
||||||
this.contentForm.get('agenda_type').setValue(visibility);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update statute paragraphs
|
// Update statute paragraphs
|
||||||
this.statuteRepo.getViewModelListObservable().subscribe(newViewStatuteParagraphs => {
|
this.statuteRepo.getViewModelListObservable().subscribe(newViewStatuteParagraphs => {
|
||||||
this.statuteParagraphs = newViewStatuteParagraphs;
|
this.statuteParagraphs = newViewStatuteParagraphs;
|
||||||
@ -724,6 +697,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
reason: reason,
|
reason: reason,
|
||||||
category_id: [''],
|
category_id: [''],
|
||||||
attachments_id: [[]],
|
attachments_id: [[]],
|
||||||
|
agenda_create: [''],
|
||||||
agenda_parent_id: [],
|
agenda_parent_id: [],
|
||||||
agenda_type: [''],
|
agenda_type: [''],
|
||||||
submitters_id: [],
|
submitters_id: [],
|
||||||
@ -1092,7 +1066,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
const title = this.translate.instant(
|
const title = this.translate.instant(
|
||||||
'Are you sure you want to copy the final version to the print template?'
|
'Are you sure you want to copy the final version to the print template?'
|
||||||
);
|
);
|
||||||
if (await this.promptService.open(title, null)) {
|
if (await this.promptService.open(title)) {
|
||||||
this.updateMotion({ modified_final_version: finalVersion }, this.motion).then(
|
this.updateMotion({ modified_final_version: finalVersion }, this.motion).then(
|
||||||
() => this.setChangeRecoMode(ChangeRecoMode.ModifiedFinal),
|
() => this.setChangeRecoMode(ChangeRecoMode.ModifiedFinal),
|
||||||
this.raiseError
|
this.raiseError
|
||||||
@ -1111,7 +1085,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
*/
|
*/
|
||||||
public async deleteModifiedFinalVersion(): Promise<void> {
|
public async deleteModifiedFinalVersion(): Promise<void> {
|
||||||
const title = this.translate.instant('Are you sure you want to delete the print template?');
|
const title = this.translate.instant('Are you sure you want to delete the print template?');
|
||||||
if (await this.promptService.open(title, null)) {
|
if (await this.promptService.open(title)) {
|
||||||
this.finalEditMode = false;
|
this.finalEditMode = false;
|
||||||
this.updateMotion({ modified_final_version: '' }, this.motion).then(
|
this.updateMotion({ modified_final_version: '' }, this.motion).then(
|
||||||
() => this.setChangeRecoMode(ChangeRecoMode.Final),
|
() => this.setChangeRecoMode(ChangeRecoMode.Final),
|
||||||
@ -1610,4 +1584,12 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
public editModifiedFinal(): void {
|
public editModifiedFinal(): void {
|
||||||
this.finalEditMode = true;
|
this.finalEditMode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addToAgenda(): void {
|
||||||
|
this.itemRepo.addItemToAgenda(this.motion).then(null, this.raiseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeFromAgenda(): void {
|
||||||
|
this.itemRepo.removeFromAgenda(this.motion.agendaItem).then(null, this.raiseError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,7 @@ export class MotionPollComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
public async deletePoll(): Promise<void> {
|
public async deletePoll(): Promise<void> {
|
||||||
const title = this.translate.instant('Are you sure you want to delete this vote?');
|
const title = this.translate.instant('Are you sure you want to delete this vote?');
|
||||||
if (await this.promptService.open(title, null)) {
|
if (await this.promptService.open(title)) {
|
||||||
this.motionRepo.deletePoll(this.poll);
|
this.motionRepo.deletePoll(this.poll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ export class MotionMultiselectService {
|
|||||||
*/
|
*/
|
||||||
public async delete(motions: ViewMotion[]): Promise<void> {
|
public async delete(motions: ViewMotion[]): Promise<void> {
|
||||||
const title = this.translate.instant('Are you sure you want to delete all selected motions?');
|
const title = this.translate.instant('Are you sure you want to delete all selected motions?');
|
||||||
if (await this.promptService.open(title, null)) {
|
if (await this.promptService.open(title)) {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
for (const motion of motions) {
|
for (const motion of motions) {
|
||||||
|
@ -280,7 +280,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
|||||||
*/
|
*/
|
||||||
public async deleteSelected(): Promise<void> {
|
public async deleteSelected(): Promise<void> {
|
||||||
const title = this.translate.instant('Are you sure you want to delete all selected participants?');
|
const title = this.translate.instant('Are you sure you want to delete all selected participants?');
|
||||||
if (await this.promptService.open(title, null)) {
|
if (await this.promptService.open(title)) {
|
||||||
for (const user of this.selectedRows) {
|
for (const user of this.selectedRows) {
|
||||||
await this.repo.delete(user);
|
await this.repo.delete(user);
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,22 @@ def get_config_variables():
|
|||||||
validators=(MaxLengthValidator(20),),
|
validators=(MaxLengthValidator(20),),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
yield ConfigVariable(
|
||||||
|
name="agenda_item_creation",
|
||||||
|
label="Auto add to agenda",
|
||||||
|
default_value="always",
|
||||||
|
input_type="choice",
|
||||||
|
choices=(
|
||||||
|
{"value": "always", "display_name": "Always"},
|
||||||
|
{"value": "never", "display_name": "Never"},
|
||||||
|
{"value": "default_yes", "display_name": "Ask, default yes"},
|
||||||
|
{"value": "default_no", "display_name": "Ask, default no"},
|
||||||
|
),
|
||||||
|
weight=212,
|
||||||
|
group="Agenda",
|
||||||
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name="agenda_numeral_system",
|
name="agenda_numeral_system",
|
||||||
default_value="arabic",
|
default_value="arabic",
|
||||||
|
@ -23,6 +23,9 @@ class AgendaItemMixin(models.Model):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Container for runtime information for agenda app (on create or update of this instance).
|
Container for runtime information for agenda app (on create or update of this instance).
|
||||||
|
Can be an attribute of an item, e.g. "type", "parent_id", "comment", "duration", "weight",
|
||||||
|
or "create", which determinates, if the items should be created. If not given, the
|
||||||
|
config value is used.
|
||||||
"""
|
"""
|
||||||
agenda_item_update_information: Dict[str, Any] = {}
|
agenda_item_update_information: Dict[str, Any] = {}
|
||||||
|
|
||||||
@ -31,17 +34,20 @@ class AgendaItemMixin(models.Model):
|
|||||||
@property
|
@property
|
||||||
def agenda_item(self):
|
def agenda_item(self):
|
||||||
"""
|
"""
|
||||||
Returns the related agenda item.
|
Returns the related agenda item, if it exists.
|
||||||
"""
|
"""
|
||||||
# We support only one agenda item so just return the first element of
|
try:
|
||||||
# the queryset.
|
|
||||||
return self.agenda_items.all()[0]
|
return self.agenda_items.all()[0]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def agenda_item_id(self):
|
def agenda_item_id(self):
|
||||||
"""
|
"""
|
||||||
Returns the id of the agenda item object related to this object.
|
Returns the id of the agenda item object related to this object.
|
||||||
"""
|
"""
|
||||||
|
if self.agenda_item is None:
|
||||||
|
return None
|
||||||
return self.agenda_item.pk
|
return self.agenda_item.pk
|
||||||
|
|
||||||
def get_agenda_title_information(self):
|
def get_agenda_title_information(self):
|
||||||
|
@ -4,7 +4,9 @@ from django.apps import apps
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
from ..core.config import config
|
||||||
from ..utils.autoupdate import inform_changed_data
|
from ..utils.autoupdate import inform_changed_data
|
||||||
|
from ..utils.rest_api import ValidationError
|
||||||
from .models import Item, ListOfSpeakers
|
from .models import Item, ListOfSpeakers
|
||||||
|
|
||||||
|
|
||||||
@ -33,16 +35,51 @@ def listen_to_related_object_post_save(sender, instance, created, **kwargs):
|
|||||||
|
|
||||||
if is_agenda_item_content_object:
|
if is_agenda_item_content_object:
|
||||||
if created:
|
if created:
|
||||||
|
|
||||||
|
if instance.get_collection_string() == "topics/topic":
|
||||||
|
should_create_item = True
|
||||||
|
elif config["agenda_item_creation"] == "always":
|
||||||
|
should_create_item = True
|
||||||
|
elif config["agenda_item_creation"] == "never":
|
||||||
|
should_create_item = False
|
||||||
|
else:
|
||||||
|
should_create_item = instance.agenda_item_update_information.get(
|
||||||
|
"create"
|
||||||
|
)
|
||||||
|
if should_create_item is None:
|
||||||
|
should_create_item = config["agenda_item_creation"] == "default_yes"
|
||||||
|
|
||||||
|
if should_create_item:
|
||||||
attrs = {}
|
attrs = {}
|
||||||
for attr in ("type", "parent_id", "comment", "duration", "weight"):
|
for attr in ("type", "parent_id", "comment", "duration", "weight"):
|
||||||
if instance.agenda_item_update_information.get(attr):
|
if instance.agenda_item_update_information.get(attr):
|
||||||
attrs[attr] = instance.agenda_item_update_information.get(attr)
|
attrs[attr] = instance.agenda_item_update_information.get(attr)
|
||||||
|
# Validation: The type is validated in the serializers (to be between 1 and 3).
|
||||||
|
# If the parent id is given, set the weight to the parent's weight +1 to
|
||||||
|
# ensure the right placement in the tree. Also validate the parent_id!
|
||||||
|
parent_id = attrs.get("parent_id")
|
||||||
|
if parent_id is not None:
|
||||||
|
try:
|
||||||
|
parent = Item.objects.get(pk=parent_id)
|
||||||
|
except Item.DoesNotExist:
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"detail": f"The parent item with id {parent_id} does not exist"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
attrs["weight"] = parent.weight + 1
|
||||||
Item.objects.create(content_object=instance, **attrs)
|
Item.objects.create(content_object=instance, **attrs)
|
||||||
|
|
||||||
if not instance.agenda_item_skip_autoupdate:
|
if not instance.agenda_item_skip_autoupdate:
|
||||||
instance_inform_changed_data = True
|
instance_inform_changed_data = True
|
||||||
|
else:
|
||||||
|
is_agenda_item_content_object = False
|
||||||
|
# important for the check for item and list of speakers together.
|
||||||
|
|
||||||
elif not instance.agenda_item_skip_autoupdate:
|
elif (
|
||||||
|
not instance.agenda_item_skip_autoupdate
|
||||||
|
and instance.agenda_item is not None
|
||||||
|
):
|
||||||
# If the object has changed, then also the agenda item has to be sent.
|
# If the object has changed, then also the agenda item has to be sent.
|
||||||
inform_changed_data(instance.agenda_item)
|
inform_changed_data(instance.agenda_item)
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import jsonschema
|
import jsonschema
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.utils.autoupdate import inform_changed_data
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
@ -15,10 +16,12 @@ from openslides.utils.rest_api import (
|
|||||||
ValidationError,
|
ValidationError,
|
||||||
detail_route,
|
detail_route,
|
||||||
list_route,
|
list_route,
|
||||||
|
status,
|
||||||
)
|
)
|
||||||
from openslides.utils.views import TreeSortMixin
|
from openslides.utils.views import TreeSortMixin
|
||||||
|
|
||||||
from ..utils.auth import has_perm
|
from ..utils.auth import has_perm
|
||||||
|
from ..utils.utils import get_model_from_collection_string
|
||||||
from .access_permissions import ItemAccessPermissions
|
from .access_permissions import ItemAccessPermissions
|
||||||
from .models import Item, ListOfSpeakers, Speaker
|
from .models import Item, ListOfSpeakers, Speaker
|
||||||
|
|
||||||
@ -42,7 +45,14 @@ class ItemViewSet(ModelViewSet, TreeSortMixin):
|
|||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve", "metadata"):
|
if self.action in ("list", "retrieve", "metadata"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
elif self.action in ("partial_update", "update", "destroy", "sort", "assign"):
|
elif self.action in (
|
||||||
|
"partial_update",
|
||||||
|
"update",
|
||||||
|
"destroy",
|
||||||
|
"sort",
|
||||||
|
"assign",
|
||||||
|
"create",
|
||||||
|
):
|
||||||
result = (
|
result = (
|
||||||
has_perm(self.request.user, "agenda.can_see")
|
has_perm(self.request.user, "agenda.can_see")
|
||||||
and has_perm(self.request.user, "agenda.can_see_internal_items")
|
and has_perm(self.request.user, "agenda.can_see_internal_items")
|
||||||
@ -56,6 +66,62 @@ class ItemViewSet(ModelViewSet, TreeSortMixin):
|
|||||||
result = False
|
result = False
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Creates an agenda item and adds the content object to the agenda.
|
||||||
|
Request args should specify the content object:
|
||||||
|
{
|
||||||
|
"collection": <The collection string>,
|
||||||
|
"id": <The content object id>
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
collection = request.data.get("collection")
|
||||||
|
id = request.data.get("id")
|
||||||
|
|
||||||
|
if not isinstance(collection, str):
|
||||||
|
raise ValidationError({"detail": "The collection needs to be a string"})
|
||||||
|
if not isinstance(id, int):
|
||||||
|
raise ValidationError({"detail": "The id needs to be an int"})
|
||||||
|
|
||||||
|
try:
|
||||||
|
model = get_model_from_collection_string(collection)
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError("Invalid collection")
|
||||||
|
|
||||||
|
try:
|
||||||
|
content_object = model.objects.get(pk=id)
|
||||||
|
except model.DoesNotExist:
|
||||||
|
raise ValidationError({"detail": "The id is invalid"})
|
||||||
|
|
||||||
|
if not hasattr(content_object, "get_agenda_title_information"):
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "The collection does not have agenda items"}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
item = Item.objects.create(content_object=content_object)
|
||||||
|
except IntegrityError:
|
||||||
|
raise ValidationError({"detail": "The item is already in the agenda"})
|
||||||
|
|
||||||
|
inform_changed_data(content_object)
|
||||||
|
return Response({id: item.id})
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Removes the item from the agenda. This does not delete the content
|
||||||
|
object. Also, the deletion is denied for items with topics as content objects.
|
||||||
|
"""
|
||||||
|
item = self.get_object()
|
||||||
|
content_object = item.content_object
|
||||||
|
if content_object.get_collection_string() == "topics/topic":
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "You cannot delete the agenda item to a topic"}
|
||||||
|
)
|
||||||
|
|
||||||
|
item.delete()
|
||||||
|
inform_changed_data(content_object)
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def update(self, *args, **kwargs):
|
def update(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Customized view endpoint to update all children if the item type has changed.
|
Customized view endpoint to update all children if the item type has changed.
|
||||||
|
@ -8,6 +8,7 @@ from ..poll.serializers import default_votes_validator
|
|||||||
from ..utils.auth import get_group_model
|
from ..utils.auth import get_group_model
|
||||||
from ..utils.autoupdate import inform_changed_data
|
from ..utils.autoupdate import inform_changed_data
|
||||||
from ..utils.rest_api import (
|
from ..utils.rest_api import (
|
||||||
|
BooleanField,
|
||||||
CharField,
|
CharField,
|
||||||
DecimalField,
|
DecimalField,
|
||||||
DictField,
|
DictField,
|
||||||
@ -69,6 +70,7 @@ class MotionBlockSerializer(ModelSerializer):
|
|||||||
Serializer for motion.models.Category objects.
|
Serializer for motion.models.Category objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
agenda_create = BooleanField(write_only=True, required=False, allow_null=True)
|
||||||
agenda_type = IntegerField(
|
agenda_type = IntegerField(
|
||||||
write_only=True, required=False, min_value=1, max_value=3, allow_null=True
|
write_only=True, required=False, min_value=1, max_value=3, allow_null=True
|
||||||
)
|
)
|
||||||
@ -81,6 +83,7 @@ class MotionBlockSerializer(ModelSerializer):
|
|||||||
"title",
|
"title",
|
||||||
"agenda_item_id",
|
"agenda_item_id",
|
||||||
"list_of_speakers_id",
|
"list_of_speakers_id",
|
||||||
|
"agenda_create",
|
||||||
"agenda_type",
|
"agenda_type",
|
||||||
"agenda_parent_id",
|
"agenda_parent_id",
|
||||||
"internal",
|
"internal",
|
||||||
@ -91,9 +94,11 @@ class MotionBlockSerializer(ModelSerializer):
|
|||||||
Customized create method. Set information about related agenda item
|
Customized create method. Set information about related agenda item
|
||||||
into agenda_item_update_information container.
|
into agenda_item_update_information container.
|
||||||
"""
|
"""
|
||||||
|
agenda_create = validated_data.pop("agenda_create", None)
|
||||||
agenda_type = validated_data.pop("agenda_type", None)
|
agenda_type = validated_data.pop("agenda_type", None)
|
||||||
agenda_parent_id = validated_data.pop("agenda_parent_id", None)
|
agenda_parent_id = validated_data.pop("agenda_parent_id", None)
|
||||||
motion_block = MotionBlock(**validated_data)
|
motion_block = MotionBlock(**validated_data)
|
||||||
|
motion_block.agenda_item_update_information["create"] = agenda_create
|
||||||
motion_block.agenda_item_update_information["type"] = agenda_type
|
motion_block.agenda_item_update_information["type"] = agenda_type
|
||||||
motion_block.agenda_item_update_information["parent_id"] = agenda_parent_id
|
motion_block.agenda_item_update_information["parent_id"] = agenda_parent_id
|
||||||
motion_block.save()
|
motion_block.save()
|
||||||
@ -417,6 +422,7 @@ class MotionSerializer(ModelSerializer):
|
|||||||
workflow_id = IntegerField(
|
workflow_id = IntegerField(
|
||||||
min_value=1, required=False, validators=[validate_workflow_field]
|
min_value=1, required=False, validators=[validate_workflow_field]
|
||||||
)
|
)
|
||||||
|
agenda_create = BooleanField(write_only=True, required=False, allow_null=True)
|
||||||
agenda_type = IntegerField(
|
agenda_type = IntegerField(
|
||||||
write_only=True, required=False, min_value=1, max_value=3, allow_null=True
|
write_only=True, required=False, min_value=1, max_value=3, allow_null=True
|
||||||
)
|
)
|
||||||
@ -456,6 +462,7 @@ class MotionSerializer(ModelSerializer):
|
|||||||
"polls",
|
"polls",
|
||||||
"agenda_item_id",
|
"agenda_item_id",
|
||||||
"list_of_speakers_id",
|
"list_of_speakers_id",
|
||||||
|
"agenda_create",
|
||||||
"agenda_type",
|
"agenda_type",
|
||||||
"agenda_parent_id",
|
"agenda_parent_id",
|
||||||
"sort_parent",
|
"sort_parent",
|
||||||
@ -528,6 +535,9 @@ class MotionSerializer(ModelSerializer):
|
|||||||
motion.parent = validated_data.get("parent")
|
motion.parent = validated_data.get("parent")
|
||||||
motion.statute_paragraph = validated_data.get("statute_paragraph")
|
motion.statute_paragraph = validated_data.get("statute_paragraph")
|
||||||
motion.reset_state(validated_data.get("workflow_id"))
|
motion.reset_state(validated_data.get("workflow_id"))
|
||||||
|
motion.agenda_item_update_information["create"] = validated_data.get(
|
||||||
|
"agenda_create"
|
||||||
|
)
|
||||||
motion.agenda_item_update_information["type"] = validated_data.get(
|
motion.agenda_item_update_information["type"] = validated_data.get(
|
||||||
"agenda_type"
|
"agenda_type"
|
||||||
)
|
)
|
||||||
|
@ -19,6 +19,7 @@ from rest_framework.request import Request
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from rest_framework.serializers import (
|
from rest_framework.serializers import (
|
||||||
|
BooleanField,
|
||||||
CharField,
|
CharField,
|
||||||
DecimalField,
|
DecimalField,
|
||||||
DictField,
|
DictField,
|
||||||
@ -55,6 +56,7 @@ __all__ = [
|
|||||||
"DestroyModelMixin",
|
"DestroyModelMixin",
|
||||||
"CharField",
|
"CharField",
|
||||||
"DictField",
|
"DictField",
|
||||||
|
"BooleanField",
|
||||||
"FileField",
|
"FileField",
|
||||||
"IntegerField",
|
"IntegerField",
|
||||||
"JSONField",
|
"JSONField",
|
||||||
|
@ -77,14 +77,20 @@ class TreeSortMixin:
|
|||||||
# layer) and a weight.
|
# layer) and a weight.
|
||||||
nodes_to_check = [fake_root]
|
nodes_to_check = [fake_root]
|
||||||
# Traverse and check, if every id is given, valid and there are no duplicate ids.
|
# Traverse and check, if every id is given, valid and there are no duplicate ids.
|
||||||
weight = 1
|
|
||||||
|
# The weight values are 2, 4, 6, 8,... to "make space" between entries. This is
|
||||||
|
# some work around for the agenda: If one creates a content object with an item
|
||||||
|
# and gives the item's parent, than the weight can be set to the parent's one +1.
|
||||||
|
# If multiple content objects witht he same parent are created, the ordering is not
|
||||||
|
# guaranteed.
|
||||||
|
weight = 2
|
||||||
while len(nodes_to_check) > 0:
|
while len(nodes_to_check) > 0:
|
||||||
node = nodes_to_check.pop()
|
node = nodes_to_check.pop()
|
||||||
id = node["id"]
|
id = node["id"]
|
||||||
|
|
||||||
if id is not None: # exclude the fake_root
|
if id is not None: # exclude the fake_root
|
||||||
node[weight_key] = weight
|
node[weight_key] = weight
|
||||||
weight += 1
|
weight += 2
|
||||||
if id in ids_found:
|
if id in ids_found:
|
||||||
raise ValidationError(f"Duplicate id: {id}")
|
raise ValidationError(f"Duplicate id: {id}")
|
||||||
if id not in all_model_ids:
|
if id not in all_model_ids:
|
||||||
|
@ -27,6 +27,8 @@ class ContentObjects(TestCase):
|
|||||||
lists of speakers. Asserts, that it is recognizes as a content
|
lists of speakers. Asserts, that it is recognizes as a content
|
||||||
object and tests creation and deletion of it and the related item
|
object and tests creation and deletion of it and the related item
|
||||||
and list of speaker.
|
and list of speaker.
|
||||||
|
Tests optional agenda items with motions, e.g. motion as a content
|
||||||
|
object without an item.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -39,7 +41,15 @@ class ContentObjects(TestCase):
|
|||||||
def test_topic_is_list_of_speakers_content_object(self):
|
def test_topic_is_list_of_speakers_content_object(self):
|
||||||
assert hasattr(Topic(), "get_list_of_speakers_title_information")
|
assert hasattr(Topic(), "get_list_of_speakers_title_information")
|
||||||
|
|
||||||
def test_create_content_object(self):
|
def test_motion_is_agenda_item_content_object(self):
|
||||||
|
assert hasattr(Motion(), "get_agenda_title_information")
|
||||||
|
|
||||||
|
def test_motion_is_list_of_speakers_content_object(self):
|
||||||
|
assert hasattr(Motion(), "get_list_of_speakers_title_information")
|
||||||
|
|
||||||
|
def test_create_topic(self):
|
||||||
|
# Disable autocreation. Topics should create agenda items anyways.
|
||||||
|
config["agenda_item_creation"] = "never"
|
||||||
topic = Topic.objects.create(title="test_title_fk3Oc209JDiunw2!wwoH")
|
topic = Topic.objects.create(title="test_title_fk3Oc209JDiunw2!wwoH")
|
||||||
|
|
||||||
assert topic.agenda_item is not None
|
assert topic.agenda_item is not None
|
||||||
@ -51,7 +61,7 @@ class ContentObjects(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
def test_delete_content_object(self):
|
def test_delete_topic(self):
|
||||||
topic = Topic.objects.create(title="test_title_lwOCK32jZGFb37DpmoP(")
|
topic = Topic.objects.create(title="test_title_lwOCK32jZGFb37DpmoP(")
|
||||||
item_id = topic.agenda_item_id
|
item_id = topic.agenda_item_id
|
||||||
list_of_speakers_id = topic.list_of_speakers_id
|
list_of_speakers_id = topic.list_of_speakers_id
|
||||||
@ -63,6 +73,65 @@ class ContentObjects(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
def test_prevent_removing_topic_from_agenda(self):
|
||||||
|
topic = Topic.objects.create(title="test_title_lwOCK32jZGFb37DpmoP(")
|
||||||
|
item_id = topic.agenda_item_id
|
||||||
|
response = self.client.delete(reverse("item-detail", args=[item_id]))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_adding_topic_twice(self):
|
||||||
|
topic = Topic.objects.create(title="test_title_lwOCK32jZGFb37DpmoP(")
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("item-list"),
|
||||||
|
{"collection": topic.get_collection_string(), "id": topic.id},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_enabled_auto_adding_item_for_motion(self):
|
||||||
|
config["agenda_item_creation"] = "always"
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_F3pApc3em9zIGCie2iwf",
|
||||||
|
"text": "test_text_wcnLVzezeLcnqlqlC(31",
|
||||||
|
"agenda_create": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
motion = Motion.objects.get()
|
||||||
|
self.assertTrue(motion.agenda_item is not None)
|
||||||
|
self.assertEqual(motion.agenda_item_id, motion.agenda_item.id)
|
||||||
|
|
||||||
|
def test_disabled_auto_adding_item_for_motion(self):
|
||||||
|
config["agenda_item_creation"] = "never"
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_OoCoo3MeiT9li5Iengu9",
|
||||||
|
"text": "test_text_thuoz0iecheiheereiCi",
|
||||||
|
"agenda_create": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
motion = Motion.objects.get()
|
||||||
|
self.assertTrue(motion.agenda_item is None)
|
||||||
|
self.assertTrue(motion.agenda_item_id is None)
|
||||||
|
|
||||||
|
def test_ask_auto_adding_item_for_motion(self):
|
||||||
|
config["agenda_item_creation"] = "default_no"
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("motion-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_wvlvowievgbpypoOV332",
|
||||||
|
"text": "test_text_tvewpxxcw9r72qNVV3uq",
|
||||||
|
"agenda_create": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
motion = Motion.objects.get()
|
||||||
|
self.assertTrue(motion.agenda_item is not None)
|
||||||
|
self.assertEqual(motion.agenda_item_id, motion.agenda_item.id)
|
||||||
|
|
||||||
|
|
||||||
class RetrieveItem(TestCase):
|
class RetrieveItem(TestCase):
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user